Chunk、召回、重排,RAG 最容易被忽略的顺序问题
补档说明:本文属于「AI 工程落地周记」系列,计划发布时间为 2025-02-07 10:20。当前先保留为草稿,后续补充真实案例、代码片段和复盘细节后再发布。
很多团队在做 RAG 优化时,容易把问题切成几个独立模块来看:Chunk 怎么切、检索怎么召回、重排怎么加、最后模型怎么答。表面上看这很合理,因为技术栈确实也是这么拆开的。但真正调过一轮系统之后就会发现,这几个环节并不是并列关系,它们是串联关系,而且前一个环节的决策会强烈限制后一个环节的上限。
也就是说,很多 RAG 项目效果不好,不是某一个组件单独弱,而是顺序没想清楚:一开始切分就把信息结构破坏了,后面再怎么改召回和重排,都只能在一堆不完整片段里做“最优选择”。
所以我现在更在意的是这条链路的顺序:先怎么切,再怎么召,再怎么排,最后才轮到模型组织答案。
第一步:Chunk 决定了你到底在检索什么
很多人做 RAG 时,第一步就是“按固定长度切块”。这种方法不是不能用,但它特别容易在真实文档里出问题。
原因很简单,文档并不是平均分布的信息块。它通常有:
- 标题和子标题
- 说明段和示例段
- 表格和附注
- 前置条件和结论
如果只是机械切分,常见后果就是:
- 关键结论被切走一半
- 解释和例外条款分到不同块
- 标题离内容太远,语义失真
- 同一块里混入多个主题
这会直接导致后面的召回拿到“看似相关、其实信息不完整”的内容。
所以我现在更倾向把 chunk 看成“知识单元设计”,而不是“文本切片”。一个好的 chunk 应该尽量满足两件事:
- 自己单独拿出来时,语义仍然完整。
- 放进检索结果里时,能看出它属于哪个主题和上下文。
如果这一步没做好,后面所有优化都会带着先天缺陷。
第二步:召回解决的是“先别漏”
一旦 chunk 设计好,下一步才轮到召回。召回的核心目标不是“立刻最精准”,而是“先把可能相关的都带进候选集合里”。
这是一个很容易被误解的点。很多团队会直接盯着最终回答质量,然后说“召回不行”。实际上召回本身该解决的是两个问题:
- 该出现的内容有没有出现。
- 不该出现的噪音是不是太多。
如果你要求召回阶段同时做到“完全精确”,往往会把阈值调得太紧,结果真正关键的信息反而被漏掉。
所以我通常会把召回看成“保守阶段”:
- 先多拿一点候选。
- 再在后面做筛选。
这也是为什么我不太喜欢在召回阶段就做太多激进裁剪,因为一旦错过,后面重排再聪明也救不回来。
第三步:重排解决的是“谁更值得进上下文”
很多人把重排理解成“可选增强”,但在文档复杂一点的系统里,它经常是效果稳定的关键。
原因在于召回往往会拿到一批“都还算相关”的片段,但用户真正需要的,通常只有其中两三段。这时重排的价值就体现出来了:
- 把结论性内容排前面
- 把只提到关键词但不回答问题的片段往后放
- 把上下文最完整的内容优先保留
如果没有重排,模型很容易在多个半相关片段里自己“做理解”,这时你会误以为是模型幻觉,实际上问题是上下文排序本来就不合理。
所以在我看来:
- Chunk 决定信息单元质量
- 召回决定候选集合覆盖率
- 重排决定最终上下文质量
这三步顺序不能反。
最容易被忽略的错误顺序
我见过几种很典型的错误做法。
错误一:回答不准就先换模型
如果上游 chunk 和召回都不稳,换更强模型通常只能把错误答案说得更像正确答案。
错误二:先加重排,后补切分
如果原始 chunk 本身已经破碎,重排也只能在一堆碎片中挑最像答案的碎片,它并不能修复信息被切坏的问题。
错误三:召回阈值越严格越好
阈值过紧会造成漏召回。很多时候系统看起来“更干净”了,但实际是把真正重要的片段也一起过滤掉了。
错误四:把每一步单独调到最好
RAG 是链路系统,不是单点竞赛。某一步单独最优,不代表整体最优。比如 chunk 切得特别细,可能有利于局部召回分数,但对最终答案完整性反而不友好。
我常用的一套排查顺序
如果现在让我接一个效果不稳定的 RAG 系统,我大概率会按这个顺序查:
1. 先看 chunk
- 是否按语义边界切分
- 是否保留了标题、章节、来源等元数据
- 是否存在明显的信息断裂
2. 再看召回
- 关键问题下,正确片段有没有出现在 top-k
- 错误片段为什么会被命中
- 同义问法的召回结果是否一致
3. 再看重排
- 候选结果里哪些应该排前但没有排前
- 是否把解释性片段压过了结论性片段
- 是否因为重排策略过强而损失了覆盖面
4. 最后看生成
- 模型是否真正误读了上下文
- Prompt 是否要求过多或过少
- 最终输出是否受格式约束影响
这种顺序最大的好处是,能尽量把“知识链路问题”和“模型生成问题”拆开。
一个简单的理解方式
我会把 RAG 类比成一条生产线:
- Chunk 是原材料切割
- 召回是把相关原料搬到操作台
- 重排是把最有用的原料放到最顺手的位置
- 生成才是最后加工
如果第一步就把原料切坏了,后面再怎么摆放、再怎么加工,成品也不会稳定。
一个最小示例
const chunks = splitByHeadingAndParagraph(documents)
const candidates = retrieveTopK(query, chunks, 12)
const ranked = rerank(query, candidates).slice(0, 4)
const answer = await generate({
query,
context: ranked,
system: '只能根据上下文回答,无法确认时明确说明。'
})
这段代码看起来普通,但它表达了一个很关键的原则:生成在最后,前面的每一步都是为了把“正确而完整的上下文”送到模型面前。
我现在更在意的不是参数,而是链路完整性
很多优化文章喜欢直接讨论:
- top-k 设多少
- chunk 大小设多少
- 重排模型选哪个
这些参数当然重要,但真正决定系统稳定性的,往往不是单个参数,而是链路有没有按正确顺序设计。
只要顺序对了,很多参数都能在可控范围内慢慢调;顺序错了,参数再精细,系统也容易陷入局部修补。
总结
RAG 的效果不是某一个模块单独决定的,而是 Chunk、召回、重排、生成这条链路共同决定的。
我现在更愿意把它理解成一句话:先保证知识单元完整,再保证候选覆盖,再保证排序质量,最后才让模型去组织答案。这个顺序一旦想反,后面几乎一定会多走弯路。
