返回文章列表

RAG 评测实战(二):切块一改,召回率为什么就失真了?

芝士AI吃鱼2026年10月2日20 分钟阅读阅读统计加载中

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

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

上一篇把“检索命中、答案支持、事实覆盖、拒答”分开计分。这一篇往前走一步:原始材料不变,切块变了,怎样让评测仍然拿着同一把尺子? 我们会真正执行切块、词法排序与上下文选择,定位三种失败:证据被边界切断、重叠占掉预算、旧版本抢走位置。

实验边界:文档与问题均为人工编写的合成教学 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

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

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

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

稳定证据映射示意:原文同一版本的证据位于40到90,只找到0到64不完整,再找到64到128后区间并集才完全覆盖证据;重叠不重复加分。
作者原创机制示意。区间数字用于解释覆盖规则,不是下方实验的结果;跨块合并只证明原文信息可用,不保证模型能理解或正确作答。 查看大图 ↗

这一步还有一个容易漏掉的前提:字符坐标必须对应同一份规范化文本。 一边把 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 或以上版本,无需安装第三方包。先把以下五个文件下载到同一个目录,并查看代码:

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和字符预算选择上下文,逐题检查候选损失、选择损失与重复开销。
作者原创实验设计图。版本过滤是单独的对照开关;字符预算只用于本例,不是模型 token 上限或费用测量。 查看大图 ↗

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

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

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

切块保留历史,Top-2保留历史,240 字符仅当前,Top-2仅当前,240 字符
fixed:120,无重叠0.3330.3330.6670.667
overlap:120,重叠600.5000.5000.8330.833
paragraph:段落0.6670.3331.0000.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)21302/2
overlap[0,120) + [60,180)240601/2
paragraph[0,185) + [187,213)21102/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

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

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

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

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

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

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

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

然后冻结一份实验清单:

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

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

Anthropic 的 Contextual Retrieval 讨论了为 chunk 补充来源上下文,并结合词法与语义检索的方案。它为“片段缺少主语或文档背景”提供了一种可测试的改进方向;厂商报告的收益依赖其数据与配置,不能移植成本文的效果数字。Anthropic:Contextual Retrieval

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

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

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

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

系列导航:

想讨论自己的切块失败,可以先看 AI 实践路线,了解知识星球的内容方向;公众号入口在关于页。带上脱敏后的原文范围、最终上下文和丢弃原因,比只贴一个“召回率 80%”更有助于定位问题。

AI 实践

想把本文的问题继续做深一点?

知识星球里会继续整理相关案例、工程约束和问题讨论;具体内容以当前社区页面为准。

AI 实践知识星球加入海报,包含可扫描二维码
芝士AI吃鱼公众号二维码

关注「芝士AI吃鱼」公众号持续更新

在这里,我用「人话」和「漫画」拆解 AI 技术。公众号更新会同步整理到本站,方便继续阅读、检索和订阅。

点击二维码可放大,使用微信扫码关注。

芝士AI吃鱼

持续记录 AI 原理和工程实践

更多文章