ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

证券知识库构建实战:从文档解析到RAG检索优化

证券知识库构建实战:从文档解析到RAG检索优化 简介一份围绕证券知识库构建和应用的PPT面向金融科技、大模型应用方向从业者与学习者系统拆解金融文档如何转化为可检索、可问答的知识库。包体为1个PPTX文件压缩包约14.69MB单文件但内容密度高。内容从大模型应用范式切入重点展示文档结构化解析流程包括跨页段落合并、无框线表格还原、表格合并与单元格合并以及结合OCR对扫描件签章、页眉页脚进行识别知识库构建部分涵盖文本切片、向量化Embedding、监督与弱监督微调等环节并给出量化效果提升数据问答应用则结合用户意图识别支撑目录生成、数据清洗和智能分析。通过完整PPT演示与案例梳理可掌握从文档解析到检索问答的整体链路理解大规模公告场景下的索引、分类与存储思路对构建金融RAG应用有直接参考价值。目前已有52人学习适合希望了解证券知识库落地方法的学习者。1. 证券知识库先从“读文档”说起建设大模型驱动的证券知识库最先卡住的往往不是Embedding选型而是“文档根本读不进去”。证券行业一年新增上市公司公告400万篇以上原生PDF、扫描件、研报、PPT版式混杂跨页段落、无框线表格、签章压字、单元格合并这些特征几乎无处不在。RAG链路里召回做得再好文档解析环节丢掉的字段永远补不回来。深交所信息公司毛瑞彬在《证券知识库构建和应用》演讲里把从布局检测、表格还原、文本切片到Embedding微调再到RAG问答的完整链路梳理得很细。对正在用dify、LangChain或自研流水线搭企业知识库的工程师来说理解这条链路上每个环节如何互相制约比单独调一个向量模型参数更有价值。下面对照这个真实项目展开按“版面解析 → 表格还原 → 切片与向量化 → 存储设计 → 验证调优”的顺序把细节和参数说透可以直接拿来做参考实现。2. 布局检测与阅读顺序让PDF恢复成可读文档2.1 布局检测为什么是知识库的第一道门槛证券文档和普通网页文本最大的区别是版式承载信息。一份港股研报常被切成两栏或三栏正文在左表格在右页脚还有免责声明一份招股书里经常出现同一段落在第5页底部开始、又延续到第6页顶部的情况。如果不先做布局检测就按原PDF顺序抽文本切出来的chunk里会混入表格列碎片、页眉页脚和多栏交叉内容后续向量化拿到的输入就是乱的。布局检测的思路是训练一个版面元素检测器识别段落、表格、标题、图片、页眉页脚这几类基本元素再依靠这些元素的坐标框做阅读顺序重建。演讲里展示的检测模块结构和YOLO系目标检测器几乎一致图片输入后经过Focus下采样再接带自注意力机制的卷积层做特征提取之后经过CSP多尺度特征融合与SPPF最后进入Detection头输出元素类别和边界框。把通用目标检测框架直接改造成版面元素检测是当前金融文档解析落地最稳的技术路线因为这类框架对“小目标”“密集排列”的检测能力经过了大量验证而版面元素恰恰具备这两个特征。2.2 阅读顺序恢复跨页与多栏的处理逻辑布局检测输出的是坐标框离知识库可用还差一步——把文本框按人类阅读顺序串起来。直接按“从上到下、从左到右”排序在多栏研报上会出现左栏读完一半又跳到右栏的情况。常见做法是先按y坐标做行带聚类行带内再按x排序同时在行与行之间检查是否存在横跨两页的文本块存在则用竖切线把页面切成两个区域分别处理。下面这段代码模拟了最小可用的阅读顺序恢复逻辑def restore_reading_order(boxes, y_overlap0.8, x_gap10): # boxes: [{text: str, x1: float, y1: float, x2: float, y2: float}] # 第一步:按y坐标聚类成“行带”,同一行带内的块共享水平投影区间 lines [] for box in sorted(boxes, keylambda b: (b[y1], b[x1])): target None for line in lines: inter min(box[y2], line[y2]) - max(box[y1], line[y1]) h min(box[y2] - box[y1], line[y2] - line[y1]) if h 0 and inter / h y_overlap: target line break if target: target[boxes].append(box) target[y1] min(target[y1], box[y1]) target[y2] max(target[y2], box[y2]) else: lines.append({y1: box[y1], y2: box[y2], boxes: [box]}) # 第二步:行带内按x坐标排序,遇到x间隔大于x_gap就插入列分隔标记 ordered [] for line in lines: line_boxes sorted(line[boxes], keylambda b: b[x1]) for i, b in enumerate(line_boxes): if i 0 and b[x1] - line_boxes[i - 1][x2] x_gap: ordered.append(col_split) ordered.append(b[text]) return orderedy_overlap控制公共水平切线的判定严苛程度。设为0.8意味着两个文本框垂直投影重叠比例达到80%才算同一行带调大会把正文和页脚判定成一行调小则会把同一段落切成两截。x_gap控制竖切线的最小间距常见PDF正文栏目间距在20像素以上取10到15像素能正确切分取太大又会把同一栏内的表格列误判成新栏。注意这里只针对原生PDF的文本层。扫描件需要先经过OCR把识别结果的坐标作为box传入再走同一套阅读顺序逻辑。2.3 开源方案与自训练模型怎么选文档解析链条上最常被问的问题是“能不能直接用PaddleOCR PP-StructureV2或者干脆用PyMuPDF按位置切”。两者都可以但都有边界。按实际使用对比选型多栏处理表格还原扫描件支持标注与训练成本适合场景PyMuPDF按坐标切块差无无零单栏原生PDF快速验证PaddleOCR PP-StructureV2中中好零通用文档首版上线LayoutLMv3中中依赖OCR前置高需要整页语义理解的场景自训练YOLO检测器好需后处理配合依赖OCR前置版式标注训练公告、研报等固定版式证券公告版式高度固定类别也就是段落、表格、标题、图片、页眉页脚这几类标注几百页就能把边角案例覆盖到七成自训练模型更有优势。PP-StructureV2适合先跑通链路版式复杂到影响上线时再切自训练模型。布局检测输出不要只存类别和坐标还要顺手记录检测置信度低置信度块建议直接丢弃避免把页眉页脚噪声带进切片流程。3. 无框线表格还原与扫描件OCR证券文档的视觉重建3.1 无框线表格的列边界怎么推断证券文档里的表格是知识库数据密度最高的部分也是最难还原的部分。招股书财务摘要里大量使用无框线表格只有横线或者干脆连横线都没有文字按列对齐摆放。OCR或PDF文本抽取得到的是一个个孤立的文本块没有行列归属直接按文本流顺序读取会把表格读成段落。布局检测只能给出表格整体所在的边框表格内部的单元格划分要靠列边界推断。常见做法是把无框线表格还原拆成两步先按y坐标把文本行聚类成表格行再在每一行内根据文本块的x坐标间距判断列边界。行聚类和阅读顺序恢复类似列边界则需要处理“同一行内左右两列间距”与“同一列内相邻字段间距”的差异。def estimate_cell_boundaries(text_boxes, center_y, col_gap18): # text_boxes: 表格区域内的文本块坐标 [(x1, y1, x2, y2, text), ...] # center_y: 当前表格行的中心y坐标 boxes [ b for b in text_boxes if abs((b[1] b[3]) / 2 - center_y) 8 ] if not boxes: return [] boxes sorted(boxes, keylambda b: b[0]) cells, current [], [boxes[0]] for b in boxes[1:]: # 无框线表格的列边界只能看x间隙,阈值按文档dpi实际调整 if b[0] - current[-1][2] col_gap: cells.append(merge_boxes(current)) current [b] else: current.append(b) if current: cells.append(merge_boxes(current)) return cellscol_gap取值很难一次定死。更稳的做法不是固定阈值而是把文本块按左边界x坐标分桶然后检查同一桶内的数字是否按十进制对齐。财务表格里的数字列如果右对齐x坐标直方图会呈现明显的边界峰用直方图波谷做列边界比固定阈值可靠得多。3.2 跨页表格合并与单元格还原表格从第3页顶部跨到第4页底部属于跨页表格表格相邻行被分到两页PDF文本层会按页切段。跨页表格合并的做法是先通过表格检测框判断当前页表格是否延续到页面边界如果是则到下一页找同列结构的表格并按行号续接。重复表头是另一类高频问题合并时要剔除续接页的重复表头行否则检索结果里会重复出现两份表头。单元格合并涉及列跨度和行跨度还原时需要保留rowspan、colspan信息转成Markdown输出时直接映射。PPT里提到的“表格内图片还原”常见做法是检测表格区域里是否包含图片框存在则把图片单独存成一个元素并在表格结构里预留一个图片占位符之后再通过OCR识别图片框区域内的文字并回填。不做这个处理印章或logo会直接吃掉相邻表格的文本。表格问题还原策略失败特征无框线表格行聚类列边界推断数字列错位或文本粘连跨页表格表头去重行号续接检索结果重复出现表头单元格合并保留rowspan/colspan映射Markdown合并单元格内容丢失表格内图片图片独立存储OCR回填表格中图片区域全空3.3 扫描件OCR与目录生成签章压字怎么办扫描件识别跟原生PDF是两套逻辑。原生PDF可以直接取文本层扫描件必须先过OCR。签章覆盖文字是证券公告里最典型的OCR难点红色印章和黑色文字的叠加区域识别结果往往会出现大段乱码。我一般会先用HSV颜色空间做红色通道过滤再把过滤后的图像送OCRimport cv2 import numpy as np def erase_red_stamp(page_img): # 输入为BGR图像,把红色签章区域置白后返回,供OCR使用 hsv cv2.cvtColor(page_img, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, (0, 120, 120), (10, 255, 255)) # 红色签章边缘不规则,先膨胀再接回原图 kernel np.ones((3, 3), np.uint8) mask cv2.dilate(mask, kernel, iterations2) page_img[mask 0] (255, 255, 255) return page_img这里的(0, 120, 120)到(10, 255, 255)针对鲜红印章。暗红色或粉色印章需要把H通道上限放宽到179同时把S阈值降到80否则印章残留会让OCR结果里出现大量无意义字符。更稳的方案是用检测模型把印章框出来OCR只处理印章框之外的区域再做一次文字识别结果叠加。目录识别是布局检测后处理里最容易漏的环节。步骤是布局识别先定位标题坐标再做文档类型分类原生PDF抽文本、扫描件走OCR拿到原文目录后做数据清洗最后用大模型生成结构化目录。生成的目录树不只是导航后面做检索过滤时按目录树切分比纯按段落切效果好得多。4. 文本切片与BGE-M3微调召回率从62.7%到73.3%的实验路径4.1 切片策略直接决定召回上限PPT对文本切片给出了三种策略的对比按字数切片解析要求低、操作简单、向量存储占用小检索后不需要回溯原文但切片内容混乱段落被截断按段落切片能保持段落完整小标题和简短段落的问答效果好但向量存储占用大检索后需要回溯拆分后合并切片不丢失段落语义向量存储和检索速度都更优难点是短文本单独成块的场景不够友好。实际项目里需要按数据形态混用。证券公告普遍存在三级小标题如果把“1.1 经营情况讨论与分析”这种短段单独切片向量化后和正文段落的相似度会非常低问答时小标题对应的正文反而召不回来。合理的做法是让短段落与后面的正文合并成一个chunk同时保留目录树字段使检索命中后仍能还原章节关系。def hybrid_split_chunks(text, max_chars700, min_chars80): paras [p.strip() for p in text.splitlines() if p.strip()] chunks, buffer [], for para in paras: if len(buffer) len(para) max_chars: buffer (\n if buffer else ) para elif len(para) min_chars: # 小标题这类短段并入前一个chunk,避免形成语义孤岛 if buffer: buffer \n para else: if buffer: chunks.append(buffer.strip()) segments split_long_paragraph(para, max_chars) chunks.extend(segments) buffer if buffer: chunks.append(buffer.strip()) return chunksmax_chars和min_chars是生产环境中最难调的两个参数。max_chars要低于Embedding模型的max_seq_length以BGE-M3默认512字符窗口为例超过窗口的内容会被截断语义信息直接丢失。min_chars低于50会把小标题合并进前一段高于100又会把正常的短段落合并成一块检索时定位不精确。三种切片策略的取舍可以按下表理解策略解析要求段落完整性向量存储占用检索后回溯适合阶段按字数切片低差小不需要快速验证链路按段落切片中好大需要生产级RAG拆分合并切片高较好中需要长文档密集场景4.2 BGE-M3微调有监督与弱监督两条路PPT里记录的数据很有参考价值用C-MTP数据集在BGE-M3上进行有监督微调和弱监督微调微调后recall10由62.7%提升到73.3%recall20由73.7%提升到83.9%。这两组数据说明微调带来的收益是实质性的尤其在证券领域“经营性现金流”和“经营活动产生的现金流量净额”这类术语差异场景通用模型很难建立同义关系。有监督微调使用“query 正例 负例”三元组训练。以FlagEmbedding库为例训练数据格式如下[ { query: 万科2023年第三季度经营性现金流净额, pos: [万科A2023年第三季度报告经营活动产生的现金流量净额为...], neg: [万科A2023年第三季度报告营业收入为...] }, { query: 中信证券2024年债券承销排名, pos: [2024年度债券承销排行榜显示中信证券承销金额...], neg: [2024年度股票承销排行榜显示...] } ]微调脚本from FlagEmbedding import BGEM3FlagModel # 有监督微调: data/finance_qa_sft.jsonl 存放上面的query/pos/neg三元组 model BGEM3FlagModel(BAAI/bge-m3, use_fp16True) model.load_finetune( model_name_or_pathBAAI/bge-m3, train_datadata/finance_qa_sft.jsonl, output_dirmodels/bge-m3-sft, batch_size16, lr2e-5, epochs3, max_len512, )这段代码的API对应FlagEmbedding开源仓库中较新的版本不同版本参数名会有差异核心配置是这几个max_len不超过模型上限超过512的部分会被截断epochs设3到5轮过高会在验证集上出现过拟合batch_size由显存决定单卡A100可以设3224G显存建议限制在8到16。4.3 弱监督与负样本挖掘的细节弱监督训练绕不开负样本质量问题。没有人工标注时负样本通常靠向量检索的topk结果来挖掘把query对应的正样本替换掉取向量相似度靠前但标签不同的文本作为负样本。这套流程的问题是证券文档里同一份公告的正文和摘要相似度极高如果不做聚类去重模型会被同源文本反复带偏。做法分三步第一步用未微调的BGE-M3对无标签段落做向量化第二步对段落做聚类组内抽样第三步对每个query从聚类中心附近抽取3到5个负样本。这样负样本分布更均匀模型不会因为某个术语出现频率过高而失衡。PPT里的指标提升正是建立在这一套“清洗问答语料—聚类—挖掘负样本—微调”的流程之上。5. 源数据表、检索表与坐标回溯RAG链路存储设计5.1 双表结构的分工逻辑知识库存储通常被理解成“把文本切片后丢进向量库”但证券公告场景必须保留原始元素。PPT给出的存储方案是两张表源数据表按PDF文档元素存储包括段落、表格、图片检索表根据检索需求把段落合并成分块存S3链接、坐标和目录树结构。双表结构的核心原因是向量检索返回的是切块而用户需要的是可溯源的原文。CREATE TABLE doc_element ( element_id BIGINT AUTO_INCREMENT PRIMARY KEY, doc_id VARCHAR(64) NOT NULL, elem_type ENUM(paragraph,table,image,title,header,footer) NOT NULL, page_no INT NOT NULL, bbox JSON COMMENT 元素坐标x1,y1,x2,y2, raw_content JSON COMMENT 段落纯文本或表格结构化JSON, s3_url VARCHAR(512), toc_path JSON COMMENT 目录树路径, KEY idx_doc_page (doc_id, page_no) ) ENGINEInnoDB; CREATE TABLE retrieval_chunk ( chunk_id BIGINT AUTO_INCREMENT PRIMARY KEY, doc_id VARCHAR(64) NOT NULL, chunk_text MEDIUMTEXT NOT NULL, element_ids JSON COMMENT 关联doc_element的element_id列表, vector_id VARCHAR(64), model_name VARCHAR(32), page_range VARCHAR(16), KEY idx_doc_chunk (doc_id) ) ENGINEInnoDB;每个chunk通过element_ids关联多个源元素这样检索命中后能拿到原始表格、段落甚至图片而不是被切得七零八落的一小段文字。doc_id、page_no、toc_path三个字段的组合可以直接定位到某一份公告的某一页某个目录小节。s3_url指向原始扫描件或PDF必要的时候让大模型回答时带上引用链接。5.2 检索命中后如何拼回上下文从检索表取得chunk后通过element_ids从doc_element取回原始内容按元素类型转换成Markdown表格或图片占位符再拼进大模型提示词def build_context_from_chunk(chunk_row, db): element_ids json.loads(chunk_row[element_ids]) elems db.query( SELECT * FROM doc_element WHERE element_id IN %s ORDER BY page_no, (element_ids,) ) context_parts [] for elem in elems: if elem[elem_type] table: # 表格在源表存的是结构化JSON,转成Markdown才能被LLM读明白 context_parts.append(json_to_markdown_table(elem[raw_content])) elif elem[elem_type] image: context_parts.append(f[图片:{elem[s3_url]}]) else: context_parts.append(elem[raw_content]) return \n.join(context_parts)表格如果直接从检索表text字段取出会把行列结构丢失大模型读到的只是一串文本流。见过不少项目在这个点翻车检索表里的文本chunk看起来完整但表格数据被拍平成一段模型回答数字和日期时经常对不上。回溯到源表恢复表格结构是RAG问答质量稳定的前提。5.3 向量库选型和元数据过滤400万篇公告规模下向量库选型不能只看检索速度还要看元数据过滤能力。用户查询“万科2024年年报的现金流”需要在向量召回前先把“万科”“年报”“2024”这三个条件过滤掉否则top100里大部分是不相关主体的公告。方案混合检索标量过滤运维复杂度合适规模Milvus支持支持高千万级向量以上pgvector中等支持低百万级和业务库同源Elasticsearch支持支持高已有ES基础设施时Redis Search限制支持低热数据缓存Milvus过滤加检索的写法from pymilvus import Collection collection Collection(sec_chunk) collection.load() results collection.search( data[query_vector], anns_fieldembedding, param{metric_type: IP, params: {nprobe: 16}}, limit20, exprdoc_year in [2024] and doc_type 年报 and subject 万科A, output_fields[chunk_id, doc_id, page_no] )nprobe设为16时如果经过元数据过滤后候选集已经缩小调到8也不会明显掉召回。Milvus不同版本的expr语法略有差异上线前先在预发环境验证表达式。如果系统里已有pgvector且向量规模不大不建议为了向量检索单独引入Milvus运维成本会显著增加。6. 验证与调优用recall指标复盘一次完整的检索实验6.1 测试集构建的硬性约束做一轮可对比的评估必须有能复用的测试集。参考PPT里的方法测试集需要覆盖三条主线主体维度万科、中信证券、贵州茅台这类实体时间维度“2023年”“2024年一季度”这类限定同义表达维度“经营性现金流”对应“经营活动产生的现金流量净额”。5000条不算多但每条应来自真实用户查询而不是从公告标题改写出来。测试集只是去公告里选几句漂亮话当query微调后的指标虚高就没有参考价值。6.2 评估脚本与参数调整评估脚本固定top_k才能在不同模型版本之间公平比较def eval_recall(retriever, queries, gold_chunk_ids, top_k10): hit 0 for q, gold in zip(queries, gold_chunk_ids): results retriever.search(q, top_ktop_k) if gold in {r.chunk_id for r in results}: hit 1 return hit / len(queries)参数调整建议参数初始值调优方向反馈信号max_chars700500到1000之间观察长文本分块后的碎片化程度min_chars80调到30或更低小标题是否单独命中负样本数3增至5到8验证集recall是否持续上升epochs3超过3轮观察验证集召回率是否回落nprobe16提及32延迟膨胀与召回提升的权衡top_k10按业务需求调场景若是精确数字问答建议把这套评估脚本固化在CI里。每次改切片策略或Embedding模型先跑一遍recall10和recall20低于当前基线的改动直接回滚。证券知识库是长线建设评估集比模型本身更值钱。本文还有配套的精品资源点击获取
返回列表