为什么你需要重排序模型
向量检索能快速把几百万条缩到几十条,却把最后这几个排得很糟。加一道把查询和文档放在一起读的工序就能修好。
对一个已经跑起来的 检索增强生成(RAG) 系统,加一个重排序模型通常是同等投入下能拿到的最大质量提升。
原因在于向量检索计算相似度的方式有一个结构性局限。
双编码器的问题
在向量检索里,文档是提前、独立地生成 embedding 的。每个文本块变成向量时,完全不知道将来会被问什么。查询时,问题单独生成 embedding,再拿来做比较。
这就是双编码器:两次互不相遇的编码。它让这套方法快——文档向量算一次,每个查询都能复用——同时也正是它的天花板。
因为每个文档在问题出现之前就被压进了一个固定向量,这个向量得同等服务于所有可能的问题。针对这一个查询的细微之处,在索引时就被平均掉了。比较发生在两个摘要之间,从来不是两段真实文本之间。
再加上高维距离压缩的问题——最近邻和最远邻之间的差别比直觉以为的要小——结论就很一致了:向量检索擅长把几百万缩到五十,却不擅长从这五十里挑出最好的五个。
交叉编码器哪里不一样
重排序模型把查询和一篇文档一起当作单个输入,输出一个相关性分数。
因为两段文本出现在同一次前向传播里,注意力(attention)可以横跨两者运作。模型能注意到:这份文档的第三段直接回答了问题里的那个具体条件;某个词出现时的含义和问题想问的不一样;文档讲的主题对,但版本不对。
事先没有经过压缩,没有任何东西被平均掉。这就是交叉编码器排序明显更好的原因。
代价是什么都无法预先计算。每一对「查询-文档」都需要自己的一次前向传播。用这种方式给一百万篇文档打分不可能——这正是这两个阶段互补而非竞争的原因。
两阶段模式
100 万篇文档
→ 向量检索(快、近似) → top 50
→ cross-encoder 重排序(慢、准确) → top 5
→ 交给模型检索优化的是召回率(recall):把正确的文档弄进候选集。重排序优化的是精确率(precision):把这个集合排对。
实用的做法:召回 30-100 个候选,重排序压到 3-10 个。给 50 对做重排序是笔不大且有界的开销——只是随后生成成本的一小部分。
为什么它比听起来更重要
有两个机制上的原因,让最终排序真的有分量。
召回的文本块会填满上下文窗口,而注意力在长上下文里的分布并不均匀。开头和结尾的内容,比中间的内容被更可靠地使用。所以文本块的顺序影响的是模型真正用了什么,而不只是它收到了什么。
而且无关的文本块并非中立。它们消耗预算、稀释注意力,偶尔还会被用上——产出一个自信的错误答案,来源只是一段看起来相关的文字。少而精的文本块往往胜过堆更多的数量。
可选项
托管的重排序 API。 有几家厂商提供专门的重排序接口。最省事的路子,按打分的文档数计费。
开源的交叉编码器模型。 为相关性打分训练的小模型,可以本地跑。在普通硬件上,面对常见的候选集规模已经够快,而且没有按查询计费的成本。
用 LLM 做重排序。 让一个通用模型给候选打分或排序。灵活,不需要额外基础设施,但比专门的重排序模型更贵也更慢。量小或者做原型时合理。
什么时候可以跳过
语料非常小。 只有几百个文本块时,检索本来就是在很窄的集合里挑。
延迟预算紧。 重排序会多一个来回。如果连每 50 ms 都要抠,那它可能放不进去。
瓶颈不在检索。 先诊断。如果正确的文本块压根不在召回的候选里,重排序帮不上忙——那是分块或混合检索的问题。重排序只能重排检索找到的东西。
这个诊断顺序很重要,却常被跳过:先查正确的文本块有没有被召回,再去改排序。
要点回顾
- 向量检索在知道问题之前就把文档做成 embedding 了,所以针对查询的细微之处已被平均掉。
- 交叉编码器把查询和文档放在一起处理,让注意力横跨两者运作——排序好得多,代价是无法预先计算。
- 标准模式:召回 30-100 个保召回率,重排序压到 3-10 个保精确率。
- 顺序要紧,因为注意力在长上下文里分布不均,而无关的文本块会主动造成伤害。
- 检索从没捞出来的文本块,重排序救不回来——先核实检索。
下一篇: RAG 还是微调?