RAG 召回率很好看,为什么仍不能证明 Agent 找对了金融证据?
给金融 Agent 评测检索,我原本想回答一个很直接的问题:换成 BM25,能不能比现有的 PostgreSQL 全文检索加向量融合找到更多有用资料?
离线实验很快出现了漂亮结果。但真正拿一份 ETF 公告去核对时,我发现更重要的问题不是谁排第一,而是:排在前面的内容,究竟能支持用户问的哪一句话?它有没有真正进入模型的研究过程?
检索排序、模型消费、答案引用和金融判断是四次不同的交接。把其中一次的高分当成最终成功,RAG 评测就会提前结束。
一、一次接近满分的实验,为什么没有改变我的技术选型?
第一轮用 12 个固定问题和 48 个合成片段做对照。为了避免一开始就比错,两种方案面对同一份候选集合;每个问题标出两个构造的正例、两个构造的负例,其余 44 个候选明确记为未判定。
测试用的 BM25 在这些已标注正例上表现很好,已标注正例的 Recall@10 达到 1。现有 PostgreSQL 词法通道在这批合成文本上甚至没有返回匹配项,融合结果也不如 BM25 的构造标签指标。
这看上去像是一个该换算法的信号。但它至少有三个问题。
首先,这些正例本来就是围绕问题构造的。命中它们,证明算法适应了这套文本和分词方式,不等于能在复杂的基金报告中找到真正需要的证据。
其次,44 个未判定候选不能默认为“不相关”。我们能报告的,是已标注正例的命中情况,不是整个资料库的完整召回率。信息检索评测里的相关性判断本来就有覆盖边界;NIST 对不完整相关性判断的研究专门讨论过这一问题。
最后,即使候选和标签都可比,检索指标仍没有回答模型会不会读取、理解、正确使用那些资料。
所以,当时的实验决策不是“BM25 胜出就替换现有检索”,而是保留生产检索,先去真实资料里确认收益是否存在。此后另一项独立改动让 PostgreSQL 在具备 ParadeDB pg_search 扩展和 BM25 索引时,可以选择 BM25 作为词法候选通道,向量检索和 RRF 融合仍然保留;普通 PostgreSQL 会明确回退到原有全文检索。这是可选工程接线,并不是本次实验已经证明 BM25 让金融研究更准确。
二、真正的难点:同一句话,三份基金资料都可能命中
后来我们用一项获准的公开基金研究任务继续检查。问题围绕基金代码 510500,以及一份 2023 年 2 月的 ETF 申购赎回清单(PCF)版本更新公告:哪些字段改变了,这份公告到底说明了多大的范围?
检索库里同时出现了 2021 年的旧版招募说明书、2023 年的公告和 2025 年的后续招募说明书。
这时,词语相似度开始显得不够用了。
2023 年公告第 1 页直接说明 510500 涉及的字段变更:原现金替代溢价字段调整为申购现金替代溢价字段,并增加赎回现金替代折价字段;公告的说明范围限于这次清单版本更新。这属于直接支持问题的来源。
2025 年招募说明书的一个示例页也能看到有关字段,但它只能提供后续资料中的背景与一致性线索。仅凭这个较新的页面,不能反推 2023 年修改的完整原因、范围或影响,更不能把它当作当天的实时持仓数据。
两页都可能包含相近词语,检索器也都可能把它们排得很高。区别却是金融研究必须认真处理的:“出现了这个词”与“证明了这次变化”不是一回事。
这也是为什么我们的人工审阅暂时使用“直接支持”“仅供背景”“未判定”等有范围的判断,不把所有被检索出来的页面硬塞进一个正负二分类。
三、先把比较对象放在同一张桌面上
最初的真实资料实验还有一个容易被忽略的不可比问题:离线 BM25 排序的是整页 PDF,而生产检索排的是切分后的 chunk。一个页面可能对应多个片段;一个排名前五的页面与排名前五的片段,并不是同一个分母。
后续我们固定了同一份获准的 RQ-08 候选范围:三个来源版本、共 306 个实际索引 chunk。离线 BM25、PostgreSQL 词法检索以及现有向量与词法融合(RRF)使用相同的候选范围,再分别保留完整名次与选出的结果。
这次比较至少回答了一个可靠的问题:到底是哪个检索阶段没有给出预期材料?
实际观察里,PostgreSQL 词法搜索在这组查询条件下没有返回候选;生产融合检索仍能给出五个结果。2023 年公告第 1 页对应的直接支持材料,在 BM25 和融合结果中都处于第一位。
因此,不能因为“词法通道为零”就写成“现有 RAG 找不到依据”,也不能因为 BM25 把某个后续说明书页面排在前面,就认定它的证据更充分。
BM25 是经典的词项相关性排序方法,RRF 则组合多个检索结果的名次,二者不是同一种评分刻度,分数更不是“这段文字支持金融主张的概率”。方法背景可参见 Robertson 与 Zaragoza 的 BM25 综述,以及 Cormack、Clarke、Büttcher 的 RRF 论文。
这里还有一条不能省略的限制:这 306 个 chunk 尚未获得完整的人工相关性标注,因此我们保存的是可复核的排序观察,没有据此计算真实 Recall、MRR 或模型答案的质量提升。
四、后来我发现,“检索工具可用”也不是“Agent 已经用上了”
即使排序比较本身成立,Agent 的研究链路还有下一关。
一次正常 HTTP 研究运行中,Host 已为 Run 保存了 Knowledge 授权,但模型的入口请求没有实际暴露工具定义。那次运行只发出一次模型请求,没有工具调用,也没有知识来源。
我不能仅凭这个结果说“模型不愿检索”。Host 已授权、工具进入本轮模型请求、模型实际提出调用,是三件事。 如果当前请求里没有工具定义,改 BM25 的分词器并不能解决这一层缺口。
后来另一轮独立的 RQ-08 真实运行确实调用了 Knowledge。持久记录中有 15 次已执行的知识检索调用,其中 13 份结果能在后续模型输入中找到对应的结果审阅提示。另两份 Child 结果也出现在后继输入里,但没有观察到相同的提示;原因尚不明确。
有意思的是,那轮运行初始的 SSE 摘要里工具调用数组为空。只看这一份摘要,会把实际调用漏掉;再沿持久执行记录和模型输入核对,才能还原这条链路。
现在我们至少能说:检索不是只在测试里运行,有真实模型请求取得并接收过知识结果。但输入里有材料,不代表模型的某条结论确实依据了它。这一步还需要对最终答案逐项核验。
五、下一次 RAG 评测,我会先问五个问题
| 检查位置 | 应当拿到的证据 |
|---|---|
| 资料资格 | 来源、版本、研究时点、授权范围和可用片段是否一致 |
| 候选比较 | 排序前是否使用相同、明确的候选集合和 chunk 粒度 |
| 相关性判断 | 哪些片段直接支持问题,哪些仅供背景,哪些尚未审阅 |
| 模型实际消费 | 工具是否在请求中可见、实际被调用、结果是否进入后续模型输入 |
| 最终主张 | 回答引用的是哪一份材料,是否支持具体结论,遗漏和错误如何独立复核 |
如果前两层没固定,比较 BM25 和 RRF 可能只是在比较不同的材料集合;如果第三层没做好,漂亮的 Recall 就可能来自构造标签;如果后两层缺席,评测再精细也只停留在检索器自身。
实际操作时,我会先固定问题和数据版本,用同一 Run 资格产生可比较的候选集;让独立审阅者记录关键页或片段究竟支持哪一项主张,并保留未判定集合。之后再检查真实模型请求、引用与回答。只有需要宣称“换算法让回答更好”时,才值得投入同题、同配置、同资料时点的匹配实验,记录质量与成本的实际变化。
这比直接开一个新索引、跑一张总分表慢一些,却能把“检索没命中”“资料没有资格”“模型没调用工具”和“模型引用了错误依据”分成不同的修复方向。
六、BM25 已经能接入,为什么仍不能宣布检索质量更好?
这次工作的成果不是宣布 BM25 没用,也不是宣布现有融合检索足够好。
它让我们看见了两个原本容易混在一起的问题:一方面,合成数据上的排序差距很大;另一方面,在实际审阅过的公告关键页面上,两种排序都能找到直接证据。真实模型也已经发生了 Knowledge 调用,但我们还没有完成对这些材料如何支持最终金融结论的独立评价。
因此,我们没有用合成数据的高分直接替换整个 RAG 架构。后续已实现的 BM25 只是满足数据库条件时可以启用的词法通道,现有向量和 RRF 仍负责联合检索;当时检查的主本地 PostgreSQL 实例没有安装 pg_search,仍走原有全文检索。一个算法现在可以接入系统,不代表它已经在日常运行中产生了可量化的研究质量收益。
下次遇到“RAG 命中了很多内容,答案却仍然不可靠”,我会先追问:需要的证据到底在哪一层丢失,还是证据已经送到,结论却没有被它支持?
只有回答了这个问题,检索改进才知道自己真正应该改哪里。