召回率低是一个结果指标,不是可操作的问题描述。在动任何分块参数之前,先把失败样本归到下面几类,因为不同类别的修复手段几乎不重叠:
- 正确片段不在候选集里:向量检索返回的 top-k 中完全没有包含答案的 chunk。
- 命中但排序靠后:把 k 放很大能命中,实际送入生成上下文的截断位置里却没有。
- 答案被切断:答案跨两个 chunk 边界,任一片段单独看都不完整,嵌入后的语义也被稀释。
- 语义错配:查询是用户的口语化表达,chunk 是术语密集的文档原文,二者在向量空间距离偏远。
- 过滤条件误伤:时间、权限、文档类型等元数据过滤在向量检索之前就把正确文档排除了。
把「召回差」笼统归因于「嵌入模型不行」,最常见的后果是换了一圈模型,问题依旧。
建立最小可用的检索评估集
没有评估集,任何基于感觉的调参都是在噪声上做梯度下降。需要的规模并不大:从真实查询里挑 30~50 条,人工标注每条查询对应「正确答案所在的最小单元」。
标注粒度要和分块粒度解耦——标到段落级,再通过映射关系推算它在任意一种分块方案下落在哪个 chunk。这样换分块方案时,同一套标注可以反复回归,不用重新标注。
检索日志至少记录四项:原始查询(以及改写后的查询)、检索使用的 top-k、每个候选的 id 与相似度分数及排名、最终进入生成上下文的片段 id。有这四项,上面五类失败基本可以在日志里直接判类。
指标上,Hit Rate@k 用来判断正确片段是否进了候选,MRR 用来判断排名是否可用,端到端答案正确率用来防止「检索指标涨了但答案没变好」。
分块边界比 chunk 长度更影响召回
固定字符数的滑动窗口切分,在结构规整的散文里勉强可用,在带标题、列表、表格、代码块的文档里会产出大量语义残缺的 chunk。一个更稳的切分顺序是:先按文档自身的结构切(标题层级、段落、列表项、表格、代码块);再对超过长度上限的结构单元做二次切分,切点落在句子或子句边界而不是字符位置;短于下限的相邻小块合并,尤其是「标题 + 一句话」这种在向量检索里几乎无法独立命中的组合。
表格和代码块不建议拆开:被切成两半的表格,两半的嵌入向量都不代表原来的语义。确实超长时,宁可单独处理成摘要加原文引用的形式。
另一个成本极低、却常被忽略的改动是给 chunk 补上文语境。单独一段「该参数默认值为 30」,脱离所属小节标题后语义几乎无法定位;在嵌入前把父级标题拼接到 chunk 文本前缀,往往能改善这类片段的命中率。注意这只影响嵌入文本,返回给生成的原文是否要带前缀是另一个决策,需要单独验证。
重叠的真实作用与代价
重叠的唯一目的是防止答案恰好落在两个 chunk 的边界上。它不是越大越好:重叠会线性放大索引体积和嵌入调用量;重叠区会在结果里产生多个高分近重复片段,挤占 top-k 名额,把真正互补的片段挤出去;结构化单元(表格、代码块、独立小节)加重叠通常只有副作用。
常见的经验起点是重叠长度取单块长度的 10%~20%,但这是经验值不是常量,必须在本项目的评估集上验证。更关键的往往不是比例,而是对齐方式:重叠应按句子或段落边界回退,而不是取最后 N 个字符——按字符截断产生的前缀碎片,嵌入后基本是噪声。
一个可执行的判断是:如果评估集里「答案被切断」这类失败占比很低,增大重叠不会带来收益,只会推高成本。
嵌入调用侧的一致性自查
分块看起来合理、日志也正常,仍然召不回,就要看调用链本身。以下几项不需要更换任何基础设施即可自查:
- 两侧是否同模型同版本。跨版本向量混用,距离计算没有可比性。
- 归一化是否一致。查询向量归一化、文档向量没有归一化,这类不一致在结果上常表现为「能排出来但排序近乎随机」。
- 查询侧与文档侧是否需要不同的编码方式。部分嵌入模型对 query 和 passage 使用不同的编码路径或指令前缀,两侧用同一套调用方式时检索效果可能明显低于预期。是否需要区分以所用模型的文档为准,不能想当然。
- 输入是否被静默截断。超长 chunk 在部分调用路径下会被截到模型可处理长度而不报错,被截掉的部分对检索完全不可见。分块长度上限应当从模型侧可处理长度反推,而不是先定一个好看的数字。
- 相似文本是否真的产生相似向量。取几条人工判断为「语义相同」和「语义无关」的样本,直接比较向量距离,这是验证整条嵌入调用链是否接错的最快方式。
按修改代价排序的排查顺序
每一轮调整都在同一套评估集上回归,顺序按代价从低到高:
- 补齐检索日志与标注集,先能区分五类失败。
- 检查模型一致性、归一化、截断这三项调用侧问题。
- 做一次 oracle 测试:把标注出的正确片段直接放进生成上下文,排除「其实是生成端没用好上下文」。
- 用字面检索(关键词或短语匹配)跑同一批查询做对照。字面能召回而向量不能,问题偏向语义表示或 chunk 语义不完整;两者都不能,问题偏向分块或过滤条件。
- 调整分块边界与上下文前缀,重新嵌入。
- 再动重叠比例、chunk 长度、top-k 数量这些连续变量。
- 仍然不足时,再引入混合检索或重排。这属于增加组件而非原地调参,收益与延迟成本要单独评估。
从工程角度看,绝大多数「教程级 RAG 召不回」的场景里,收益最大的改动来自分块边界和调用侧一致性,而不是换嵌入模型。换模型是一次性的大成本动作,应该放在前面几步都做过之后再考虑。
还有两个边界需要盯住:top-k 增大、chunk 变小、重叠变大,都会同时改变召回与噪声,检索指标单调上升不代表端到端答案变好,必须把答案正确率和上下文 token 成本一起看;此外,任何分块方案的调整都会让现有索引失效,需要规划重建窗口,而不是在线上直接替换索引。