news 2026/9/17 17:49:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
证券知识库构建实战:从文档解析到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_overlap=0.8, x_gap=10): # boxes: [{'text': str, 'x1': float, 'y1': float, 'x2': float, 'y2': float}] # 第一步:按y坐标聚类成“行带”,同一行带内的块共享水平投影区间 lines = [] for box in sorted(boxes, key=lambda 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'], key=lambda 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 ordered

y_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_gap=18): # 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, key=lambda 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 cells

col_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颜色空间做红色通道过滤,再把过滤后的图像送OCR:

import 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, iterations=2) 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_chars=700, min_chars=80): 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 chunks

max_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上进行有监督微调和弱监督微调,微调后recall@10由62.7%提升到73.3%,recall@20由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_fp16=True) model.load_finetune( model_name_or_path="BAAI/bge-m3", train_data="data/finance_qa_sft.jsonl", output_dir="models/bge-m3-sft", batch_size=16, lr=2e-5, epochs=3, max_len=512, )

这段代码的API对应FlagEmbedding开源仓库中较新的版本,不同版本参数名会有差异,核心配置是这几个:max_len不超过模型上限,超过512的部分会被截断;epochs设3到5轮,过高会在验证集上出现过拟合;batch_size由显存决定,单卡A100可以设32,24G显存建议限制在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) ) ENGINE=InnoDB; 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) ) ENGINE=InnoDB;

每个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_field="embedding", param={"metric_type": "IP", "params": {"nprobe": 16}}, limit=20, expr='doc_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_k=10): hit = 0 for q, gold in zip(queries, gold_chunk_ids): results = retriever.search(q, top_k=top_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模型,先跑一遍recall@10和recall@20,低于当前基线的改动直接回滚。证券知识库是长线建设,评估集比模型本身更值钱。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 17:48:23

国产电源芯片选型避坑指南:DC-DC与LDO可靠性实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 17:47:31

CAN总线调试实战:物理层、协议栈与接收机制避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 17:46:21

AI论文平台:提升科研效率的智能工具

1. 学术研究效率革命&#xff1a;AI论文平台的价值解析在科研领域&#xff0c;文献检索与阅读时间约占学者工作量的40%。传统学术搜索引擎往往存在结果冗余、相关性差、更新滞后等问题。2026年新一代AI论文平台通过深度学习与自然语言处理技术&#xff0c;实现了从"人找信…

作者头像 李华
网站建设 2026/9/17 17:41:42

GARCH模型实战:波动率建模、ARCH检验与VaR预测

简介&#xff1a;这份PPT课件面向金融学、计量经济学方向的学生与研究人员&#xff0c;系统讲解GARCH类模型在金融时间序列波动性分析中的原理与应用。内容从Engle提出的ARCH过程切入&#xff0c;梳理条件异方差性的来源与ARCH(q)的建模思路&#xff0c;进而展开GARCH(1,1)及高…

作者头像 李华
网站建设 2026/9/17 17:41:33

机器人基础模型OM-1解析:范式变革、技术原理与落地挑战

先说个背景。这几年“基础模型”这个词在AI圈已经被说烂了&#xff0c;从大语言模型到多模态模型&#xff0c;现在终于烧到了机器人领域。RewardAI这次发布的OM-1&#xff0c;名字听起来低调&#xff0c;但“机器人基础模型”这个定位本身就值得仔细拆一下。它不是某个机械臂的…

作者头像 李华