# RAG 评测实战（二）：切块一改，召回率为什么就失真了？

> 固定原文、版本与证据范围，实际运行零依赖词法检索，对照固定切块、重叠切块和段落切块，拆开 Top-k、上下文预算与旧版本造成的失败。附完整代码、数据、测试与逐题结果。

- 作者：芝士AI吃鱼
- 发布日期：2026-10-02
- 主题：RAG、知识库、AI工程、检索评测、文档切块
- HTML 正文：[https://ai-knowledgepoints.cn/blog/rag-evaluation-02-chunking-evidence-mapping](https://ai-knowledgepoints.cn/blog/rag-evaluation-02-chunking-evidence-mapping)
- Markdown 永久链接：[https://ai-knowledgepoints.cn/blog/rag-evaluation-02-chunking-evidence-mapping/index.html.md](https://ai-knowledgepoints.cn/blog/rag-evaluation-02-chunking-evidence-mapping/index.html.md)

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

## 正文

把 chunk 从 120 个字符改成 240 个字符，召回率变高了。接着加上重叠窗口，分数又变了。到这一步，你能说检索真的进步了吗？

先别急着选参数。可能正确证据确实更容易找到了，也可能两个实验的“正确 chunk”已经不是同一套东西；还有一种更隐蔽的情况：同样取前两块，大块方案其实给了模型更多文字。

[上一篇](https://ai-knowledgepoints.cn/blog/rag-evaluation-01-evidence-to-answer)把“检索命中、答案支持、事实覆盖、拒答”分开计分。这一篇往前走一步：**原始材料不变，切块变了，怎样让评测仍然拿着同一把尺子？** 我们会真正执行切块、词法排序与上下文选择，定位三种失败：证据被边界切断、重叠占掉预算、旧版本抢走位置。

**实验边界：文档与问题均为人工编写的合成教学 fixture；切块、词法检索、预算选择与计分由随文代码实际运行。没有调用 Embedding、BM25、重排模型或生成模型，没有真实用户数据。这是机制实验，不是生产 RAG 或大模型 benchmark。** 文中结果只对应下载文件的固定设置。资料核对日期：2026 年 10 月 2 日。

## 1. 先别让答案跟着 chunk ID 一起搬家

上一篇的教学计分器按必需证据 ID 计算召回。只要两组检索共享同一批稳定证据，那个口径足够清楚。但如果 ID 直接来自切块序号，比较切块策略就会出现麻烦。

假设一个原始文档中，真正需要的证据位于 `[40, 90)`。旧方案把它放进 `chunk-0`，新方案把它拆进 `chunk-0` 与 `chunk-1`。此时继续要求“命中 chunk-0”，可能把只找到前半句算作成功；若给新切块分配全新 ID，再直接与旧 ID 比较，又可能把正确结果算作漏检。

Ragas 的 ID-based Context Recall 明确按参考 ID 与检索 ID 的交集计分。这种实现不会替我们判断两套 ID 是否还表示同一个证据对象。更换切块时，要先解决对象对齐，再使用相应指标。[Ragas v0.4.3：Context Recall](https://docs.ragas.io/en/v0.4.3/concepts/metrics/available_metrics/context_recall/)

本文把标注锚定到三个层次：

- **原始文档**：稳定的文档 ID，表示是哪份材料
- **不可变版本**：明确的版本标识，区分当前说明与历史说明
- **证据范围**：该版本原文中的半开区间 `[start, end)`，包括起点、不包括终点

切块只是从这些原文范围派生出来的视图。无论 chunk ID 怎么变化，都能回到原文坐标重新映射。

![稳定证据映射示意：原文同一版本的证据位于40到90，只找到0到64不完整，再找到64到128后区间并集才完全覆盖证据；重叠不重复加分。](https://ai-knowledgepoints.cn/images/articles/rag-evaluation-02/evidence-mapping.svg)

作者原创机制示意。区间数字用于解释覆盖规则，不是下方实验的结果；跨块合并只证明原文信息可用，不保证模型能理解或正确作答。

[查看竖版大图](https://ai-knowledgepoints.cn/images/articles/rag-evaluation-02/evidence-mapping-mobile.svg)

这一步还有一个容易漏掉的前提：**字符坐标必须对应同一份规范化文本。** 一边把 Windows 换行转换成单个换行，一边保留原始换行，后面的 offset 就可能全部错位。PDF 提取器升级、表格转文本方式变化、Unicode 规范化也可能产生同样问题。

因此，工程记录不能只存“文档叫使用手册”。应同时保存原始来源、解析器版本、规范化规则与文本摘要校验值；重新解析后重新验证证据文本。本例刻意使用 ASCII 英文原文，避免把中文分词、UTF-16 码元与 token 计数混进同一个教学实验。真实中文资料应明确 offset 单位，并用所选模型的 tokenizer 计算输入预算。

## 2. 一条事实可以有多个出处，但不能靠重复加分

如果“日志保留 7 天”同时出现在主文档和 FAQ，找到任何一个有效出处都应该算覆盖。反过来，如果答案必须同时满足“等待 30 秒”和“仅对幂等请求生效”，只找到其中一半就不够。

本例的标注把每道题拆成必需事实，每个事实有若干备选支持组：

1. 不同支持组之间是 **OR**：任意一个完整组成立，就覆盖该事实
2. 同一组内部的证据范围是 **AND**：所有范围都必须被覆盖
3. 每个范围只接受同一文档、同一版本的上下文区间
4. 多个选中 chunk 可以联合覆盖一个范围；先合并区间，再判断是否有缺口
5. 同一事实最多得一次分。两块重复包含同一句话，不会得到两份奖励

本文定义的“原文证据事实覆盖率”是：**被完整支持的必需事实数 ÷ 必需事实总数**。先逐题计算，再对可回答题做宏平均。它与上一篇“生成答案实际答出了哪些事实”处在不同阶段：这里没有生成答案，只问最终上下文有没有提供标注要求的原文信息。

举个最小边界：目标是 `[40, 90)`，检索区间是 `[0, 64)` 和 `[65, 128)`。虽然只缺一个字符，严格覆盖规则仍然判失败。这个规则便于稳定回归，但它可能比人类阅读更苛刻，所以标注时要尽量选完整的、最小充分的证据，避免把无关空格或整页都标进去。

另一方面，范围覆盖也可能高估真正可用性。切块顺序打乱、引用关系丢失，或者“它”“上述条件”的指代不在标注范围内，模型仍可能答错。**标注是否充分需要人工检查，计分器只执行已经约定的规则。** 不能让被测系统自己选择对它最有利的证据范围。

## 3. 把切块、排序和预算分开控制

完整实验只依赖 Node.js 18 或以上版本，无需安装第三方包。先把以下五个文件下载到同一个目录，并查看代码：

- [evaluate.mjs：切块、检索与计分](https://ai-knowledgepoints.cn/examples/rag-evaluation-02/evaluate.mjs)
- [fixture.json：固定原文、问题与证据标注](https://ai-knowledgepoints.cn/examples/rag-evaluation-02/fixture.json)
- [test.mjs：可独立运行的边界测试](https://ai-knowledgepoints.cn/examples/rag-evaluation-02/test.mjs)
- [results.json：本文运行的完整结果](https://ai-knowledgepoints.cn/examples/rag-evaluation-02/results.json)
- [README.md：运行说明与实验边界](https://ai-knowledgepoints.cn/examples/rag-evaluation-02/README.md)

```bash
node evaluate.mjs fixture.json > actual-results.json
node test.mjs
```

脚本不联网、不下载模型、不读取凭证。第一条命令实际从原文构建 chunk，再对问题执行检索，不是读取一份事先填好的排行榜。第二条命令检查确定性和失败边界。仓库内也可运行 `npm run test:rag-chunking`。

对照设置如下：

| 变量 | 本文固定设置 |
| --- | --- |
| 原文与问题 | 同一份人工合成 fixture；3 道可回答题，1 道无证据题 |
| 固定切块 | 每块最多 120 个 ASCII 字符，步长 120 |
| 重叠切块 | 每块最多 120 个 ASCII 字符，重叠 60，步长 60 |
| 段落切块 | 按原文段落边界切分，保留原文坐标 |
| 排序 | 问题不同词项与 chunk 词项的交集数量；并列使用固定规则 |
| 上下文选择 | 分别测 Top-2，以及总计不超过 240 个字符的整块选择 |
| 版本条件 | 允许历史版本、只保留当前版本，分别运行 |

这个词法排序器没有逆文档频率、词频饱和或长度归一化，因此**不能称作 BM25**。它也不理解同义词。我们刻意采用简单的确定性函数，让每个排序变化都能追到词项与边界；本例没有资格推导中文向量检索中的最优块大小。

要注意，段落切块是一个不同长度的对照，并没有悄悄强制成 120 字符。它恰好可以暴露固定 Top-k 的问题：两段长文字与两段短文字，数量相同，输入体积不同。

![检索对照设计：固定知识快照、问题、词法排序，只改变切块；分别按Top-k和字符预算选择上下文，逐题检查候选损失、选择损失与重复开销。](https://ai-knowledgepoints.cn/images/articles/rag-evaluation-02/context-budget.svg)

作者原创实验设计图。版本过滤是单独的对照开关；字符预算只用于本例，不是模型 token 上限或费用测量。

[查看竖版大图](https://ai-knowledgepoints.cn/images/articles/rag-evaluation-02/context-budget-mobile.svg)

预算选择也要写清楚。本文按排名顺序扫描候选，当前 chunk 放不下就跳过，继续检查后面的较小块；不截断 chunk，也不做背包优化。重叠部分照常占用输入字符，不先去重后报一个更漂亮的成本。换一种选择策略，结果可能不同，所以选择器本身也是实验配置的一部分。

## 4. 看逐题失败，而不是急着宣布哪种切块胜出

下面是脚本在随附 fixture 上实际运行的宏平均结果，分母始终是 3 道可回答题，四舍五入到三位小数。每种策略各跑两个版本条件和两种选择规则，共 12 组；没有先挑最好的一组再展示。

| 切块 | 保留历史，Top-2 | 保留历史，240 字符 | 仅当前，Top-2 | 仅当前，240 字符 |
| --- | --- | --- | --- | --- |
| fixed：120，无重叠 | 0.333 | 0.333 | 0.667 | 0.667 |
| overlap：120，重叠60 | 0.500 | 0.500 | 0.833 | 0.833 |
| paragraph：段落 | 0.667 | 0.333 | 1.000 | 0.667 |

这张表不能被读成“段落切块最好”。同一段落策略，Top-2 是 1.000，限制到 240 字符后就变成 0.667。先看原因。

### 失败一：条件被切走，关键词还留着

`boundary-condition` 需要 `retry/v2` 的 `[100, 178)`，内容包括 R-17、等待 30 秒，以及“仅当请求幂等”的适用条件。fixture 用点号填充背景，让证据有意跨过 120 字符边界；这些填充并非真实业务语料。

固定切块选中了 50 字符的索引提示 `retry-guide`，以及 `retry/v2` 的 `[0, 120)`，合计 170 字符。提示里有三个查询词，却没有实际规则；另一块只带来证据开头。后续块的词项没有命中查询，因此没有进入正分候选。这里还暴露了字符硬切会截断单词的问题，不能把该现象全部解释成语义上下文不足。

重叠方案得到 `[60, 180)`，完整覆盖 `[100, 178)`，于是该题从 0 变为 1。这个反例只说明这次重叠恰好保护了证据边界。换一个证据位置、分词器或 query，排序和结论都可能改变。

### 失败二：两个位置都被同一条事实占了

`two-facts` 要求“保留 7 天”和“支持 JSON 导出”。仅当前版本、240 字符预算下：

| 方案 | 最终选中的 logs/v2 原文区间 | 输入字符 | 重复原文字符 | 覆盖事实 |
| --- | --- | --- | --- | --- |
| fixed | `[0,120)` + `[120,213)` | 213 | 0 | 2/2 |
| overlap | `[0,120)` + `[60,180)` | 240 | 60 | 1/2 |
| paragraph | `[0,185)` + `[187,213)` | 211 | 0 | 2/2 |

重叠方案的两个块都包含保留期限。导出说明在 `[187,213)`，对应候选确实存在，但排在后面，剩余预算已经装不下了。两个高相关结果并没有给出两个不同事实。

重复字符按同文档、同版本区间并集计算：240 个输入字符只覆盖 180 个不同原文位置，重复了 60 个。不同文档写了相同文字不在这个重复口径内；实际输入还可能有引用标签和分隔符，本例没有把这些包装开销算进去。

### 失败三：Top-k 满分来自更大的输入

`boundary-condition` 的完整段落长 243 字符，加上 50 字符提示，Top-2 实际输入 293 字符，证据覆盖为 1。换成 240 字符预算，243 字符的整段被跳过，只剩 50 字符提示，覆盖变成 0。

不是计分器忽然不喜欢段落了，而是限制条件变了。更合适的下一轮对照可以测试“过长段落二次切分”或“按句子装配”，同时固定真实 token 预算；不能把不受字数限制的 Top-2 与受限方案直接比较效率。

### 独立对照：旧版排得更高，仍然算错

`current-version` 查询使用 `current logs retention policy`。旧版重复这组词，排在当前有效证据之前。三种切块在不做版本过滤时，该题覆盖都是 0；只允许 fixture 声明的当前版本后，都变为 1。这个变化来自版本资格过滤，不应归功于切块算法。

第四题 `no-evidence` 没有任何必需证据，覆盖率为 `null`，不进入宏平均。部分设置仍返回带 `archive` 的片段，这只说明词法相关并不等于有答案。因为没有生成模型，本文不计算拒答率，也不把检索为空当成正确拒答。

完整结果会保留每个候选的词项得分、原文坐标、选中片段、丢弃原因、已覆盖事实、输入字符与重复字符。调试时先看某题的 `ranked`、`selected`、`skipped`，再看汇总。随附测试既锁定上述失败，也覆盖相邻区间、单字符缺口、不同版本不可拼接、备选证据组、非法参数和命令行结果一致性。

## 5. 为什么“原文还在”不代表“证据还在上下文里”

排查时，至少保留三个检查点：

- **索引覆盖**：所有切块的并集还能覆盖标注证据吗？解析阶段是否直接丢了表格、脚注或范围末尾？
- **候选覆盖**：排序器返回的候选是否覆盖证据？如果缺失，查分词、关键词、Embedding 或召回路径
- **最终上下文覆盖**：候选中存在的证据，经过 Top-k、预算选择、版本过滤与重排后还在吗？

只有最终一层是本文主表计分的对象。若把“整个索引中有这个句子”当成召回成功，几乎每种保留原文的切块方式都能拿高分，却没有回答用户问的东西是否真的送进模型。

重叠尤其容易制造错觉。它增加了同一证据被某块完整容纳的机会，同时也增加重复候选。Top-k 是按块占位置，输入窗口是按文本占预算，这两个位置都可能被重复内容消耗。检索去重、邻接合并或父文档扩展可以成为下一轮实验，但必须连同新增上下文体积一起测，不能只比较召回分数。

这也是为什么本文不把“最小 chunk”或“最大 overlap”写成建议参数。对于需要连续操作步骤的问题，过小的块可能把前提切走；对于短错误码查询，过大的块可能让其他主题混在一起。原始材料结构、问题分布和排序器共同影响结果。

HiChunk 的研究把证据密度和切块质量的评测联系起来，提醒我们：一份主要由单点事实组成的问题集，可能暴露不了多段证据任务的失败。这里引用的是研究动机，不代表本文复现了其模型或结论。[HiChunk 论文 v3](https://arxiv.org/abs/2509.11552v3)

## 6. 版本过滤应先于“这个片段看起来很相关”

旧版说明可能包含更多与问题完全相同的词，因此相关性得分更高。“排得靠前”不会自动证明它仍然有效。

本例的当前版本由 fixture 显式声明，过滤发生在排名之前。历史版本仍保留在原始数据中，以便运行对照和检查；过滤没有把文档永久删除。计分时，旧版本即便逐字覆盖类似内容，也不能充当当前版本的证据。

生产系统则比这个开关复杂得多：“当前”可能随租户、产品版本、生效日期、地区或合同而变。一个查询如果没有说明适用版本，正确动作有时是追问，而不是一律选版本号最大的文档。历史问题还需要查历史资料，因此应保存可复现的知识快照和明确的查询约束。

权限是另一条单独的边界。本文版本过滤不实现 ACL，不验证当前用户能否访问文档，也没有模拟提示注入。不能因为这个教学 fixture 通过，就认为多租户知识库已具备安全隔离。真实项目应把无权文档的召回视作独立阻断条件，而不是允许平均召回上涨抵消它。

## 7. 把这个小实验换成你的真实回归集

先找一批获准用于评测的问题，覆盖精确标识、多事实、前提条件、版本冲突和无答案。每个问题保留最小充分证据、备选支持组与适用范围。不要只从现有检索器已能答对的结果反推标注，否则系统的盲区会从评测集里消失。

然后冻结一份实验清单：

```text
知识快照 + 文本摘要 + 解析/规范化版本
问题集 + 标注版本 + 适用版本/权限条件
切块方式 + 长度单位 + size/overlap
检索器版本 + 并列规则 + 候选数量
上下文选择器 + tokenizer + token预算
逐题候选 + 最终上下文 + 丢弃原因
```

接入真正的生成模型后，再回到第一篇：用实际送入模型的上下文审核答案支持度、事实覆盖与拒答。检索证据覆盖提高，是值得继续验证的信号；答案是否受益仍需单独测量。报告延迟时，区分解析建索引、查询检索和生成；报告成本时，用真实 token 与计费条件，本例没有测量这两项。

Anthropic 的 Contextual Retrieval 讨论了为 chunk 补充来源上下文，并结合词法与语义检索的方案。它为“片段缺少主语或文档背景”提供了一种可测试的改进方向；厂商报告的收益依赖其数据与配置，不能移植成本文的效果数字。[Anthropic：Contextual Retrieval](https://www.anthropic.com/engineering/contextual-retrieval)

最后要给评测保留一个没被反复调参污染的留出集。本文的 4 道教学题及参数都是为了展示机制而设计，公开、确定、样本极小；它们适合作为单元测试，不适合做显著性分析或泛化结论。真实系统应报告每类失败的数量、逐题差异与重复运行波动，先约定阻断门槛，再看候选结果。

## 8. 这一次应该带走什么

切块参数改变后，第一件事是让证据锚点仍然指向同一份原文和适用版本；第二件事是检查真实进入上下文的内容；第三件事才是比较分数、体积和后续答案表现。

如果一次改动让召回率升了，你现在应该能够继续问：升的是哪个阶段？用的是同一标注吗？多占了多少输入？哪些题反而退化？旧版和重复块有没有把位置挤掉？这些问题答得清楚，分数才对下一步改动有用。

系列导航：

- [第一篇：检索命中了，为什么答案还是错的？](https://ai-knowledgepoints.cn/blog/rag-evaluation-01-evidence-to-answer)
- 本篇：切块、稳定证据映射与预算对照
- 下一篇计划：固定候选与预算，比较混合检索和重排；完成可复现实验后发布
- [RAG 主题文章](https://ai-knowledgepoints.cn/blog?tag=RAG)

想讨论自己的切块失败，可以先看 [AI 实践路线](https://ai-knowledgepoints.cn/planet)，了解知识星球的内容方向；公众号入口在[关于页](https://ai-knowledgepoints.cn/about#wechat)。带上脱敏后的原文范围、最终上下文和丢弃原因，比只贴一个“召回率 80%”更有助于定位问题。
