Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

一个财报 RAG 的七步链路,和每一步都会踩的坑

把 119 页的腾讯 2025 年中报灌进系统,问一句“毛利率多少”,系统秒回:“毛利……第 45 页”。

它检索到的是目录页——相似度分数不低,信息量为零。

这次答错,错不在模型。从它往回拆,能拆出一条七步链路;这套财报 RAG 后来的每次翻车,几乎都能挂到链路的某一环上。这篇是全系列的总纲:把链路画出来,把每一环最常见的烂法点名。怎么修是后面各篇的事,这里只给地图。

一、七步链路全景:一份 PDF 到一个答案

离线(数据进库):       PDF → 1.抽取 → 2.清洗 → 3.分片 → 4.标注 → 5.索引
在线(从 query 到答案):6.检索 → 7.生成

每一步的输入输出都很直白:上一步的产物是下一步的原料,任何一环把东西弄坏,下游拿到的就是坏东西。我把七步分成离线五步和在线两步,不是步数凑巧——离线烂的是数据,在线烂的是匹配,修法完全不同。和通用的“七环”说法(解析、分块、向量化、索引、检索、生成、评估)对照,这条链路基本一一对应,差别是我在财报视角下把清洗和标注单独拎了出来。评估没有单列:它不是链路上的一站,是贯穿全线的事,留给第 14 篇。

二、离线五步:数据进库之前,烂法都在数据里

抽取:把为打印设计的东西,转成程序能处理的东西。 PDF 告诉打印机“这个字在第 45 页第 3 行的哪个位置”,从没说过“这是标题”“这是表格的第二行”——所有结构信息都要靠代码还原。这一步最被低估的烂法是 reading order:多栏页面左右栏交叉拼接,页眉、正文、页脚被换行符拼成拼盘。抽出来的 raw.txt 看着成段通顺,语义其实已经坏了;肉眼很难发现,直到检索阶段才暴露。pdftotext -layout 在单栏页面上是准的,在这份中报的多栏页面上有 20–30% 的错位——这是我这一份文档上的经验值,不是普遍数字。

清洗干两件事:去掉噪声(页眉页脚、目录、纯页码),把被分页符切断的句子拼回去。这一步的烂法在顺序。先切 chunk 后清洗,噪声就和正文锁死在同一个 chunk 里,清理成本翻倍。切断的句子不拼回去更麻烦:断点恰好落在语义中间,后面任何分片策略都救不了它。

分片的目标不是“按 token 数均分”,是切出检索粒度合适的信息单元。最常见的烂法恰恰就是按 token 均分:一张表格拦腰切断,数字和表头分进两个 chunk——模型只看到数字,不知道这些数字代表什么。还有一个反向的迷信:“chunk 越大,上下文越足”。这话在检索阶段是反的,chunk 越大,embedding 被无关内容稀释得越厉害,检索先输。

切完的 chunk 还是“黑户”,标注给它发身份证:年份、期间类型、章节类型,这些领域约束标签。这一步的烂法一句话就能说完——标了不用。metadata 只做 citation 展示、不进检索层,等于没标。这不是假设出来的问题:上一篇《当向量相似度把“2020 Q4”当成“2025 Q4”》拆的根因就是它,report_year 入库时标了,检索时却没拿它做过滤。

离线阶段的收尾是建索引:把 chunk 编码成向量,存进向量库。烂法有两个。一是迷信“选最强的 embedding 模型”——领域适配比榜单分数重要,通用语料上的 SOTA 到了金融文本里,可能不如一个领域微调过的小模型。二更隐蔽:一些 embedding 模型训练时区分 query 和 passage,使用时得按各自的约定加前缀——E5 系列加 query: / passage:,BGE 系列是另一套,query 侧加固定指令 Represent this sentence for searching relevant passages: ,passage 侧不加。漏加了不报错,只是召回分数偏低——质量肉眼可见地下降,日志里却什么错误都找不到。

三、在线两步:从 query 到答案,烂法都在匹配上

数据进了库,问题换一种烂法。

检索做的事很直白:query 走向量取 top-k,可选 reranker 精排。最要命的错觉是把相似度当相关度——没有 metadata 过滤的时候,目录页的相似度可以高过正确数据页,开场那句“毛利率”就是这么错的。demo 阶段这个错觉不明显,库里只有一份年报时怎么问都对;孪生文档一多,系统就开始答错得很流畅。再点一个反直觉的:top-k 不是越大越好。跨年财报是典型的“语义近似但约束不同”,放大 top-k 只会引入更多假阳性,连 reranker 都会被低质量候选拖累。时间错配这个坑本身,上一篇已经整篇讲过,这里不重复。

生成把 top-N 个 chunk 和 query 拼进 prompt,让模型基于证据回答。这一步的烂法是“chunk 塞得越多越好”。prompt 里的 chunk 数量有一个收益递减点:Lost-in-the-Middle 效应下,LLM 对中间位置的注意力最弱,5 条精准上下文通常优于 20 条泛泛上下文——塞得越多,关键证据越容易被埋在中间。财报场景另有一条:数字问答别让模型一步“找+算+总结”,出错率会显著上升,该拆成先找证据、再生成回答两步。

四、最高频的三个翻车点:约束都在数据里,系统没看见

七步走完回头看,这套系统的失败模式其实集中在三个地方。

目录污染,烂在清洗与检索的交界。 就是开场那个错法。目录页把全书的关键词完整抄了一遍——“收入”“毛利”“经营现金流”,这些 token 在 embedding 空间里和正文几乎一致:相似度满分,信息量零分。它一半烂在清洗,噪声没剔掉;一半烂在检索,没有任何机制区分“提到这个词”和“能回答这个问题”。怎么剔、怎么防,第 2 篇和第 8 篇分别讲。

时间错配,烂在标注与检索的交界。 2020 Q4 和 2025 Q4 的财报行文是语义同构的——管理层讨论的模板每年只换几个数字,在 embedding 看来两者几乎是邻居。report_year 标了,不进检索层,相似度就把 2020 当成了 2025。上一篇《当向量相似度把“2020 Q4”当成“2025 Q4”》已整篇拆过,这里只点名。

口径混乱,烂在数据本身。 同名“利润”,同一家公司、同一期有两个数字:GAAP 口径 1,150 亿,Non-GAAP 口径 1,320 亿。两个都对,都是财报里白纸黑字披露的;但系统从不同 chunk 各召回一个、拼在一起比较,就错了。这不是检索能修的问题,是数据自带的业务规则,第 7 篇专门拆口径。

三个翻车点,没有一个是模型的锅。约束都在数据里,只是系统没看见——这正是后面十几篇要逐个拆的东西。

五、全系列地图:每一步对应哪一篇,按痛点跳读

链路和篇目的对应关系:

  • 抽取、清洗烂了:第 2 篇(reading order)、第 3 篇(表格)、番外 A1(五种解析工具对比)
  • 分片烂了:第 4 篇;标注烂了:第 5 篇
  • 索引烂了:第 12 篇(embedding 选型)、第 13 篇(默认参数)
  • 检索烂了:第 6 篇(时间错配)、第 7 篇(口径)、第 8 篇(相似度)、第 9 篇(Hybrid)、第 10 篇(Rerank)
  • 生成和全链路:第 14 篇(评估)、第 15 篇(排查清单);系统层面:第 11 篇(多市场)、第 16 篇(运维)

按痛点跳读,我的建议是三句话。库还没搭、数据还脏,从第 2 篇顺序读;检索不准,直接跳第 6 到 10 篇;已经上线要扛,看第 11 到 16 篇。第 17 到 21 篇是边界和展望,读完前面再来。

回到开场那句“毛利率多少”。它不是模型不行,是链路第一环就把数据弄坏了;而脏数据只会顺流而下,所以修链路的顺序是从上游往下游修。也有一条边界:这张地图来自我做的这一个财报系统,链路是通用的,烂法的优先级因文档类型而异——你的文档如果没有多栏和表格,前两环的优先级就没有这么高。

链路从最上游开始烂,下一篇先讲最上游:同一份 119 页中报扔进 pdfplumber 之后,到底发生了什么——第 2 篇《PDF 解析:抽不出文本早不是问题了,reading order 才是》。