为什么要评测

搭一个 RAG 检索问答不难,难的是回答这个问题:它到底行不行?

我见过太多”效果还行”的知识库——问几个问题答得不错,就上线了。但真正该问的是:

  • 该答的,答对了吗?
  • 不该答的(知识库里没有的),它有没有硬编?
  • 我调了分块/模型/阈值之后,是变好了还是变坏了?

没有评测集,这三个问题一个都答不了,只能靠感觉。而”感觉变好了”是最贵的错觉。

一、评测集:20 条,其中 5 条必须”答不出来”

我用的语料是一份自造的合成手册(6 篇文档),问题分两类:

类型 数量 期望行为
正例(语料里有答案) 15 检索到正确文档并引用
负例(语料里没有答案) 5 明确拒答,不能编

负例才是评测的灵魂。 只测正例的系统,会奖励”什么都敢答”的模型——分数很好看,上线全是幻觉。负例的作用就是把”敢编”的成本显式计进来。

设计负例有个小技巧:用同领域但语料里不存在的问题(比如手册里写了差旅报销,我就问”加班餐补每天多少钱”)。这样模型不能靠”话题不相关”蒙对,必须真的去检索。

二、四个指标

1
2
3
4
命中@1 / 命中@3 / 命中@5   正确文档是否出现在前 k 条
MRR 首个正确结果的排名倒数(衡量"排得靠不靠前")
负例拒答率 该拒答的是否拒答
端到端准确率 正例命中 + 负例拒答 的综合

三、实测结果(零依赖骨架)

这套骨架我刻意用纯标准库实现(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 规范中的时限回收”)。这不是检索错,是我的评测标注有问题:这条问题的正确答案其实是两篇都算。

从这里得到两条经验:

  1. 评测失败先查标注。语料本身有重叠、有重复表述时,评测集不是”标准答案”,而是”我以为的标准答案”。
  2. 评测集本身也要评审:每条问题写下”为什么期望这个答案”,比只写答案有用。

六、下一步的升级路径

评测闭环跑通之后,替换组件就是有方向的迭代:

  1. 真向量检索:换成多语言 embedding(bge-m3 / qwen3-embedding 一类),先看命中@3 提升多少;
  2. 重排:加 cross-encoder 重排,主要影响命中@1 与 MRR;
  3. 混合检索:BM25 与向量做融合,比单通道更稳(专有名词靠词面,语义靠向量);
  4. 引用溯源:回答必须带文档出处,前端可点开原文——这一步对”能不能被信任”比指标更重要。

每一步都跑同一套评测集。这样”改进”就不是感觉,而是一次可比的实验。

小结

如果只能带走一句话:先写 5 条”应该答不出来”的问题。 它们会立刻暴露你的系统到底是在检索,还是在编。