跳到主要内容

Chunk、召回、重排,RAG 最容易被忽略的顺序问题

· 阅读需 7 分钟
一介布衣
全栈开发者 / 技术写作者

补档说明:本文属于「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、召回、重排、生成这条链路共同决定的。

我现在更愿意把它理解成一句话:先保证知识单元完整,再保证候选覆盖,再保证排序质量,最后才让模型去组织答案。这个顺序一旦想反,后面几乎一定会多走弯路。