RAG 不靠感觉:20 条评测集、命中率与拒答阈值
为什么要评测
搭一个 RAG 检索问答不难,难的是回答这个问题:它到底行不行?
我见过太多”效果还行”的知识库——问几个问题答得不错,就上线了。但真正该问的是:
- 该答的,答对了吗?
- 不该答的(知识库里没有的),它有没有硬编?
- 我调了分块/模型/阈值之后,是变好了还是变坏了?
没有评测集,这三个问题一个都答不了,只能靠感觉。而”感觉变好了”是最贵的错觉。
一、评测集:20 条,其中 5 条必须”答不出来”
我用的语料是一份自造的合成手册(6 篇文档),问题分两类:
| 类型 | 数量 | 期望行为 |
|---|---|---|
| 正例(语料里有答案) | 15 | 检索到正确文档并引用 |
| 负例(语料里没有答案) | 5 | 明确拒答,不能编 |
负例才是评测的灵魂。 只测正例的系统,会奖励”什么都敢答”的模型——分数很好看,上线全是幻觉。负例的作用就是把”敢编”的成本显式计进来。
设计负例有个小技巧:用同领域但语料里不存在的问题(比如手册里写了差旅报销,我就问”加班餐补每天多少钱”)。这样模型不能靠”话题不相关”蒙对,必须真的去检索。
二、四个指标
1 | 命中@1 / 命中@3 / 命中@5 正确文档是否出现在前 k 条 |
三、实测结果(零依赖骨架)
这套骨架我刻意用纯标准库实现(BM25 + 向量通道占位 + RRF 融合 + 轻量重排),目的是先把评测闭环跑通,而不是先堆模型:
| 指标 | 结果 |
|---|---|
| 命中@1 | 0.933 |
| 命中@3 | 1.000 |
| MRR | 0.967 |
| 负例拒答率 | 1.000 |
| 端到端 | 1.000 |
注意:这不是”我的 RAG 很好”的意思。 语法语料干净、问题与原文措辞接近,分数自然高。真实的内部文档会有表格、扫描件、术语变体、多版本冲突——那时候分数会掉下来,而评测集的作用就是让这个”掉”看得见。
四、拒答阈值:用扫描定,不用拍脑袋
拒答不是一个布尔开关,而是一个阈值。阈值定得太低,什么都敢答;定得太高,该答的也不答。所以我把阈值扫描出来看权衡:
| 阈值 | 正例命中@3 | 负例拒答率 | 端到端 |
|---|---|---|---|
| 0.10 | 100% | 0% | 0.75 |
| 0.25 | 100% | 40% | 0.85 |
| 0.34 | 100% | 100% | 1.00 |
| 0.45 | 93% | 100% | 0.95 |
| 0.50 | 87% | 100% | 0.90 |
看这张表就明白为什么不能拍脑袋了:阈值 0.10 时”命中率”满分(因为它什么都答),但端到端只有 0.75;真正的最优在 0.34。只盯命中率,你会一路把阈值调低——然后得到一个自信的胡说八道机器。
五、踩到的坑:先怀疑标注,再怀疑系统
评测跑出来,有一条失败:问”离职后多久回收 VPN 账号和系统权限”,检索排到了《离职交接》那篇,而不是我标注的《VPN 与远程办公》。
第一反应是”检索有问题”。但打开语料一看——两篇文档都写了这件事(《离职交接》原文就是”按 VPN 规范中的时限回收”)。这不是检索错,是我的评测标注有问题:这条问题的正确答案其实是两篇都算。
从这里得到两条经验:
- 评测失败先查标注。语料本身有重叠、有重复表述时,评测集不是”标准答案”,而是”我以为的标准答案”。
- 评测集本身也要评审:每条问题写下”为什么期望这个答案”,比只写答案有用。
六、下一步的升级路径
评测闭环跑通之后,替换组件就是有方向的迭代:
- 真向量检索:换成多语言 embedding(bge-m3 / qwen3-embedding 一类),先看命中@3 提升多少;
- 重排:加 cross-encoder 重排,主要影响命中@1 与 MRR;
- 混合检索:BM25 与向量做融合,比单通道更稳(专有名词靠词面,语义靠向量);
- 引用溯源:回答必须带文档出处,前端可点开原文——这一步对”能不能被信任”比指标更重要。
每一步都跑同一套评测集。这样”改进”就不是感觉,而是一次可比的实验。
小结
如果只能带走一句话:先写 5 条”应该答不出来”的问题。 它们会立刻暴露你的系统到底是在检索,还是在编。