作者通过构建细粒度 RAG 错误归因工具发现:在 hit@k=0.86 的配置中,作为优化手段引入的 reranker 竟然是绝大多数 remaining miss 的根因,chunking/检索/fusion 问题需不同修复策略,聚合指标掩盖了真实瓶颈。
我的 RAG 评估会告诉我 hybrid: 0.86,然后我就坐在那里,不知道实际该改什么。要提升它?怎么提升?换 embedder?切更小的 chunk?加 reranker?aggregate score 能选出胜者,却完全不说失败者为什么失败。
所以我写了一个小工具来回答这个"为什么",运行它之后发现了一些我意想不到的东西:在性能最好的配置中,我为了提升结果而添加的组件 —— the reranker —— 几乎要为所有剩余的失败负责。
Here's the idea, the finding, and how to check it on your own corpus.
hit@k = 0.86 是一个平均值,掩盖了大量相互独立的失败,而这些失败的原因各不相同。一个 query 会 miss,是因为:
每一种情况都需要不同的修复方式。把它们平均成 0.86,就丢掉 了唯一能告诉你该拉哪个杠杆的信息。
修复方法是:不再把检索当作黑盒,而是在 pipeline 的每个阶段记录哪些内容存活了下来,然后把每个 missed query 归因到最早无法覆盖到答案的那个阶段:
representation → ann_index → candidate_generation → fusion →
reranker_demotion → final_cutoff → budget_cutoff
一个重要的细节能让这件事稳定:gold answers 以字符跨度(character spans)的形式存储在源文档中,而不是 chunk ID。这样一来当你改变 chunk 策略时标签不会坏掉,而且 scorer 能够把由多个 chunk 共同覆盖的答案正确归因。
我跑了 50 个配置(chunking × embedding × dense/BM25/hybrid × reranking,使用真实的 E5/BGE embedder 和 cross-encoder reranker)在一个小的合成 API 文档语料上 —— 22 篇文档,400 条带标签的 query。有三件事浮出水面,而只有归因视图能让它们显现出来。
最差到最优配置的 hit@k 跨度是 0.79 → 0.99。表格底部被小的固定 200 chunk 配置主导,它们的失败大多是"not retrieved"和"final cutoff",而它们的 hit 经常被标记为"fragile"——答案之所以被覆盖只是因为几个 chunk 拼凑在一起,所以小小的 chunking 改动就会打破它。顶部是 parent-child 800x200。换 E5 成 BGE 相比之下几乎没动任何东西。如果我只盯着 aggregate score,我会在 embedder 上折腾;是归因分析告诉我 chunk 策略才是出成绩的地方。
这是让我惊讶的一点。拿一个强配置——e5 · semantic · dense · rerank ce,hit@k 0.97。它的剩余 miss 去哪了?
Reranker demotion: 13 (all of them)
每一个 miss 都是 cross-encoder 把正确的 chunk 从 top-k 里拉出去了。这不是个例——在所有加了 reranker 的配置中,残留的 miss 压倒性地是 reranker_demotion。对比同一个配置不加 reranker(e5 · semantic · dense,hit@k 0.96):现在 miss 全是 final_cutoff——那些排名本来没问题但刚好落在第 k 名之后的 chunk。
所以诚实的解读不是"reranker 是坏的"。reranker 把 MRR 提升了不少(0.80 → 0.86),也把 hit@k 往上推了。但一旦检索本身已经很强,reranker 就成了剩余失败中最大的单一来源——这指向一个精确的、小范围的修复(增加 candidate 深度 / rerank top-N,或者调优 reranker),而不是"检索坏了,推倒重来"。
最优配置是 0.99 [0.98–1.00];后面几个是 0.98 [0.97–0.99]。这些区间重叠了——在 400 条 query 上,0.99 对比 0.98 是平局,不是胜利。没有 CI 你会"选 0.99 的那个"然后对噪声自我庆祝。
最优配置 hit@k 0.99,但平均检索了 ~577 个 token。一个 e5 · recursive 400 · hybrid · ce 配置达到 0.963,只用了 ~311 个 token——质量差约三分,但上下文少了一半。如果你受制于 context 或成本,这是更聪明的选择,而 quality-vs-tokens(Pareto)视图正是揭示它的工具。
离线演示不需要 API key 或下载模型:
pip install retrieval-lab
retrieval-lab demo
要跑真实的 sweep,你给它两个 JSONL 文件——你的文档和带标签的 query——它会写出一个独立的 HTML 报告(排名、置信区间、分阶段归因、延迟/成本,以及 Pareto 视图):
pip install "retrieval-lab[real-embed,rerank]"
retrieval-lab run \
--corpus docs.jsonl \
--queries queries.jsonl \
--embed-models e5,bge \
--chunkers fixed:200,fixed:400,recursive:400,parentchild:800x200 \
--retrieval dense,sparse,hybrid \
--rerank none,ce \
--html report.html
Live example report (the 50-config sweep above): https://ashwinugale.github.io/Retrieval-Lab/ Code: https://github.com/AshwinUgale/Retrieval-Lab
It's only as representative as your labeled query set — a thin or biased set biases the winner. Every score describes your corpus, never "best" in the abstract.
Missing valid gold alternatives make measured recall a lower bound.
Latency and index cost are whatever your machine reports.
Stage attribution needs a decomposable pipeline; a black-box retriever can only be scored at its output. It's beta. If the attribution gets something wrong for you — misattributes a miss, or blames a stage you don't think is at fault — that's exactly the feedback I want. What's the failure stage you wish your RAG eval could point at?
For further actions, you may consider blocking this person and/or reporting abuse