news 2026/10/5 17:11:53

证券知识库构建与应用:从文档解析到RAG问答的落地实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
证券知识库构建与应用:从文档解析到RAG问答的落地实践

简介:这是一份面向金融科技从业者、大模型应用开发者与证券数据工程师的证券知识库构建与应用演示文稿,聚焦大模型与知识库结合的落地路径,帮助读者理解如何将海量证券信息转化为可检索、可问答的智能知识资产。压缩包内共1个pptx文件,约14.69MB,以幻灯片形式系统梳理背景、文档结构化解析、知识库构建与问答、应用案例及思考等模块。内容涵盖跨页段落合并、无框线表格还原、跨页表格合并、单元格合并、表格内图片还原、多栏阅读顺序分割、扫描件签章覆盖文字识别及目录页眉页脚处理等版式解析要点,并涉及用户意图识别、文本切片策略、向量化Embedding、监督与弱监督微调、检索表与源数据表设计等关键环节。目前已有52人学习,适合需要搭建证券领域知识库、优化检索召回或设计智能问答系统的读者参考,可从中获取从文档解析到向量检索的完整技术框架与工程思路。

1. 证券知识库构建和应用:从散落文档到可检索问答的落地路径

券商研究所每天产出几十份研报,经纪业务线攒了上百个产品说明书,合规部门还有一堆监管问答和内部制度。这些文档散在共享盘、邮件附件和 OA 系统里,想查一个「某类产品适当性匹配规则」得翻三个系统、问两个人。证券知识库构建和应用要解决的就是这件事:把非结构化文档变成可检索、可问答、可追溯的知识资产。它适合有大量文档沉淀、但检索效率低下的证券从业团队,也适合想用 RAG 思路做垂直领域问答的工程师。核心链路是文档解析、切片、向量化、检索、生成五步,每一步都有证券行业的特殊坑。

2. 证券文档解析:PDF 表格和扫描件为什么总翻车

2.1 证券文档的三种典型格式与解析选型

证券行业的文档格式比通用场景复杂得多。第一类是交易所公告和招股书,以 PDF 为主,排版规整但表格密集;第二类是内部制度文件,Word 和 PDF 混用,版本迭代频繁;第三类是扫描件,比如盖章的纸质合同和传真件,需要 OCR 才能提取文字。

选型上,我一般按这个优先级处理:原生 PDF 优先用 pdfplumber 或 PyMuPDF 提取文本层,Word 用 python-docx,扫描件走 PaddleOCR 或 Tesseract。不要一上来就全量 OCR,原生 PDF 的文本层精度远高于 OCR,而且速度快一个数量级。只有确认没有文本层时才走 OCR 分支。

import pdfplumber from pathlib import Path def extract_pdf_text(pdf_path: str) -> list[dict]: """提取 PDF 每页文本,保留页码用于溯源""" pages = [] with pdfplumber.open(pdf_path) as pdf: for i, page in enumerate(pdf.pages): text = page.extract_text() or "" # 表格单独提取,后续结构化处理 tables = page.extract_tables() pages.append({ "page_num": i + 1, "text": text, "tables": tables, "source": Path(pdf_path).name }) return pages

这段代码的关键点是保留page_num和source,证券问答场景里溯源和引用来源是刚需,用户问「这个规则出自哪份文件第几页」必须能答上来。extract_tables()返回的是嵌套列表,后续需要转成 Markdown 或 JSON 再切片,否则表格里的数字和表头会在切片时被拆散,检索时匹配不到完整语义。

2.2 表格和附件的结构化处理步骤

证券文档里最有价值的信息往往在表格里:费率表、适当性匹配矩阵、持仓限额表。这些表格如果按纯文本切片,一行数据被切成三段,检索时基本废掉。我的做法是先把表格转成 Markdown 格式,再按表格整体作为一个 chunk 入库。

def table_to_markdown(table: list[list]) -> str: """将 pdfplumber 提取的表格转为 Markdown,保留表头语义""" if not table or not table[0]: return "" header = table[0] rows = table[1:] md = "| " + " | ".join(str(c or "") for c in header) + " |\n" md += "| " + " | ".join("---" for _ in header) + " |\n" for row in rows: md += "| " + " | ".join(str(c or "") for c in row) + " |\n" return md

参数说明:table是 pdfplumber 返回的二维列表,第一行默认当表头。如果表格没有表头(比如跨页续表),需要手动补一个占位表头,否则 Markdown 渲染会错位。转成 Markdown 后,表格作为一个完整 chunk 存入向量库,检索时表头信息不会丢失。

附件处理是另一个坑。证券文档经常在正文里写「详见附件一」,附件单独一个文件。解析时需要建立正文和附件的关联关系,否则检索到正文的引用但找不到附件内容。常见做法是在元数据里加一个related_docs字段,把主文档和附件的 ID 关联起来。

3. 切片与向量化:证券术语的语义保持策略

3.1 按语义边界切片而不是按字数硬切

通用 RAG 教程喜欢按 512 或 1024 字符硬切,这在证券场景下会出大问题。比如「连续二十个交易日收盘价跌幅累计超过百分之三十」这句话,硬切可能把「百分之三十」切到下一个 chunk,检索时匹配到「跌幅累计超过」但阈值丢了,生成的回答就是错的。

我一般用递归切片加语义边界优先的策略:先按段落切,段落超过阈值再按句子切,句子还超才按字符切。LangChain 的RecursiveCharacterTextSplitter支持自定义分隔符优先级,把中文句号、分号、换行符排在前面。

from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=80, separators=["\n\n", "\n", "。", ";", ",", ""], length_function=len, ) chunks = splitter.split_text(full_text)

参数说明:chunk_size=600是证券文档的经验值,太小会丢上下文,太大检索精度下降。chunk_overlap=80保证跨 chunk 的句子有重叠,避免边界信息丢失。separators的顺序很重要,中文句号优先于逗号,逗号优先于空字符串,这样切出来的 chunk 尽量在语义完整的位置断开。

3.2 证券术语的向量化模型选择与微调思路

通用 embedding 模型对证券术语的区分度不够。「融资融券」和「转融通」在通用模型里向量很接近,但业务上完全是两回事。选型时优先考虑在金融语料上继续预训练过的模型,比如在中文金融文本上做过领域适应的 BGE 系列。

如果检索准确率还是不达标,可以考虑微调。构造正负样本对:正样本是同一业务规则的不同表述,负样本是不同业务规则的相似表述。用对比学习损失微调,通常几百对样本就能看到明显提升。

# 微调样本构造示例(伪代码,展示数据结构) train_pairs = [ { "anchor": "融资融券维持担保比例不得低于130%", "positive": "信用账户维持担保比例低于130%将触发平仓", "negative": "转融通保证金比例不得低于20%" }, # ... 更多样本对 ]

注意:微调 embedding 模型需要 GPU 资源,如果团队没有这个条件,退而求其次的做法是在检索时加一层关键词过滤,把证券术语作为必须匹配的 token,先过滤再向量检索。这个方案工程上更简单,效果也能接受。

4. 检索与问答:把召回率从60%拉到90%的四个手段

4.1 混合检索:向量加关键词的互补逻辑

纯向量检索在证券场景下召回率大概 60% 到 70%,漏掉的主要是数字、日期和专有名词。比如用户问「2024年3月修订的适当性管理办法」,向量检索可能召回一堆适当性相关文档,但日期不对。混合检索的思路是向量检索和 BM25 关键词检索各跑一遍,然后融合排序。

from rank_bm25 import BM25Okapi import jieba # 构建 BM25 索引 tokenized_corpus = [list(jieba.cut(doc)) for doc in documents] bm25 = BM25Okapi(tokenized_corpus) def hybrid_search(query: str, top_k: int = 10): # 向量检索 query_vec = embed_model.encode(query) vec_results = vector_store.search(query_vec, top_k=top_k) # BM25 检索 tokenized_query = list(jieba.cut(query)) bm25_scores = bm25.get_scores(tokenized_query) bm25_results = sorted( range(len(bm25_scores)), key=lambda i: bm25_scores[i], reverse=True )[:top_k] # 融合:RRF 倒数排名融合 fused = reciprocal_rank_fusion(vec_results, bm25_results) return fused

参数说明:top_k=10是每路召回的数量,融合后取前 5 送入生成。reciprocal_rank_fusion是倒数排名融合算法,不依赖分数绝对值,只依赖排名,适合向量分数和 BM25 分数不可比的情况。jieba 分词对证券术语的分割效果一般,可以加载自定义词典把「融资融券」「维持担保比例」等词加进去。

4.2 重排序与上下文压缩的工程取舍

混合检索召回 10 条,但生成模型只需要最相关的 3 到 5 条。重排序模型(reranker)的作用是对召回结果精排,把真正相关的排到前面。证券场景下重排序的收益很明显,因为很多文档表面相似但业务规则完全不同。

from sentence_transformers import CrossEncoder reranker = CrossEncoder("BAAI/bge-reranker-base") def rerank(query: str, candidates: list[str], top_n: int = 5): pairs = [[query, doc] for doc in candidates] scores = reranker.predict(pairs) ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) return [doc for doc, _ in ranked[:top_n]]

重排序的代价是延迟增加,CrossEncoder 对 10 条候选打分大概增加 200 到 500 毫秒。如果对延迟敏感,可以只对前 5 条重排,或者用更小的 reranker 模型。上下文压缩是另一个手段:把召回的长文档压缩成只保留和 query 相关的句子,减少生成模型的输入长度,同时降低无关信息干扰。

5. 避坑指南:证券知识库落地中最容易翻车的五个点

5.1 文档版本混乱导致检索到过期规则

现象:用户问「当前生效的适当性规则」,系统返回了 2022 年已废止的版本。原因:知识库没有版本管理,新旧文档同时入库,向量检索按语义相似度召回,旧版本表述可能更接近 query。解决:入库时给每份文档打上effective_date和status标签,检索时默认过滤status=active,需要查历史版本时显式指定。

5.2 表格切片后表头丢失

现象:检索到「管理费率 1.5%」,但不知道是哪个产品的费率。原因:表格按行切片,表头被切到单独的 chunk 里。解决:表格整体作为一个 chunk,或者在每行数据前拼接表头信息,比如「产品A | 管理费率 | 1.5%」。

5.3 数字和百分比在向量化后语义漂移

现象:问「保证金比例 20% 的产品有哪些」,返回了比例 30% 和 15% 的结果。原因:embedding 模型对数字不敏感,20% 和 30% 的向量距离很近。解决:在 chunk 里保留原始数字文本,检索时加一层数值过滤,或者用混合检索让 BM25 兜底。

5.4 扫描件 OCR 错误导致关键术语识别错

现象:OCR 把「融券」识别成「融卷」,检索不到。原因:OCR 模型对证券术语的识别率不够。解决:建立证券术语纠错词典,OCR 后做一轮后处理替换;或者对关键文档人工校对一遍再入库。

5.5 生成模型编造不存在的规则条款

现象:用户问「创业板适当性匹配规则」,模型生成了一段看起来合理但实际不存在的条款。原因:检索没召回到相关文档,生成模型自由发挥。解决:在 prompt 里加约束「只根据提供的上下文回答,上下文没有的内容回答不知道」,同时设置检索分数阈值,低于阈值直接返回「未找到相关规则」。

6. 用评估集把知识库效果量化:一个可复用的验证方法

知识库上线后怎么判断好不好用?拍脑袋说「感觉还行」是不行的。我一般会构造一个评估集,包含 50 到 100 个真实用户问题,每个问题标注标准答案和来源文档。然后跑三个指标:召回率(相关文档是否被检索到)、准确率(生成的答案是否正确)、溯源准确率(引用的来源是否对)。

def evaluate(qa_pairs: list[dict], kb) -> dict: """qa_pairs: [{"question": ..., "expected_doc": ..., "expected_answer": ...}]""" recall_hits, answer_hits, source_hits = 0, 0, 0 for item in qa_pairs: results = kb.search(item["question"], top_k=5) retrieved_docs = [r["source"] for r in results] # 召回率:标准文档是否在前5条里 if item["expected_doc"] in retrieved_docs: recall_hits += 1 # 生成答案并判断 answer = kb.generate(item["question"], results) if item["expected_answer"] in answer: answer_hits += 1 # 溯源:引用的第一条来源是否等于标准文档 if results and results[0]["source"] == item["expected_doc"]: source_hits += 1 n = len(qa_pairs) return { "recall@5": recall_hits / n, "answer_accuracy": answer_hits / n, "source_accuracy": source_hits / n, }

这个评估函数跑一遍大概几分钟,但能暴露大部分问题。如果召回率低于 80%,优先调切片策略和混合检索权重;如果召回率高但答案准确率低,问题在生成 prompt 或重排序;如果溯源准确率低,检查元数据是否完整。

我自己的习惯是每次调整切片参数或换 embedding 模型后都跑一遍评估集,对比指标变化。有一次把 chunk_size 从 600 调到 400,召回率涨了 5 个点但答案准确率掉了 3 个点,原因是切片太碎导致上下文不完整。这种取舍没有理论最优解,只能靠评估集在具体数据上试出来。希望帮到你。

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

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

大模型代码执行的安全沙箱:OpenSandbox 隔离 AI 生成代码的架构与实践

先说一个前两天发生在团队里的真实场景。同事为了让 AI 编程助手能自己写测试、自己跑测试,把模型生成的命令行直接丢进了本地终端。第一次跑得很顺利,模型自动补了一个依赖、执行完测试、返回了结果。第二次就没那么幸运了:模型读到项目里一…

作者头像 李华
网站建设 2026/10/5 17:10:37

开源集市摆摊记:Apache Pulsar社区线下布道实战与复盘

周五晚上九点多,我还在客厅里清点第二天要带去的物料。桌上摊着四件印了 Pulsar Logo 的速干 T 恤、两盒徽章、半袋贴纸,外加一台本地跑着 Pulsar 集群的笔记本。我是 Apache Pulsar 社区里负责周边工具链维护的志愿者,这周末的任务&#xff…

作者头像 李华
网站建设 2026/10/5 17:10:34

eBPF内核可观测性实战:从bpftrace到生产环境排障

最近几年只要聊到 Linux 内核可观测性,几乎绕不开 eBPF 这个词。无论你是搞网络、搞安全、搞性能调优,还是纯粹在做云原生基础设施,都会发现 eBPF 正在悄悄改变我们和内核打交道的方式。我自己第一次真正被 eBPF 震撼到,是在一次生…

作者头像 李华
网站建设 2026/10/5 17:10:31

反射实现Java对象字段统一非空校验,告别手写判空代码

“又要写判空了。”这句话在我维护老项目的日子里,出现频率比“好的”都高。每个新接口过来,第一件事就是把入参对象从头到尾捋一遍,然后为每个字段写一段if (xx null || xx.isEmpty()),返回错误之前还得把字段名拼进提示语里。一…

作者头像 李华
网站建设 2026/10/5 17:10:26

YOLO实战:男女性别检测数据集双格式解析与训练避坑指南

简介:面向目标检测与计算机视觉学习者,男女性别检测数据集提供9769张已标注图片,涵盖Female与Male两类目标,共9799个标注框,其中Female类别5491框、Male类别4308框,类别分布较均衡。数据集采用Pascal VOC和…

作者头像 李华
网站建设 2026/10/5 17:08:34

OpenClaw本地化实践:Gitee仓库构建与维护工作流全拆解

最近把 OpenClaw 这套开源智能体框架完整接进了 Gitee,从建仓库、配 SSH、定分支规范,到把构建流程跑顺、把依赖源配成国内可用的状态,前前后后折腾了不少时间。圈子里有人管它叫“龙虾”,有人管它叫“那只爪子”,其实…

作者头像 李华