RAG 实验室:把检索质量算清楚
本文总结统一 Recall、Hit、MRR 与 nDCG 的口径,用小样本、消融和无答案集定位问题。
先固定评估单位
算指标之前,先要定下“一条结果”指的是什么。同一个问题可能有好几份相关文档,一份文档入库时又会切成很多片段(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 双阶段流程:离线解析与索引,在线权限过滤、混合召回、重排、生成和引用校验 · 点击放大
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 更严格,看的是“该找的找全了多少”。
写成公式:
逐个符号看:
| 符号 | 读法 | 在上面例子里是 |
|---|---|---|
| 这道题全部标注相关的证据 | {A, B, C} | |
| 检索返回的前 K 条(去重后) | {A, X, B},K = 3 | |
| 两个集合的交集,也就是“返回了、而且确实相关”的那几条 | {A, B} | |
| 集合里有几个元素 | , | |
| 指示函数:条件成立记 1,不成立记 0 | 条件“交集个数 > 0”成立,记 1 |
Hit 公式看着唬人,其实就是把“有没有命中”写成了 0 或 1。
上面算的是一道题。评测集有很多题,就把每道题的分数加起来再除以题数。下面三道题算一遍:
| 题 | 标注证据 | 返回前 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。所以如果你的数据集大多是“一问一条答案”,两者差不多;只要有多跳、需要多条证据的题,就必须分开报,否则会把“找到一条”误报成“找全了”。
没有标准答案的题(例如问制度里根本没写的事)不能放进来算: 时 Recall 要除以零,强行记 0 又会冤枉检索系统。这类题单独统计“正确拒答”和“错误作答”两个数。
排得靠前,还要排得完整
Recall 和 Hit 只管“前 K 名里有没有”,不管排第几。可同样是命中,排第 1 和排第 3 对用户和模型差别很大。MRR 就是用来奖励“排得靠前”的:
- 是题目数, 表示把第 1 题到第 N 题的值加起来。
- 是第 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:
- 是名次,从 1 到 K。
- 是第 i 名结果的相关等级,由人工事先约定,例如 2 = 能直接回答,1 = 有帮助,0 = 无关。分子 让高等级的结果拿到明显更多的分:等级 2 得 3 分,等级 1 得 1 分,等级 0 得 0 分。
- 分母 是位置折扣,名次越靠后除得越多:第 1 名除以 1,第 2 名除以 1.58,第 3 名除以 2。
- IDCG 是把同一批证据按最理想的顺序排好后算出的 DCG,作为满分。nDCG 就是“实际得分 ÷ 满分”,落在 0 到 1 之间,方便不同题之间比较。
拿 q1 算一遍。设 A 和 C 等级为 2,B 等级为 1,X 等级为 0。实际排序 A、X、B:
理想排序是 A、C、B(先放两条等级 2,再放等级 1):
nDCG@3 = 3.5 / 5.393 ≈ 0.65。扣分主要来自两处:第 2 名放了无关的 X,最重要的 C 没进前 3。
一道题如果所有证据都无关,IDCG = 0,nDCG 没法算。这种题和无答案题一样,要提前约定怎么处理并在报告里单列。跨题汇总时是简单平均还是按题型加权,也要写明。
混合检索:先对齐身份,再融合名次
BM25 给出的分数可能是 12.7、8.3 这种没有上限的数,向量检索给的余弦相似度在 0.8 上下。两种分数直接相加,BM25 那一路几乎总是说了算。RRF(倒数排名融合)干脆不看分数,只看名次:
- 是某一个文档或片段。
- 是第 j 路检索返回的列表,比如 是 BM25 的结果, 是向量检索的结果。
- 的意思是:只在 出现过的那几路里求和,没出现的那一路不加分。
- 是 在第 j 路里排第几,从 1 开始数。
- 是一个平滑常数,常用 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 看重的就是这一点:两种检索方法都认可的结果更可信。
越大,第 1 名和第 10 名的分差越小,融合更看重“出现在几路里”; 越小,越看重“在某一路里排第一”。把上面的例子改成 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 参考不同的段落。
本文讲的混合检索、权限过滤和这些评估练习,是面向企业应用系统的扩展做法,不属于原论文的方案。