☰
金融RAG实战:从财报解析到可信问答的完整链路搭建
2026/10/22 21:18:02 网站建设 项目流程

把一份五十页的年度财报丢给大模型,让它准确说出“去年第四季度毛利率是多少,比第三季度变化了多少”,你会得到什么?如果直接问通用大模型,它大概率会给你一个看似合理、其实是编出来的数字。这正是我想通过 Financial RAG Demo 解决的核心问题:用一条完整的RAG服务链路,把金融文档变成可问答、可溯源、可验证的“本地知识库”。这篇内容会完整拆解这个Demo从文档解析、切片、向量检索到生成回答的整个设计与实操过程,适合正在做知识库问答、金融NLP场景或者刚接触RAG应用开发的同学参考。

金融场景对RAG的要求和通用文档问答不太一样,数据密度高、术语密集、表格多、对数字准确性要求极高。整个Demo做完,我最大的感受是:RAG服务的难点不在“接入大模型”,而在前面的数据准备和检索质量。链路中任何一个环节偷懒,最后都会在回答质量上暴露出来。下面直接进入正题,按我实际开发的顺序,把所有关键技术决策和踩坑经验逐一讲清楚。

1. 为什么做Financial RAG Demo:金融场景对RAG服务的特殊要求

1.1 金融文档问答,难在哪里

金融领域的文档管理是个典型的海量数据场景。上市公司的年报、季报、临时公告、研报、监管政策文件,动辄几十上百页,PDF格式居多,内部还塞满了财务报表、管理层讨论、脚注注释。过去这些文档的利用方式基本就是“存起来、搜出来、人肉看”,想要跨文档得到一个综合性的答案,效率非常低。

我把这个项目定位在“金融文档问答”而不是泛泛的“聊天机器人”,就是因为金融问答有几个很难绕开的痛点。

第一个痛点是术语密集。像“毛利率”“净资产收益率”“经营活动产生的现金流量净额”这种表述,一个概念有多种叫法,缩写和全称混用。通用问答系统很难理解这些术语之间的关联,检索时容易出现“关键词命中了,但拿到的段落根本不是用户想问的那个指标”。

第二个痛点是数字敏感。金融问答最怕胡说,一个人问“去年研发投入多少钱”,如果系统回答里数字跟真实财报对不上,这个系统基本就失去信任了。数字对错的根源往往不在大模型,而在检索阶段有没有把正确的表格和上下文段落召回来。RAG服务如果不针对表格做专门处理,检索到的内容经常是残缺的。

第三个痛点是来源必须可溯。“根据哪份报告、哪一页得出这个结论”在金融场景几乎是刚需。基金经理、风控人员、研究员拿到一个AI回答,第一反应就是去看原始依据。没有引用来源的答案在金融场景等于无效答案。

第四个痛点是时效性。财报是定期发布的,公告是实时增加的,模型固有的知识永远滞后。RAG天然适合这种“知识库持续变化、需要即时更新”的场景,换数据源就能换知识,不需要重新训练。

1.2 为什么是RAG而不是微调大模型

在做这个Demo之前,一个绕不开的问题是:为什么不用微调的方式,把财报内容直接喂给大模型训练?我当时把两种方案放在一起对比过,结论非常明确。

对比维度RAG方案全量微调方案
知识更新成本换索引、加文档即可,分钟级生效需要重新训练,小时到天级,成本高
输出可信度回答基于检索到的原文片段,可溯源知识固化在参数里,无法定位证据
幻觉控制能力可在提示词层强约束“只能依据上下文”模型容易把训练知识与上下文混杂
实施成本无需训练GPU,开发成本集中在检索链路需要训练环境和高质量标注数据
适合场景动态知识库、专有文档、持续更新的资料固定领域语料的指令遵循能力提升

微调解决的是“让模型更懂这个领域的说话方式”,而不是“让模型记住这份文档里写了什么”。你把一份三千行的财报PDF塞进微调数据集,训练完之后你问它其中某个具体数字,它依然可能编造。因为大模型的学习本质是压缩和泛化,不是精确存储。RAG的思路完全不同:我不要求模型记住文档内容,我只要求它学会“基于给定的材料回答问题”。知识存储在检索系统里,模型负责组织和表达。这一层解耦是金融场景格外看重的,因为文档是流动的、权限是分级的、答案是必须能翻到原始页面的。

1.3 Demo项目的目标与边界

一个Demo不能什么都做,否则会变成“什么都很糙的半成品”。我在动手之前先划定了边界,明确这个Financial RAG Demo要证明三件事:一是能对结构化程度低的金融PDF完成高质量解析与切片;二是能准确检索到包含关键数字和对比信息的片段;三是能基于检索内容生成带来源引用的可读回答。

演示数据源我选的是公开的模拟财报PDF、模拟公司公告和模拟行业研报,一共准备了十几份。为什么不直接用真实财务数据?一是合规风险,二是不想在Demo阶段花大量时间处理数据授权问题。模拟数据已经足够把链路跑通、把问题暴露出来。这个项目的定位是“最小可用的金融问答参考实现”,不是生产级系统。在这个定位下,我要的是快速验证方案、沉淀可复用的工程代码,而不是一开始就去追求过高的准确率。

2. RAG链路逐个拆:从金融PDF到可信答案

2.1 文档解析:金融PDF结构化处理,不能只做文本抽取

RAG服务的上游是文档解析。很多人一开始觉得解析很简单,不就是把PDF里的文字提取出来嘛。金融PDF分分钟教你做人。

我遇到的第一个坑是表格。财报的利润表、资产负债表、现金流量表全是表格。如果用最简单的文本抽取工具直接提取,表格会变成一行行没有任何结构逻辑的文字堆叠,数字和项目名称的对应关系全部丢失。比如“营业收入 1,023,456,789”和“营业成本 987,654,321”在一张表里,提取成纯文本后,你根本不知道哪个数字对应哪个指标。要解决这个问题,必须使用能识别表格结构边界、能按单元格关系输出的解析流程。

我实际用的方案是两层处理:先用布局分析方式识别页面中的文本块和表格区域,再对表格区域单独走表格结构识别逻辑,尝试还原出行列关系。解析完成后,每条内容都带上坐标信息、所在页码和所属章节标题,这一步为后面的切片和引用打基础。页眉页脚也是必须去掉的,不然每页重复的“某某公司2023年年度报告”字样会被切进多个切片,检索时严重干扰相关性排序。

这里有一个容易被忽视的点:解析结果不要直接扔给切片器,而是要转成统一的中间结构。我给每个文档块设计了一个简单的元数据模型,包括doc_id、page、section、content、type(文本/表格/图表标题字段)。后面所有环节都依赖这套元数据,作用类似给每块内容贴上了身份证,检索到之后能回答“这段出自哪里”。

2.2 切片策略:保留语义、保住上下文、还要留得住表格

切片是RAG里看似简单、实际影响非常大的环节。切片太粗,检索粒度大,相关性漂移严重,一个大片段里只含一小段有效信息,浪费向量空间;切片太细,语义不完整,一个财务指标的定义被切断,或者表格的表头和数据被拆到两个切片里,检索到了也没法用。

我测试过三种切片方式,结果差异非常明显:

切片方式优点缺点适合场景
固定长度切片(如每512字一刀)实现简单,索引稳定切断段落语义,表格极易拆散纯文本资料
按章节/标题切片语义完整,边界自然章节长度差异大,长章节片段过大结构化良好的文档
自定义语义切片(标题+段落+表格保护)兼顾语义与检索精度需要额外开发规则金融财报、公告

我最终用的是第三种,核心逻辑很简单:先把文档按标题层级切成大的区块,再对超过长度阈值的区块按段落边界二次切分,同时加了一个保护规则——表格区域凡是检测到表头和首行数据的,就整个作为一个独立切片,不允许切散。切片之间加少量重叠窗口,避免把一个概念的结尾和下一个概念的开头隔开。

重叠窗口大小我设了64个字符左右。这个数值不是拍脑袋定的,是通过检索召回率对比得出来的。太小起不到衔接作用,太大会让相邻切片高度相似,检索结果里出现四五条高度重复的内容,白白占用上下文窗口。

2.3 Embedding与检索:混合检索加排序,为什么是标配

这个Demo的检索模块没有只依赖向量相似度,而是用了“稀疏检索+稠密检索”的混合方案。原因很现实:向量检索擅长处理语义相近但字面不同的表达,比如“公司赚了多少钱”和“净利润”的语义关联;但在金融场景,关键词精确匹配依然非常重要,尤其是年份、股票代码、专有指标名这种字符特征强烈的查询,稀疏检索的精确命中率往往高于向量召回。

我使用的Embedding特征不是自己训练的,而是选择了在通用语料上预训练好的模型来生成向量。选择模型时主要看两方面:一是对中文长文档的支持,二是金融术语的理解能力。实测下来,通用Embedding模型对“ROE”“经营性现金流”这类术语的字面匹配还行,但有时会抓不住“股东回报能力”这种同义表达。当时时间有限没有自训练模型,但我在检索层做了补偿:加入BM25稀疏召回,再用重排序模型把两路结果合并打分,这样即使向量语义召回没抓住精确关键词,BM25也能补位,反之亦然。

混合检索的合并我用的是RRF算法,对两路候选按排名融合,而不是简单加权平均分数。因为不同检索算法产出的分数区间差异很大,直接加权容易被某一方带偏。排序阶段再通过一个轻量级的重排序模型,对召回的Top 20段重新打分,取Top 5作为上下文。这一步的效果立竿见影,Top 5命中率明显提升,冗余内容也被滤掉不少。

2.4 生成增强:提示词约束、来源引用与拒答机制

检索做得再好,生成环节设计不到位,回答质量同样拉跨。我在提示词层面做了三个重要设计。

第一个是上下文约束。原原本本在系统提示词里写清楚:“只根据下面提供的资料回答问题。如果资料未提及,直接回答‘未在现有资料中找到相关信息’,严禁编造。”这是对抗幻觉的第一道防线,虽然不能完全消除幻觉,但能大幅降低模型自由发挥的概率。

第二个是来源标注。我要求模型在回答的每个关键结论后用中括号标注资料编号,例如“[资料1][资料3]”。这些编号对应检索结果的元数据,最终在展示层映射回“报告名称+页码”。用户看到数字想核实,只要照着页码翻原文就行。这是金融场景必须有的能力。

第三个是拒答机制。对于检索相关度不够高的提问,我设置了一个门槛逻辑:如果重排序后的最高分数低于设定阈值,或者上下文与问题的关键词重叠度太低,服务端就直接返回“当前资料库中没有覆盖到您的问题”。宁可说不知道,也不硬答。这个思路在可靠性要求高的场景非常值得推广。

3. 实操记录:我如何搭起这个RAG服务

3.1 技术选型与环境准备

技术选型只有一个原则:能用最简方案搞定的就不引入重型组件。整套RAG服务验证下来,我的核心组成是:一个PDF解析模块(负责布局分析和文本抽取)、一个向量化模块(负责将切片转为向量)、一个向量数据库(负责存储和召回)、一个搜索引擎(做BM25稀疏检索)、一个重排序模型(负责精排),以及一个大模型服务(负责生成最终答案)。

为什么向量数据库我选了本地部署而不是云服务?因为这个Demo涉及金融资料处理,数据不出本地是更稳妥的姿态。实际选型时看重的点也不复杂:是否支持过滤条件、是否有稳定易用的客户端、查询延迟能不能控制在几百毫秒以内。开源方案完全够用。

大模型这一层,我同时测试了本地部署的开源轻量模型和商业API两种方案。Demo阶段为了效果展示更稳定,最终选择了API方式,因为显卡资源有限。但代码层面我把模型服务封装成了可替换的接口,后期如果要在纯内网环境部署,只需要把接口实现换成本地模型,其他逻辑完全不用动。这一点对金融项目尤其重要,因为生产环境很可能不允许调用外部服务。

3.2 离线索引构建的完整流程

索引构建是RAG服务的离线环节,我刚才梳理了一下,完整流程大概是这样的:

  1. 读取PDF文件,逐页做布局分析,识别正文、表格区域和页眉页脚;
  2. 对表格区域走表格结构还原逻辑,整理成带有行列信息的Markdown格式文本;
  3. 将每页内容按“章节切片+区块切分+表格保护”的规则切分,同时记录元数据;
  4. 对每个切片做清洗,去掉空白字符,修正编码问题;
  5. 调用Embedding接口生成向量;
  6. 将切片文本、元数据、向量全部写入向量数据库,同时将文本写入稀疏检索引擎;
  7. 构建完成后做一个抽样自检,把几个典型问题对应的切片捞出来确认内容是否完整。

清洗这一步在代码里必须做好,否则后面检索会出现各种奇怪问题,金融文本里常见的全角半角混用、制表符残留、编号列表的缩进符号,都需要在这一步抹平。

索引构建完成后我建议加个自动统计脚本,输出切片总数、平均长度、表格切片数量等指标。这一步能帮你迅速发现解析环节有没有出问题。我第一次跑完发现表格切片数量异常少,回去排查才发现表格识别逻辑对某些样式的表格失效了,这个脚本直接帮我定位了问题,非常实用。

3.3 在线问答服务的关键代码实现

在线问答服务我拆成了三个模块:Query理解、检索召回、生成回答。以下是我核心链路简化后的实现逻辑:

# 检索模块:混合召回 + RRF融合 + 重排序 def retrieve_documents(query: str, top_k: int = 20, final_k: int = 5): # 1. 稀疏召回(BM25) sparse_results = sparse_searcher.search(query, top_k) # 2. 稠密召回(向量) query_vector = embedding_service.embed(query) dense_results = vector_db.search(query_vector, top_k) # 3. RRF融合两路结果 fused = rrf_fusion(sparse_results, dense_results) # 4. 用重排序模型做精排,取Top K reranked = rerank_service.rerank(query, fused) return reranked[:final_k]
# RRF融合:按排名倒数累加分数 def rrf_fusion(sparse_results, dense_results, k: int = 60): score_dict = {} for rank, doc in enumerate(sparse_results): doc_id = doc["id"] score_dict[doc_id] = score_dict.get(doc_id, 0) + 1 / (k + rank) for rank, doc in enumerate(dense_results): doc_id = doc["id"] score_dict[doc_id] = score_dict.get(doc_id, 0) + 1 / (k + rank) return sorted(score_dict.items(), key=lambda x: x[1], reverse=True)

生成模块的提示词我构建成了一段结构化的文本,把检索到的片段按编号排列,并要求大模型严格按照编号引用:

请回答用户问题。只允许参考下面列出的资料,严禁使用资料以外的知识作答。 资料: [1] 来源:模拟公司2023年度报告 第12页 内容:报告期内公司实现营业收入142.8亿元,同比增长18.5%... [2] 来源:模拟公司2023年三季度报告 第5页 ... 要求: - 回答每一点关键信息后标注来源编号,例如[1]; - 若资料中没有相关信息,直接回复“现有资料中未找到相关信息”; - 语言简洁,使用金融行业常用表述。

有个接口细节必须注意:问答接口的响应时间几乎全花在大模型生成上,检索阶段一般耗时不大。用户侧体验上建议加流式输出,一边生成一边推送,而不是等全部生成完再一次性返回。金融场景里用户看到的是逐段出现的答案加引用标注,体验会好很多。

3.4 演示界面与效果评估

Demo要给别人看,一个丑陋的命令行界面肯定不行。我花了一下午用轻量级Web框架搭了个极简的聊天界面:左侧是文档目录和索引状态,右侧是对话窗口,回答内容下面的“查看来源”按钮可以直接定位到原始PDF的页码。界面本身只做展示,重点是把检索链路的所有中间信息暴露出来。

展示中间信息是我特别坚持的一点。每次问答,界面右侧会显示命中的Top 5片段、各片段的重排序分数、用了哪些资料。别小看这个设计,它能极大增强使用者的信任感,也让这个Demo更适合用于内部技术评审。别人看到的不只是一个“漂亮的回答”,而是一条可解释的处理路径。

效果评估我建了一个小规模的人工评测集,三十个问题,覆盖三类:事实查询型(“去年营收是多少”)、对比分析型(“四季度毛利率环比变化”)、跨文档综合型(“两份报告中关于研发投入的描述有什么差异”)。我按四个维度打分:检索命中率、回答准确率、来源引用准确率、拒答合理率。初版全量跑完,回答准确率大概在七成左右,后来经过切片策略和重排序调优后有明显提升。这里说一句,构建评测集不需要很多条,但要保证问题的典型性和多样性,比盲目堆数量有用得多。

4. 踩坑实录:金融RAG的常见问题与排查

4.1 表格数据解析后“散架”了

我踩过无数次的坑:表格数据解析成Markdown后行错位、列错位。原因很隐蔽,PDF里某些单元格的文字过长会自动换行,如果解析器没有正确处理“单元格内换行”,就会把下一列的内容顶到下一行,整个表的结构直接崩掉。

排查方式是把解析后的表格与原文并排对比,逐列检查。修复方式有两种:一是选用对单元格边界更敏感的解析配置;二是在解析后增加一条校验逻辑,对项目名与数字的对应关系做简单规则检查,发现明显异常就触发重新解析。这个教训让我明白一件事:金融PDF解析永远需要人工抽检,不能只看一两个页面的效果就默认全部没问题。

4.2 财报数字检索错位,指标和数值对不上

有一次测试问“经营活动现金流净额”,系统返回的是“投资活动现金流”那个表的片段。乍一看两个表结构非常像,但指标名差了几个字,数字完全不是用户想要的。原因在于Embedding模型对“经营活动”和“投资活动”这两个高度相似短语的区分能力不足,向量距离太近,导致错误召回。

解决办法是引入字段级元数据的硬约束:检索时如果问题中识别到“经营”“投资”“筹资”这类关键词,就在向量检索的过滤条件中限定对应的表头区域,不让其他表的数据参与候选。混合检索加规则化是金融RAG必须有的组合,纯粹靠语义理解来处理财务术语的精确对应,目前还不可靠。

4.3 金融缩写与专业术语让向量检索失效

“ROE”这个词,通用Embedding模型大概率能关联到“净资产收益率”,但“EBITDA”这种更专业的缩写,向量召回效果就明显差了。测试问“公司EBITDA情况”,召回到的片段经常是正文里顺带提到这个缩写的那句话,而不是真正披露指标的表格页。

我之前一直强调混合检索和重排序,就是在这里发挥作用的。BM25对“EBITDA”这种精确词命中很好,重排序模型也能根据上下文重新识别真正包含指标的表格段落。此外我还手动维护了一小份金融术语同义词词典,查询进来先做一遍术语扩展,把“ROE”映射成“净资产收益率”,这样一来向量检索的命中率有明显提高。

4.4 答案仍然幻觉,提示词约束并不万能

就算提示词写了“严禁编造”,模型在上下文中找不到明确答案时,依然可能把相邻年份的数据张冠李戴,或者把上下文没有的因果关系编出来。我见过最典型的情况是:上下文中只有2022年和2023年的营收数据,问“2021年营收”的时候,模型硬是从两句数据的表述里推测了一个数字,还说得很笃定。

应对幻觉靠两道防线。第一道是检索质量,只要正确的切片被召回到Top位置,幻觉概率就大降;第二道是“拒答门槛”,我在生成前置逻辑里加了相关性闸门,如果检索结果综合分数过低,就不再调用生成接口,而是直接返回提示让用户换种问法。前面提到过,宁可答不上来,也不要给一个貌似严谨的错误答案,这在金融领域是一条底线。

4.5 检索性能与索引更新问题

文本量放大之后,检索延迟会明显上升。我曾在索引里塞了几千份文档,发现一次查询到了秒级,赶紧查了原因——向量索引的索引参数没有调优,导致暴力检索。解决方式是把索引类型切换成更高效的近邻检索,并调整构建参数,延迟立刻降到了百毫秒以内。建议在压测阶段就关注这个参数,不要等线上慢了再来排查。

索引更新是另一个容易被忽略的环节。业务上新增了一份财报,需要让新知识立刻被检索到。我采用的是全量重建与增量写入结合的方式:常规更新走增量接口,文档结构大改或数据清洗规则变化时触发重建。增量更新时务必维护好文档ID与切片ID的映射表,否则旧切片无法定位,更新后容易出现新老数据混答的问题。

最后再分享一个小技巧

整套Financial RAG Demo跑下来,如果要我说最值得关注的优化点,不是换更大的模型,而是把数据准备和检索调优做扎实。数据准备阶段的每个决策,都会在最终的回答质量上加分或减分。我建议每个做类似项目的人,在第一次跑通端到端之后,不要急着加强大模型能力,而是建立一个“检索失败案例台账”,把问答中检索不到正确片段的案例一条条记录下来,分析是切片问题、解析问题还是Embedding问题。我自己就是这样,连续记录和修复了三周,数据准确率才真正稳定下来。这个台账方法免费、实用、任何场景都能用,强烈推荐。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询