知识库回答底下挂着两条引用,内容读起来也很顺,就算答对了吗?
先看一个问题:“当前日志保留多久,能导出什么格式?”系统找到了“保留 7 天”,于是回答“日志保留 7 天”,还附上正确出处。它没有编造,但漏掉了用户问的导出格式。换一次检索,系统找到旧版的“保留 30 天”,照样能生成一句有出处的错误答案。
这两种问题,靠一个笼统的“回答质量分”很难定位。做 RAG,值得先建立一套能区分没找到、没答全、用错版本、无依据仍作答的检查方法,再决定要不要换 Embedding、加重排或接 Agent。
这是「RAG 评测实战」第一篇。读完可以拿走三样东西:一张故障定位图、一套明确分母的指标,以及一个无需 API Key 的可运行计分器。
实验边界:本文提供的是合成教学样例。文档、两组检索结果、回答与审核标签均为手工构造,没有调用真实检索器或大模型。代码实际执行的是计分和回归检查;这些分数不能证明某个模型、检索策略或生产系统更好。 资料核对日期为 2026 年 9 月 30 日。
1. 把 RAG 拆成能排查的三段#
RAG 的基本思路,是在生成时引入检索到的外部信息。原始 RAG 论文讨论了参数化模型与非参数化知识索引的组合;如今工程实现可以有不同的切块、检索和生成方式,不能把某一种框架的调用链当作唯一标准。原始论文
对排错而言,可以先保留三段:
- 检索前:文档是否完整,版本是否有效,当前用户是否有权读取
- 检索中:正确证据是否进入候选,重排后是否仍在最终上下文里
- 生成后:回答是否受证据支持,是否覆盖问题,证据不足时是否停止下结论
作者设计的排错示意图,表示检查位置,不表示实测性能。虚线表示用同一份人工核验基准检查不同阶段。
排查时不要只存最终回答。最小记录还应包括问题 ID、知识库快照、文档版本、候选排序、实际送入模型的片段、模型与提示词版本、引用和耗时。若只保留最终的三段文字,很难判断错误发生在召回、重排、上下文截断还是生成。
2. 先把“正确答案”写成可检查的约定#
以文档问答为例,一条评测题至少要有:问题、可回答性、必需证据、参考事实,以及适用版本。不要只写一段标准答案,再用字符串相等判断所有回答。
本例使用四条虚构文档:
| 文档 ID | 内容与状态 |
|---|---|
| retry-v2 | 当前说明:R-17 表示限流,等待 30 秒重试 |
| retention-v1 | 已失效的旧说明:日志保留 30 天 |
| retention-v2 | 当前说明:日志保留 7 天 |
| export-v2 | 当前说明:支持导出 JSON |
对应四道题:
| 测试切片 | 问题 | 检查重点 |
|---|---|---|
| 精确标识 | R-17 要等多久再重试? | 错误码是否找到正确条目 |
| 多事实 | 保留多久,支持什么导出格式? | 两个问题是否都回答 |
| 版本冲突 | 按当前说明,日志保留多久? | 旧文档有出处也不能充当当前答案 |
| 无答案 | 最多能扩容到多少天? | 资料未说明时能否明确告知缺口 |
这里把 retention-v2 和 export-v2 都列为第二题的必需证据。现实数据里,同一个事实可能被多个片段等价支持,不能要求系统找齐所有重复文档。此时应标注“满足同一事实的备选证据组”,或者直接按参考事实是否被上下文覆盖来评分。
切块策略一变,chunk ID 也可能变化。对照实验应保留稳定的原始文档 ID、版本和证据文本范围,再映射到新切块。否则评测分数可能只是因为 ID 变化而下降。
3. 四层指标,分别回答四个问题#
证据召回:该找的内容进来了多少?#
本例的 Evidence Recall@k,是“前 k 个结果中命中的必需证据 ID 数 ÷ 必需证据 ID 总数”。每个 ID 只计一次,重复结果直接报错,避免重复片段虚增计数。
第二题需要两个证据,前两条只找到一个,Recall@2 就是 1/2。若基准本来就没有答案,分母为空,应记为“不适用”,不能填 0 或 1 再混进可回答题的平均值。
这个口径接近 Ragas 的 ID-based Context Recall。Ragas 也提供把参考答案拆成事实、判断它们能否从检索上下文得到支持的口径。不同口径不能只因为都叫 Recall 就放在同一张榜单比较。Ragas:Context Recall
答案支持度:说出来的事实,有多少能从上下文得到支持?#
把回答拆成事实主张,逐条判断上下文是否支持,再算“受支持主张数 ÷ 总主张数”。Ragas 的 Faithfulness 采用这一类思路,衡量的是回答与给定上下文的一致性。Ragas:Faithfulness
注意两个边界:
- 回答只说“保留 7 天”,可能支持度为 100%,但没有回答导出格式
- 回答引用旧版“保留 30 天”,可能忠实于那段旧文本,但不满足“当前版本”的问题
因此,忠于检索文本、符合当前事实、完整解决问题,需要分别检查。 检索到了什么不能自动成为正确性的唯一基准。
参考事实覆盖:用户需要的信息,究竟答全了多少?#
本例额外定义一个简单指标:正确表达的参考事实数 ÷ 应回答的参考事实数。它是本文用于故障定位的指标,不冒充通用行业标准。
第二题需要“7 天”和“JSON”两个事实,只回答前者就得 1/2。答对这两项也不代表整段回答都正确:若又补上一句没有来源的“可无限扩容”,事实覆盖仍为 100%,但答案支持度会下降。两个指标正好互相补充。
拒答:不知道时停下来,知道时正常回答#
无答案题单独统计正确拒答率;可回答题单独统计误拒答率。若系统把每题都回复成“不知道”,前一个指标可能很好,后一个会直接暴露问题。
这里的拒答是清楚说明“现有材料没有这个信息”,并指出还缺什么证据。真实系统里,有时更合适的动作是追问版本、时间或适用对象;那就应把“澄清问题”单独标注,而不是硬塞进二分类。
此外,引用 ID 存在、且位于最终上下文,只能证明引用的格式和范围合法。它不能证明被引用的片段支持这句话。 下面脚本把这项检查命名为 citationPresenceAndValidity,没有把它叫作“引用准确率”。
4. 跑一次零依赖的合成样例#
把计分脚本 evaluate.mjs和样例数据 fixture.json下载到同一个目录,先查看代码,再用 Node.js 18 或以上版本运行:
node evaluate.mjs fixture.json 2
脚本不联网,不创建索引,不调用模型,也不需要密钥。JSON 文件中包含四道题和两组手工填写的结果,分别名为 synthetic-baseline 与 synthetic-corrected。
召回计分的核心非常短:
export function recallAtK(requiredIds, rankedIds, k) {
assert(Number.isInteger(k) && k > 0, 'k must be a positive integer')
const required = ids(requiredIds, 'requiredIds')
ids(rankedIds, 'rankedIds')
if (!required.size) return null
const retrieved = new Set(rankedIds.slice(0, k))
return [...required].filter((id) => retrieved.has(id)).length / required.size
}
完整文件中的 ids 会检查 ID 类型和重复项;evaluate 还检查缺失题目、未知文档、引用范围与拒答记录的一致性。输出既包含逐题结果,也包含汇总。用随附数据和 k=2 运行,得到:
| 指标及分母 | 手工基线 | 手工修正样例 |
|---|---|---|
| 必需证据召回,3 道可回答题的宏平均 | 0.50 | 1.00 |
| 参考事实覆盖,3 道可回答题的宏平均 | 0.50 | 1.00 |
| 答案支持度,按事实主张数微平均 | 3/4 | 4/4 |
| 正确拒答,无答案题 | 0/1 | 1/1 |
| 误拒答,可回答题 | 0/3 | 0/3 |
“宏平均”是先算每题再平均,让每道题权重相同;“微平均”是把主张汇总后相除,主张较多的回答权重更大。报告时必须把口径写出来。
最值得读的是逐题输出:基线的 stale-version 支持度为 1,证据召回和参考事实覆盖却都是 0;two-facts 支持度也为 1,另两项只有 0.5。这正是“有出处仍答错”和“没编造却没答全”的区别。
修正样例的满分是手工构造的预期结果,不是优化实验。 这里只验证计分器会不会按定义工作,不能把 0.50 到 1.00 写成某种 RAG 技术带来的提升。
还有一条重要约束:review.supportedByIds 和 review.coveredReferenceFactIds 是可信审核者的标签。脚本不会理解自然语言,也不会验证标签本身是否诚实。接入真实结果时,应先从完整回答提取主张,再由人工或经过校准的评审模型标注,不能让被测模型自行填写这些字段后直接拿来算分。改变 k 或上下文后,也必须重新审核支持关系。
5. 从分数回到具体改动#
得到分数以后,先看失败切片,再决定改哪一层。下面是可检验的工程假设,效果需要在自己的数据上测量:
| 观察到的失败 | 优先尝试 | 保持不变的条件 |
|---|---|---|
| 型号、错误码等精确词漏检 | 词法检索与向量检索组合 | 相同问题集、生成模型和最终上下文预算 |
| 片段找到了,但主语或版本丢了 | 保留标题、章节、版本等上下文信息 | 相同评测标注口径和生成提示词 |
| 正确证据在候选中,却掉出最终上下文 | 检查重排与截断位置 | 相同候选集合与最终 k |
| 证据齐全,答案仍遗漏条件 | 调整回答约束和参考事实检查 | 相同检索结果 |
| 无答案时仍给确定结论 | 增加不足证据示例和拒答规则 | 同时检查可回答题的误拒答 |
例如,Anthropic 的 Contextual Retrieval 文章讨论了在切块中补充上下文、结合 BM25 与语义检索、再进行重排的方法。这些机制适合提出实验假设,但文章中报告的提升对应其测试设置,不能直接当作自己的预期收益。Anthropic:Contextual Retrieval
也不要简单地把 k 调得越来越大。更多片段会改变噪声比例、输入成本和生成延迟。《Lost in the Middle》在其研究设置中观察到模型表现会受到关键信息位置影响;这提供了检查上下文排序的理由,但不能据此断言所有新模型都存在相同程度的问题。论文:Lost in the Middle
做成对比较时,固定知识快照、问题集、模型版本和提示词,每次先改一个关键变量;保存逐题差异,而不是只展示平均分。接入有随机性的生成系统后,还应重复运行并报告波动。四道教学题只能用来理解口径,不能用来做统计显著性或生产质量结论。
用于反复调参的开发集、保护既有行为的回归集,以及最后检查泛化的留出评测集,应分别管理。不要一边看留出题的答案一边改提示词;在同一批题上反复优化后得到的高分,也不能直接解释为对新问题的泛化能力。
6. 把计分器接到真正的发布检查里#
上线前的检查可以分成三层,以下是工程建议,并非统一标准:
- 确定性回归:ID 和数据格式正确,文档版本过滤生效,引用不越出上下文,已修复的失败案例没有复发
- 语义审核:事实是否受支持、是否答全、是否用了适用版本;先用人工样本校准评审模型,并保留“无法判断”结果
- 业务边界:用户是否有权看到证据,敏感内容是否被排除,文档中的指令是否被当成数据处理,失败后是否能转人工
本文代码只覆盖第一层中的数据一致性,以及对第二层已有审核标签的计分。它没有实现权限控制、提示注入检测或自动事实核验。真实资料也不应为了评测而直接上传到未经授权的模型服务。
Anthropic 在 2026 年的评测工程文章中区分了代码、模型与人工评审,并强调评审模型需要与人工判断校准。把代码能稳定判断的条件交给代码,把需要语义理解的部分交给明确的评分规则,再抽样复核,通常比让一个模型给出总分更容易追查问题。Anthropic:Demystifying evals for AI agents
发布门槛应事先约定:哪些高影响错误一旦出现就阻断,哪些一般问答指标允许多大波动,延迟和单题成本的预算是什么。不要看到候选结果后再调整门槛。平均分上涨也不能抵消越权泄露这一类单独的失败。
本例随仓库提供了回归测试源码,覆盖空分母、重复 ID、缺失题目、无效引用、旧版本证据和“全部拒答”等边界。在网站仓库中运行 npm run test:rag-evaluation,可以检查样例输出与这些边界是否仍然一致。
7. 从自己的第一批失败问题开始#
如果手里已经有一个 RAG Demo,先整理最近真实发生、并且获准用于评测的失败问题:漏检一类、答非所问一类、旧版本一类、无答案仍作答一类。逐题写清需要哪些证据、适用什么条件,再把稳定成功的案例留下来做回归。
这一轮最有价值的结果,不一定是换掉模型,而是能准确说出:“这次退化发生在重排后丢失导出说明;版本选择和拒答没有变化。”有了这个粒度,下一步改动才有明确目标。
继续阅读:
- Agent 演示与实际部署的差距:把评测放回完整系统的可靠性问题
- 自动化系统的人工接管设计:证据不足或风险升高后,流程怎样交回给人
- RAG 主题文章:查看本站这一方向的内容
想继续讨论自己的知识库问题,可以先看 AI 实践路线,了解知识星球的内容方向;公众号入口在关于页。交流时带上脱敏问题、预期证据与失败步骤,比只说“RAG 效果不好”更容易找到原因。

