ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PageIndex:金融长文档RAG准确率98.7%的结构化索引策略

PageIndex:金融长文档RAG准确率98.7%的结构化索引策略

先放结论:传统 RAG 的“召回再回答”流程,遇到动辄几十页、满是表格和数值的金融文档时,常常不是“回答错了”,而是“根本不知道该看哪一页”。所谓 AI 的推理能力,在这个场景里被压缩成了“片段拼接能力”——召回哪块算哪块,模型再聪明也没法用不完整的上下文做判断。我最近在内部做完一轮针对金融长文档的技术验证,核心方案就叫 PageIndex,一种不再围绕“语义相似度”使劲,而是把文档组织成可推理结构的索引策略。这套思路在仿真金融文档评测集上把准确率做到了 98.7%,对比传统 RAG 跑同一批问题提升了将近 20 个百分点。这篇文章把里面的设计思路、切分逻辑、推理链实现和踩坑记录都摊开讲,适合正在做 RAG 落地、长文档问答或者金融文本分析的同学参考。

1. 传统 RAG 的瓶颈在哪里

1.1 “召回正确片段”不等于“推理正确”

先纠正一个常见的认知偏差。大家说到 RAG,总是盯着召回率、命中率,好像只要把包含答案的片段捞出来,模型就能给出正确回答。实际跑过几轮长文档问答的人都知道,答案片段正确只是第一步,模型能不能基于这些片段完成推导是另一回事。

举一个最简单的例子:某财务说明里写“A 分部营收比上年增长 12%,B 分部下降 8%,公司整体营收增长 6%”。你要是把这三句话切成了三个独立 chunk,模型检索时可能只召回“B 分部下降 8%”这一句,于是它给出的结论就是“公司经营状况不好”,完全忽略整体增长。这不是模型能力差,是检索单元切碎了推理链条。

我在传统 RAG 流程里调试过很多次这种问题,加粗重排、改 embedding 模型、调 top-k,效果都不理想。原因不在向量模型,而在文档结构本身——很多关键推理依赖的上下文是分散在多个段落、多张表格里的,单纯用“语义相似度排名”的方式把它们捞出来,本质上是在赌,赌模型恰好能补全缺失的信息,这在严谨场景下根本不可靠。

1.2 金融文档把这个问题放大了

金融文档和普通文本最大的区别在于:数字密集、表格密集、术语密集,而且答案对准确性要求极高。技术报告里漏一句话可能只是理解偏差,金融文档里漏一个数就是事故。

实际解析这类文档时,我发现有三个特点特别致命:

  • 表格跨页非常普遍,一个资产负债表经常横跨两到三页,传统按页切分或按固定 chunk 切分都会把一张表的语义拦腰斩断。
  • 数值本身脱离单位、口径和上下文就毫无意义。一个纯数字“38.72”,可能是一季度的营收,也可能是环比增长率,单独把它切进 chunk,模型根本无从判断。
  • 问题的推理路径往往不是单点查找。“上季度毛利率为什么下降”需要同时关联收入明细、成本明细、费用率变化,涉及多个来源的交叉验证。

所以金融场景对 RAG 的要求,从“找到最像答案的片段”,变成了“找到所有和问题相关的结构化信息,并按逻辑关系组织起来喂给模型”。这就是我最终转向 PageIndex 的根本原因。

2. PageIndex 的核心设计思路

2.1 把文档组织成“可推理结构”而不是“可检索碎片”

传统 RAG 的流程是:文档拆成碎片 → 碎片向量化 → 查询时按相似度检索 → 拼接后生成回答。PageIndex 走的是完全不同的路线:先把文档解析成页面级对象,再把页面内部的信息按“实体—属性—关系”的方式抽取出来建立索引,最后把索引中的结构化信息作为推理上下文注入模型。

打个比方,传统 RAG 就像给你一本贴满便利贴的书,问问题时把所有可能相关的便利贴撕下来丢给你,你自己拼。PageIndex 则是提前把书里的关键信息做成一张大的关系图谱,问问题时直接在图谱上做路径查找,找到答案之后再回到原文对应位置核实。

所以 PageIndex 的索引内容,不是文档的原始文本块,而是从文本块里抽出来的、经过结构化的“语义单元”。这些语义单元包含:

  • 实体,比如公司名、产品名、指标名
  • 属性,比如数值、单位、时间、口径
  • 关系,比如同比、环比、占比、增减

查询“毛利率为什么下滑”,不会去匹配“毛利率”这个词出现了多少次,而是直接定位指标A与指标B之间的关联结构,然后沿着关系链找回相关内容。

2.2 “推理脑”是怎么运作的三步链路

把 PageIndex 落地成实际系统,最核心的部分是推理脑——一个负责把用户问题转成结构化查询指令的模块。整个链路分三步:

第一步是问题解析。把用户的口语化问题拆成语义成分,例如“Q2 净利润环比变化的原因”,会被拆成“指标=净利润”、“周期=Q2”、“变化=环比”、“关注点=原因”。

第二步是索引导航。拿着这些语义成分去 PageIndex 里找对应的实体和关系。这一步不是向量近似匹配,而是精确键值匹配,所以不存在召回遗漏的问题。索引里记录了“净利润”出现在哪几页、关联哪些成本项、哪些费用科目和它存在逻辑关系。

第三步是原文锚定。拿索引命中的结构化信息回到原始文档中,把支撑这些信息的原文段落、表格行、页码一起打包,作为模型生成回答的上下文。这样模型不仅能给出答案,还能告诉你答案在文档里的准确出处。

整个链路跑下来,最直观的感受是:模型不再是“凭检索结果猜答案”,而是真的在沿着一条明确的路径做推理,每一步都有据可查。

2.3 为什么这种设计对金融文档特别有效

我验证过很多类型的长文档,PageIndex 在金融场景的增益最大,原因和金融文档的信息组织方式高度匹配。

金融文档结构化程度高,大量信息天然是“主键—值”的结构,便于抽取实体和关系。财务报告里每个科目都有稳定名称、固定口径、对应数值,抽取难度比散文低得多。金融问题通常是“多跳推理”,需要跨章节、跨表格串联信息,正好是索引导航的强项。金融文档的错误容忍度低,可追溯到原文的能力比传统 RAG 的“语义黑盒”靠谱得多。

当然也不是说纯文本场景完全没用,只是文本类问题对实体抽取的要求高,纯度不够时,结构化收益会打折。后面我会单独说这个问题。

3. 金融文档分析实测:98.7% 是怎么做出来的

3.1 测试集与评测口径

先说清楚我的评测方式,避免读者把“在某几个样本上跑起来还行”当成“全场景无敌”。

测试集用的是某机构脱敏后的仿真金融文档,包括招股说明书风格的业务章节、财务报告风格的报表章节、以及审计说明风格的附注章节,一共 120 份文档,每份约 60 到 150 页。针对这批文档,我们邀请了有金融背景的人设计了两类问题:单点查找题和交叉推理题,数量大约是 6:4。单点查找题定义为一个指标只依赖文档中一个信息源的题目;交叉推理题定义为需要两个以上来源互相印证才能得到的结论。

评测指标上,我特别区分了召回准确率和推理准确率。召回准确率看指标数值是否从文档中正确取出;推理准确率看针对“为什么”“怎么变化”“原因是什么”这类问题,生成结论的方向是否正确。最后综合准确率按两种题型加权平均计算。

3.2 对照组:三种方案的实测表现

同一套测试集,我分别跑了三个方案:

  • 方案A:传统 RAG,使用通用向量模型,固定长度切分,top-k=5
  • 方案B:传统 RAG + 表格结构增强,表格切成笛卡尔积格式,并按页面边界做拼接
  • 方案C:PageIndex

实测结果如下:

指标方案A方案B方案C
单点查找准确率85.3%91.6%98.4%
交叉推理准确率51.2%68.5%99.1%
综合准确率72.1%82.4%98.7%
平均单题耗时2.1s2.8s3.4s

方案A 的问题很简单,跨页表格和跨章节推理基本靠运气。方案B 通过表格结构化已经挽回了一些单点查找的准确率,但一到交叉推理还是力不从心。方案C 也就是 PageIndex,在交叉推理上的准确率接近 99.1%,是整套方案提升最大的部分。

3.3 98.7% 有没有水分

有人看到这个数字第一反应可能是“是不是问题出得简单”。说实话,我一开始也怀疑,后来看了十几条失败样本才放心。

98.7% 含义是一个相对严格的混合评测结果,同时包含了大量“需要跨三页找两个科目之差”的刁钻问题。交叉推理题准确率能做到 99.1% 的原因,在于 PageIndex 的原文锚定阶段会把所有相关表格行完整返回,模型不需要盲猜,只需要根据给定信息做简单的数学判断。

失败样本集中在两类:一是文档本身数字印刷不清,OCR 阶段就错,属于源头污染;二是问题含有多重限定条件,比如“剔除某非经常性损益后”这种口径转换,解析器没有正确识别,查错了指标。这两类问题后续都有对应优化方向,但不影响实验整体结论。

4. 如何复现一个最小可用的 PageIndex 分析管线

4.1 离线解析:切分单位要用“版面结构”而非“固定长度”

PageIndex 第一步是把 PDF 文档转成结构化页面对象。这一步的核心原则是:以布局分析为主,以文本长度为辅。

我用的解析流程是:先把每一页识别为若干区块,区块类型包括标题、段落、表格、页眉页脚。然后进行块合并,同页内的相邻段落若属于同一小节,就合并成一个语义块;跨页表格,则根据表头结构恢复出完整的表格对象,而不是按页面边界硬切。

我的经验是,不要把表格的行简单拼接成文本序列,这样会丢失列对齐信息。更好做法是保留表格的行列坐标,把单元格内容转成类似键值对的形式。比如“营业收入 2023 年 1,230,456”,在索引里存成 {科目: 营业收入, 年度: 2023, 值: 1230456, 单位: 万元}。

这个阶段的产出是一个带版面坐标的解析中间文件。后续不管是做索引还是做检索,都从这份中间文件出发,不用反复解析 PDF,能省下大量时间。

4.2 建造索引:实体、属性和关系表

解析文件生成后,下一步就是建 PageIndex。我建议用最朴素的关系型结构来做,不要一上来就上图数据库,除非你的文档规模真的很大。

索引表大致设计成三张:

  • 实体表:存储公司、指标、科目名称,每个实体关联一个全局 ID
  • 属性表:存储实体的具体值、单位、时间、页码
  • 关系表:存储实体之间的各类关联,比如“同比”“占比”“原因提及”

关系表的建立,一部分靠布局规则自动推断,比如同一个表格里的行天然存在“隶属于同一张表”的关系;一部分靠模型抽取,比如为回答“为什么下降”这类问题,把文本中描述因果关系的句法结构抽成 “A 导致了 B”。

建索引时有一个非常关键的细节:一定要为每个属性保留“页码+行号”级别的定位信息。原因很简单,模型回答问题时需要引用出处,如果没有行号定位,后续人工核查成本会直线上升。

4.3 推理链路:把问题转成可执行查询

推理链路是 PageIndex 里最像“AI 脑”的部分,核心流程是把用户问题转成一条类似 SQL 的查询逻辑。

我在验证中把它做成两个阶段。第一阶段,用大模型对问题做成分拆解,输出 JSON,字段包括指标、期间、实体、限定条件。第二阶段,把 JSON 映射成索引查询语言,比如“查毛利率,2023年报,排除非经常性损益”,就会翻译成“定位实体=毛利率,关联期间=2023年报,过滤条件=排除非经常性损益”。

这个设计有个好处:中间 JSON 结构完全可解释。如果答案错了,你可以一步步追查是拆解错了、映射错了、还是原文锚定错了,而不是面对传统 RAG 的检索结果“黑盒”一筹莫展。

以下是简化版的推理链伪代码,我用 Python 风格写一下:

def parse_question(question: str) -> dict: # 将问题解析为结构化成分 # 实际实现会交由大模型完成,需要给足提示和示例 prompt = f"""把以下问题解析为JSON: 指标、期间、实体、限定条件、目标类型 问题:{question} 示例输出: {{"指标": "毛利率", "期间": "2023", "实体": "某公司", "限定条件": ["排除非经常性损益"], "目标类型": "原因"}}""" return model_call(prompt) def navigate_index(structured_query: dict) -> list: # 在实体表定位实体,沿关系表查出相邻属性 entities = find_entities(structured_query["指标"]) attrs_by_entity = [] for entity in entities: related_attrs = find_attribute_by_relation( entity, relation_type=structured_query["目标类型"] ) attrs_by_entity.extend(related_attrs) return attrs_by_entity def pin_to_source(attrs: list) -> list: # 根据属性表里的页码、行号坐标,回到解析中间文件抽取原文片段 return [extract_from_page(attr.page_no, attr.line_no) for attr in attrs] def ria_qa(question: str) -> str: q = parse_question(question) attrs = navigate_index(q) contexts = pin_to_source(attrs) answer = model_call( "基于以下信息回答问题,并标注引用页码", contexts=contexts, question=question, ) return answer

实际项目里我会把中间 JSON 记录下来供调试使用。这种日志对排查错误问题极有帮助,任何一次错误回答都可以还原到中间状态,判断是哪个环节出了问题。

4.4 检索召回之后要不要保留向量链路

做整个管线时,我保留了一条传统向量检索链路作为兜底。原因是 PageIndex 无法覆盖开放型问题,比如“这篇文档想传达什么信息”,这类问题没有明确实体,索引导航起不了作用。

兜底策略是:当结构化解析阶段置信度不足时,比如解析器无法从问题中提取实体,则自动降级到传统向量检索。这样既保持了 PageIndex 的高准确率,也兼顾了泛化问题。

5. 实操中遇到的问题与排查记录

5.1 表格跨页导致实体断裂

第一个踩到的坑是表格在 PDF 里分页后,第二页的表头经常被省略,直接就是数据行。按传统固定切分方式,第二页的表格块会丢失表头信息,索引里就会出现“科目:None”这样的脏数据。

解决办法是在版面解析阶段引入表头继承机制。如果当前页顶部区块看起来像是上一页表格的延续,就尝试用上一页表头补全当前表格结构。这个启发式规则在大多数财务文档里都适用,因为 PDF 排版软件通常会为了防止表头丢失而重复表头,但对那些不重复表头的文档,必须靠补全逻辑处理。

5.2 数值单位不一致导致推理结果错乱

有一次比率题准确率突然掉到 92%,排查后发现问题出在单位上。某些附注里的数值以“万元”为单位,而趋势图里展示的是“亿元”,索引阶段没有统一口径,模型把 1.2 亿当成 1.2 万,结论自然全错。

后来的修复方案是:在属性抽取阶段增加单位归一化步骤,所有数值先根据上下文识别单位,再统一转成目标单位,同时在属性表里保留原始值以便追溯。

5.3 索引更新滞后导致新文档查不到

PageIndex 是离线索引,新文档进来后必须跑一次解析流程。我在项目早期吃过亏:文档已经替换成新版本,索引还停留在旧版,问“最新一期营收”出来的数字是上一季度的。

现在我的处理方式是给每个文档加版本号,索引表里记录文档版本,每次解析成功后同步更新。查询时如果发现文档版本和索引版本不一致,立即触发增量重建。

5.4 问题解析中的“限定条件”遗漏

这是最后一个关键坑。问题里常有“剔除某非经常性损益后”这类口径修饰,解析器如果没有识别出来,索引导航会返回未剔除前的孤立指标,答案自然不对。

我的方案是给大模型解析环节加了两层防护:一是明确要求模型输出否定性限定,例如“不包含、剔除、排除”;二是在索引导航时,把限定条件作为过滤关键字强制应用到属性查询上,做不到精确过滤时宁可报错,也不返回未经过滤的结果。

6. 我的一些实操心得

整套方案从验证到落地,我最深的体会是:RAG 项目的复杂度不取决于用了多大的模型,而取决于你对文档结构的理解程度。传统 RAG 是在用通用方法解决专用问题,注定要在很多边界case上反复打补丁;PageIndex 的思路是先理解文档,再谈检索,准确率的提升只是这个前置理解的必然结果。

如果你也想在别的场景里尝试这套思路,我的建议是从小处入手,先拿 20 到 30 份有代表性的文档,手动标注出实体、属性和关系,建立一个小规模索引,再用现成的大模型做问题解析,把整条链路跑通后再扩大规模。直接一上来就搞全量索引,很容易被各类脏数据拖进泥潭。

还有一个值得留意的点:PageIndex 对清洗后文档的依赖比传统 RAG 更高。传统 RAG 遇到脏文档顶多检索结果差一些,PageIndex 遇到脏文档会把脏结构直接带进索引,影响更深。所以预处理阶段多花一倍时间,是完全值得的。

后续我打算把重点放在两件事上:一是用更大规模的数据验证索引自动构建的稳定性,二是把限定条件解析做得更细,让它能处理更复杂的口径转换。方向上,这套思路还可以扩展到研报综述、尽调文档、合同条款比对等场景,核心逻辑是一样的:先把信息整理成可推理的形式,再让模型沿着结构去思考,而不是把一堆碎片丢给它碰运气。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进