# RAG 评测实战（一）：检索命中了，为什么答案还是错的？

> 把 RAG 拆成证据召回、答案支持度、事实覆盖与拒答四层，用一个可下载、零依赖的合成样例跑通评测，定位漏检、旧版本和有引用却答错的问题。

- 作者：芝士AI吃鱼
- 发布日期：2026-09-30
- 主题：RAG、知识库、AI工程、大模型、评测
- HTML 正文：[https://ai-knowledgepoints.cn/blog/rag-evaluation-01-evidence-to-answer](https://ai-knowledgepoints.cn/blog/rag-evaluation-01-evidence-to-answer)
- Markdown 永久链接：[https://ai-knowledgepoints.cn/blog/rag-evaluation-01-evidence-to-answer/index.html.md](https://ai-knowledgepoints.cn/blog/rag-evaluation-01-evidence-to-answer/index.html.md)

引用本文时，请注明作者与 HTML 正文链接。Markdown 版本用于机器阅读，与 HTML 正文共享同一内容来源。

## 正文

知识库回答底下挂着两条引用，内容读起来也很顺，就算答对了吗？

先看一个问题：“当前日志保留多久，能导出什么格式？”系统找到了“保留 7 天”，于是回答“日志保留 7 天”，还附上正确出处。它没有编造，但漏掉了用户问的导出格式。换一次检索，系统找到旧版的“保留 30 天”，照样能生成一句有出处的错误答案。

这两种问题，靠一个笼统的“回答质量分”很难定位。做 RAG，值得先建立一套能区分**没找到、没答全、用错版本、无依据仍作答**的检查方法，再决定要不要换 Embedding、加重排或接 Agent。

这是「RAG 评测实战」第一篇。读完可以拿走三样东西：一张故障定位图、一套明确分母的指标，以及一个无需 API Key 的可运行计分器。

**实验边界：本文提供的是合成教学样例。文档、两组检索结果、回答与审核标签均为手工构造，没有调用真实检索器或大模型。代码实际执行的是计分和回归检查；这些分数不能证明某个模型、检索策略或生产系统更好。** 资料核对日期为 2026 年 9 月 30 日。

## 1. 把 RAG 拆成能排查的三段

RAG 的基本思路，是在生成时引入检索到的外部信息。原始 RAG 论文讨论了参数化模型与非参数化知识索引的组合；如今工程实现可以有不同的切块、检索和生成方式，不能把某一种框架的调用链当作唯一标准。[原始论文](https://arxiv.org/abs/2005.11401)

对排错而言，可以先保留三段：

1. **检索前**：文档是否完整，版本是否有效，当前用户是否有权读取
2. **检索中**：正确证据是否进入候选，重排后是否仍在最终上下文里
3. **生成后**：回答是否受证据支持，是否覆盖问题，证据不足时是否停止下结论

![RAG 评测流程：固定知识快照和问题集，经检索与上下文选择生成回答；分别检查证据召回、事实覆盖与支持度，以及拒答和权限边界](https://ai-knowledgepoints.cn/images/articles/rag-evaluation-01/evaluation-flow.svg)

*作者设计的排错示意图，表示检查位置，不表示实测性能。虚线表示用同一份人工核验基准检查不同阶段。*

排查时不要只存最终回答。最小记录还应包括问题 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](https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/context_recall/)

### 答案支持度：说出来的事实，有多少能从上下文得到支持？

把回答拆成事实主张，逐条判断上下文是否支持，再算“受支持主张数 ÷ 总主张数”。Ragas 的 Faithfulness 采用这一类思路，衡量的是回答与给定上下文的一致性。[Ragas：Faithfulness](https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/faithfulness/)

注意两个边界：

- 回答只说“保留 7 天”，可能支持度为 100%，但没有回答导出格式
- 回答引用旧版“保留 30 天”，可能忠实于那段旧文本，但不满足“当前版本”的问题

因此，**忠于检索文本、符合当前事实、完整解决问题，需要分别检查。** 检索到了什么不能自动成为正确性的唯一基准。

### 参考事实覆盖：用户需要的信息，究竟答全了多少？

本例额外定义一个简单指标：正确表达的参考事实数 ÷ 应回答的参考事实数。它是本文用于故障定位的指标，不冒充通用行业标准。

第二题需要“7 天”和“JSON”两个事实，只回答前者就得 1/2。答对这两项也不代表整段回答都正确：若又补上一句没有来源的“可无限扩容”，事实覆盖仍为 100%，但答案支持度会下降。两个指标正好互相补充。

### 拒答：不知道时停下来，知道时正常回答

无答案题单独统计正确拒答率；可回答题单独统计误拒答率。若系统把每题都回复成“不知道”，前一个指标可能很好，后一个会直接暴露问题。

这里的拒答是清楚说明“现有材料没有这个信息”，并指出还缺什么证据。真实系统里，有时更合适的动作是追问版本、时间或适用对象；那就应把“澄清问题”单独标注，而不是硬塞进二分类。

此外，引用 ID 存在、且位于最终上下文，只能证明引用的格式和范围合法。**它不能证明被引用的片段支持这句话。** 下面脚本把这项检查命名为 `citationPresenceAndValidity`，没有把它叫作“引用准确率”。

## 4. 跑一次零依赖的合成样例

把[计分脚本 evaluate.mjs](https://ai-knowledgepoints.cn/examples/rag-evaluation/evaluate.mjs)和[样例数据 fixture.json](https://ai-knowledgepoints.cn/examples/rag-evaluation/fixture.json)下载到同一个目录，先查看代码，再用 Node.js 18 或以上版本运行：

```bash
node evaluate.mjs fixture.json 2
```

脚本不联网，不创建索引，不调用模型，也不需要密钥。JSON 文件中包含四道题和两组手工填写的结果，分别名为 `synthetic-baseline` 与 `synthetic-corrected`。

召回计分的核心非常短：

```javascript
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](https://www.anthropic.com/engineering/contextual-retrieval)

也不要简单地把 k 调得越来越大。更多片段会改变噪声比例、输入成本和生成延迟。《Lost in the Middle》在其研究设置中观察到模型表现会受到关键信息位置影响；这提供了检查上下文排序的理由，但不能据此断言所有新模型都存在相同程度的问题。[论文：Lost in the Middle](https://arxiv.org/abs/2307.03172)

做成对比较时，固定知识快照、问题集、模型版本和提示词，每次先改一个关键变量；保存逐题差异，而不是只展示平均分。接入有随机性的生成系统后，还应重复运行并报告波动。四道教学题只能用来理解口径，不能用来做统计显著性或生产质量结论。

用于反复调参的开发集、保护既有行为的回归集，以及最后检查泛化的留出评测集，应分别管理。不要一边看留出题的答案一边改提示词；在同一批题上反复优化后得到的高分，也不能直接解释为对新问题的泛化能力。

## 6. 把计分器接到真正的发布检查里

上线前的检查可以分成三层，以下是工程建议，并非统一标准：

1. **确定性回归**：ID 和数据格式正确，文档版本过滤生效，引用不越出上下文，已修复的失败案例没有复发
2. **语义审核**：事实是否受支持、是否答全、是否用了适用版本；先用人工样本校准评审模型，并保留“无法判断”结果
3. **业务边界**：用户是否有权看到证据，敏感内容是否被排除，文档中的指令是否被当成数据处理，失败后是否能转人工

本文代码只覆盖第一层中的数据一致性，以及对第二层已有审核标签的计分。它没有实现权限控制、提示注入检测或自动事实核验。真实资料也不应为了评测而直接上传到未经授权的模型服务。

Anthropic 在 2026 年的评测工程文章中区分了代码、模型与人工评审，并强调评审模型需要与人工判断校准。把代码能稳定判断的条件交给代码，把需要语义理解的部分交给明确的评分规则，再抽样复核，通常比让一个模型给出总分更容易追查问题。[Anthropic：Demystifying evals for AI agents](https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents)

发布门槛应事先约定：哪些高影响错误一旦出现就阻断，哪些一般问答指标允许多大波动，延迟和单题成本的预算是什么。不要看到候选结果后再调整门槛。平均分上涨也不能抵消越权泄露这一类单独的失败。

本例随仓库提供了[回归测试源码](https://github.com/alg-bug-engineer/self-web-host/blob/main/scripts/tests/test-rag-evaluation.mjs)，覆盖空分母、重复 ID、缺失题目、无效引用、旧版本证据和“全部拒答”等边界。在网站仓库中运行 `npm run test:rag-evaluation`，可以检查样例输出与这些边界是否仍然一致。

## 7. 从自己的第一批失败问题开始

如果手里已经有一个 RAG Demo，先整理最近真实发生、并且获准用于评测的失败问题：漏检一类、答非所问一类、旧版本一类、无答案仍作答一类。逐题写清需要哪些证据、适用什么条件，再把稳定成功的案例留下来做回归。

这一轮最有价值的结果，不一定是换掉模型，而是能准确说出：“这次退化发生在重排后丢失导出说明；版本选择和拒答没有变化。”有了这个粒度，下一步改动才有明确目标。

继续阅读：

- [Agent 演示与实际部署的差距](https://ai-knowledgepoints.cn/blog/agent_demo_gap)：把评测放回完整系统的可靠性问题
- [自动化系统的人工接管设计](https://ai-knowledgepoints.cn/blog/daily-2026-08-11-engineering-human-override-design)：证据不足或风险升高后，流程怎样交回给人
- [RAG 主题文章](https://ai-knowledgepoints.cn/blog?tag=RAG)：查看本站这一方向的内容

想继续讨论自己的知识库问题，可以先看 [AI 实践路线](https://ai-knowledgepoints.cn/planet)，了解知识星球的内容方向；公众号入口在[关于页](https://ai-knowledgepoints.cn/about#wechat)。交流时带上脱敏问题、预期证据与失败步骤，比只说“RAG 效果不好”更容易找到原因。
