← 回到知识图谱
实践持续完善

RAG 实验室:把检索质量算清楚

本文总结统一 Recall、Hit、MRR 与 nDCG 的口径,用小样本、消融和无答案集定位问题。

创建 2026-10-31更新 2026-11-02阅读约 14 分钟
Agent工程实践扩展学习

先固定评估单位

算指标之前,先要定下“一条结果”指的是什么。同一个问题可能有好几份相关文档,一份文档入库时又会切成很多片段(chunk)。比如《差旅费管理办法》2026 版切成了 40 个片段,第 4.2 条(市内交通)正好被切成两段:4.2-a 写“上海市内交通每次 80 元”,4.2-b 写“超出部分须主管审批”。

如果标注员标的是文档级答案“《差旅费管理办法》”,而检索系统返回的是片段,评分就会出错。返回前 3 名是第 1.1 条、第 6.3 条、第 7.2 条,都属于这份办法,按文档级口径算 Hit@3 = 1,可三条里没有一条讲市内交通。反过来,标注的是片段 4.2-a,系统返回了 4.2-b,按片段级口径算没命中,可 4.2-b 对回答“超标怎么办”其实也有用。所以先定好标注到哪一级,评分就按同一级来;需要片段级评分,就把标注细到片段。

还有三件事要提前固定:

  • 去重:同一份文档的三个相邻片段都进了前 3 名,算三条命中还是一条?不定下来,一个“切得碎”的文档就能把 Recall 撑高。常见做法是按文档 ID 或条款 ID 去重后再算。
  • 版本快照:库里同时有办法 2025 版(标准 60 元)和 2026 版(标准 80 元)。评测时要写明用的是哪天构建的索引,标注也要对应同一版本,否则检索出 2025 版第 4.2 条该算对还是错,谁也说不清。
  • 权限过滤:同一个问题,P6 员工和财务人员能看到的文档不一样。评测要固定用哪个身份跑,并且在检索阶段就过滤,过滤后的结果才是要打分的结果。

RAG 双阶段流程:离线解析与索引,在线权限过滤、混合召回、重排、生成和引用校验RAG 双阶段流程:离线解析与索引,在线权限过滤、混合召回、重排、生成和引用校验 · 点击放大

Recall 与 Hit 不是一回事

先用一个例子把两个指标分开。员工问“打车超标怎么报”,标注员认为能回答这个问题的条款有三条:A(市内交通标准)、B(超标处理)、C(审批权限)。检索系统返回了前 3 条结果:A、X、B,其中 X 是一条无关的住宿条款。

  • Recall@3 问的是:三条该找的证据,前 3 名里找回了几条?找回 A 和 B,共 2 条,所以 Recall@3 = 2/3 ≈ 0.67。
  • Hit@3 问的是:前 3 名里有没有至少一条对的?有(A 就是),所以 Hit@3 = 1。只要命中一条就是 1,一条都没有就是 0,没有中间值。

所以 Hit 更宽松,可以理解为“这次检索有没有完全落空”。Recall 更严格,看的是“该找的找全了多少”。

写成公式:

Recall@K⁡=∣R∩TK∣∣R∣,Hit@K⁡=1[ ∣R∩TK∣>0 ]\operatorname{Recall@K}=\frac{|R\cap T_K|}{|R|},\qquad \operatorname{Hit@K}=\mathbf{1}[\,|R\cap T_K|>0\,]

逐个符号看:

符号 读法 在上面例子里是
RR 这道题全部标注相关的证据 {A, B, C}
TKT_K 检索返回的前 K 条(去重后) {A, X, B},K = 3
R∩TKR\cap T_K 两个集合的交集,也就是“返回了、而且确实相关”的那几条 {A, B}
∣⋅∣\lvert\cdot\rvert 集合里有几个元素 ∣R∩TK∣=2\lvert R\cap T_K\rvert=2,∣R∣=3\lvert R\rvert=3
1[条件]\mathbf{1}[\text{条件}] 指示函数:条件成立记 1,不成立记 0 条件“交集个数 > 0”成立,记 1

Hit 公式看着唬人,其实就是把“有没有命中”写成了 0 或 1。

上面算的是一道题。评测集有很多题,就把每道题的分数加起来再除以题数。下面三道题算一遍:

题 标注证据 RR 返回前 3 名 Recall@3 Hit@3
q1 打车超标 A、B、C A、X、B 2/3 1
q2 住宿标准 D Y、Z、D 1/1 = 1 1
q3 跨境报销 E、F U、V、W 0/2 = 0 0
平均 (0.67 + 1 + 0) / 3 ≈ 0.56 2/3 ≈ 0.67

平均下来的 Hit@3 常被叫做 Hit Rate:这里 3 道题有 2 道没有落空,就是 67%。同一批结果的 Recall@3 只有 56%,因为 q1 漏了 C。

两个指标什么时候相等?每道题只有一条相关证据时。比如 q2 只有 D,那么命中就等于找全,Recall 和 Hit 都是 1。所以如果你的数据集大多是“一问一条答案”,两者差不多;只要有多跳、需要多条证据的题,就必须分开报,否则会把“找到一条”误报成“找全了”。

没有标准答案的题(例如问制度里根本没写的事)不能放进来算:∣R∣=0\lvert R\rvert=0 时 Recall 要除以零,强行记 0 又会冤枉检索系统。这类题单独统计“正确拒答”和“错误作答”两个数。

排得靠前,还要排得完整

Recall 和 Hit 只管“前 K 名里有没有”,不管排第几。可同样是命中,排第 1 和排第 3 对用户和模型差别很大。MRR 就是用来奖励“排得靠前”的:

MRR@K⁡=1N∑q=1N1rq\operatorname{MRR@K}=\frac{1}{N}\sum_{q=1}^{N}\frac{1}{r_q}
  • NN 是题目数,∑q=1N\sum_{q=1}^{N} 表示把第 1 题到第 N 题的值加起来。
  • rqr_q 是第 q 题里第一条相关结果排在第几名。1/rq1/r_q 叫倒数排名:排第 1 得 1 分,排第 2 得 0.5 分,排第 3 得 0.33 分。前 K 名都没命中记 0 分。

还是上面三道题:q1 第一条相关的 A 排第 1,得 1;q2 的 D 排第 3,得 1/3;q3 没命中,得 0。MRR@3 = (1 + 0.33 + 0) / 3 ≈ 0.44。q2 虽然 Hit 和 Recall 都是满分,在 MRR 里却只拿了三分之一,MRR 要抓的就是这种情况。

MRR 也有盲区:它只看第一条相关结果。q1 需要 A、B、C 三条,MRR 对“B 排第几、C 有没有出现”完全不关心。需要多条证据、而且证据有主次之分时,用 nDCG:

DCG@K⁡=∑i=1K2reli−1log⁡2(i+1),nDCG@K⁡=DCG@K⁡IDCG@K⁡\operatorname{DCG@K}=\sum_{i=1}^{K}\frac{2^{rel_i}-1}{\log_2(i+1)},\qquad \operatorname{nDCG@K}=\frac{\operatorname{DCG@K}}{\operatorname{IDCG@K}}
  • ii 是名次,从 1 到 K。
  • relirel_i 是第 i 名结果的相关等级,由人工事先约定,例如 2 = 能直接回答,1 = 有帮助,0 = 无关。分子 2reli−12^{rel_i}-1 让高等级的结果拿到明显更多的分:等级 2 得 3 分,等级 1 得 1 分,等级 0 得 0 分。
  • 分母 log⁡2(i+1)\log_2(i+1) 是位置折扣,名次越靠后除得越多:第 1 名除以 1,第 2 名除以 1.58,第 3 名除以 2。
  • IDCG 是把同一批证据按最理想的顺序排好后算出的 DCG,作为满分。nDCG 就是“实际得分 ÷ 满分”,落在 0 到 1 之间,方便不同题之间比较。

拿 q1 算一遍。设 A 和 C 等级为 2,B 等级为 1,X 等级为 0。实际排序 A、X、B:

DCG@3⁡=22−1log⁡22+20−1log⁡23+21−1log⁡24=31+0+12=3.5\operatorname{DCG@3}=\frac{2^2-1}{\log_2 2}+\frac{2^0-1}{\log_2 3}+\frac{2^1-1}{\log_2 4}=\frac{3}{1}+0+\frac{1}{2}=3.5

理想排序是 A、C、B(先放两条等级 2,再放等级 1):

IDCG@3⁡=31+31.585+12≈3+1.893+0.5=5.393\operatorname{IDCG@3}=\frac{3}{1}+\frac{3}{1.585}+\frac{1}{2}\approx 3+1.893+0.5=5.393

nDCG@3 = 3.5 / 5.393 ≈ 0.65。扣分主要来自两处:第 2 名放了无关的 X,最重要的 C 没进前 3。

一道题如果所有证据都无关,IDCG = 0,nDCG 没法算。这种题和无答案题一样,要提前约定怎么处理并在报告里单列。跨题汇总时是简单平均还是按题型加权,也要写明。

混合检索:先对齐身份,再融合名次

BM25 给出的分数可能是 12.7、8.3 这种没有上限的数,向量检索给的余弦相似度在 0.8 上下。两种分数直接相加,BM25 那一路几乎总是说了算。RRF(倒数排名融合)干脆不看分数,只看名次:

RRF⁡(d)=∑j: d∈Lj1k+rank⁡Lj(d)\operatorname{RRF}(d)=\sum_{j:\,d\in L_j}\frac{1}{k+\operatorname{rank}_{L_j}(d)}
  • dd 是某一个文档或片段。
  • LjL_j 是第 j 路检索返回的列表,比如 L1L_1 是 BM25 的结果,L2L_2 是向量检索的结果。
  • ∑j: d∈Lj\sum_{j:\,d\in L_j} 的意思是:只在 dd 出现过的那几路里求和,没出现的那一路不加分。
  • rank⁡Lj(d)\operatorname{rank}_{L_j}(d) 是 dd 在第 j 路里排第几,从 1 开始数。
  • kk 是一个平滑常数,常用 60。它和 Top-K 的 K 没有关系。

用 k = 60 算一个例子。BM25 返回 [条款 4.2, 条款 6.1, 条款 3.5],向量检索返回 [条款 5.3, 条款 6.1, 条款 4.2]:

片段 BM25 名次 向量名次 RRF 得分
条款 4.2 1 3 1/61 + 1/63 ≈ 0.0164 + 0.0159 = 0.0323
条款 6.1 2 2 1/62 + 1/62 ≈ 0.0161 + 0.0161 = 0.0323
条款 5.3 无 1 1/61 ≈ 0.0164
条款 3.5 3 无 1/63 ≈ 0.0159

两路都找到的 4.2 和 6.1 排在前面(两者只差小数点后第五位,这里显示成一样),只被一路找到的 5.3 即使在向量那边排第一,也落到第三。RRF 看重的就是这一点:两种检索方法都认可的结果更可信。

kk 越大,第 1 名和第 10 名的分差越小,融合更看重“出现在几路里”;kk 越小,越看重“在某一路里排第一”。把上面的例子改成 k = 1 再算:4.2 得 1/2 + 1/4 = 0.75,6.1 得 1/3 + 1/3 ≈ 0.67,5.3 得 1/2 = 0.5。4.2 因为在 BM25 里排第一,明显超过了 6.1;5.3 的得分也从前两名的一半左右涨到三分之二左右。k 的取值就是在这两种倾向之间调节。

实际使用还要注意三点:不同检索器必须先把结果映射到同一个文档或片段 ID,否则同一条款会被当成两条;召回窗口、k、rerank 数量都要在验证集上选;融合后的分数只用来排序,不代表答案正确的概率。公式与实现口径参见 Elasticsearch RRF 官方文档。

一个可以开始做的消融表

消融的意思是每次只改一处,看指标怎么变。如果从 A 直接跳到 D,同时加了向量召回、融合和 reranker,分数涨了也说不清是谁的功劳。

版本 唯一变化 要观察什么
A 仅 BM25 精确名词、编号能否命中
B 仅向量召回 同义改写是否改善
C BM25 + 向量 + RRF 两路互补是否成立
D C 加 reranker Top-K 的有效证据是否前移
Oracle 直接给标准证据 生成器是否仍然答错

每一行要观察的东西,用具体问题去看最清楚(以下是示意,不是实测):

  • 问“INV-102 能报多少”,关键在发票编号这种精确字符串。BM25 一般能直接命中含 INV-102 的记录;向量检索可能把 INV-103、INV-120 也排上来,因为它们在语义空间里离得很近。
  • 问“打车超出额度了咋办”,制度原文写的是“市内交通费用超出标准”。字面上几乎没有重合的词,BM25 容易漏掉第 4.2 条,向量检索更可能找到。
  • C 版本要看的是:上面两类问题是不是都能答好。如果 C 在“编号题”上比 A 差,说明融合把精确命中挤下去了,可以回头调 RRF 的 k 或召回窗口。
  • D 版本要看的是:第 4.2 条是不是从第 5 名挪到了第 1、2 名。可以对比 C 和 D 的 MRR@5。送进 reranker 的候选如果是前 50 条,Recall@50 不会变,因为 reranker 只给这 50 条重新排序,不会召回新东西。
  • Oracle 行是把标注好的第 4.2 条直接塞给生成器。如果这时它还答“超标 18.5 元”,问题出在生成或计算上(应为 118.5 − 80 = 38.5 元),继续调检索没有用。

可以先手工标注 30 个问题做探索,覆盖单跳、多跳、无答案、冲突(比如 2025 版和 2026 版说法不同)、表格和跨权限几类。30 题只够起步,从 Agent 评测:从测试集到发布门槛 里的区间算法看,30 题对 24 题的 95% 区间大约是 63% 到 90%,两个版本差几个百分点基本分不出来,不能当作充分的统计证据。

每次运行固定模型、文档版本和 Prompt。记录召回、引用支持率、拒答质量、延迟和费用。表里不要预先填“提升多少”,跑出来是多少就写多少。

防止评测泄漏

题目分两份:开发集用来调参数,比如 RRF 的 k、召回窗口、reranker 的截断数;保留集要等方案定下来、不再改了,才拿出来跑一次。如果在保留集上看了分数又回头改参数,它就变成了第二个开发集,分数会偏高。

近重复的问题要分到同一边。开发集里有“打车超标怎么报”,测试集里有“出租车费用超过标准了怎么报销”,两句话问的是同一件事,答案都是第 4.2 条。参数在前一句上调好了,后一句自然也答得好,测试分数就虚高了。做法是先按“答案落在哪份文档、哪一条”给题目分组,同一组整体放进开发集或测试集。同一份文档改写出来的多个变体也照此处理。

重复生成不等于新样本。30 道题每道生成 3 次,得到 90 个回答,这 3 次只反映模型的随机波动,样本量仍然是 30 道题。算区间、做 bootstrap 时按题来,完整流程见 Agent 评测:从测试集到发布门槛。

原始背景

Lewis 等,RAG(2020) 提出把参数化的生成模型和非参数化的检索记忆结合起来:先用一个稠密检索器从维基百科段落里找出相关段落,再让序列到序列的生成模型以这些段落为条件生成答案,训练时检索器的查询编码器和生成器一起微调,文档索引保持不变。论文给了两种用法:RAG-Sequence 让整段回答基于同一批检索到的段落,RAG-Token 允许每个 token 参考不同的段落。

本文讲的混合检索、权限过滤和这些评估练习,是面向企业应用系统的扩展做法,不属于原论文的方案。

这是一篇可以继续生长的笔记。沿着关系继续阅读 ↗