重排修不了召回
重排(rerank)是 RAG 里单点收益最大的一步,也是被误解最多的一步。加完之后效果没变化的团队,绝大多数不是重排没用,而是他们期待它解决一个它根本解决不了的问题。
先把这句话放在最前面:重排是精度层,不是召回层。它只能在已经拿到的候选里重新排序,候选里没有的东西,它永远排不出来。
1. 为什么需要两阶段
检索和重排用的是两类不同的模型,区别在"两个文本怎么比较"。
| Bi-encoder(检索用) | Cross-encoder(重排用) | Late-interaction(第三条路) | |
|---|---|---|---|
| 输入方式 | query 和文档各自单独编码 | query 和文档拼在一起送进去 | 各自编码,但文档保留一组 token 级向量 |
| 能否预计算 | 能,文档向量提前存好 | 不能,每一对都要现算 | 能,文档的多向量提前存好 |
| 比较方式 | 两个向量算相似度 | 全交互,词级注意力 | MaxSim:每个 query token 取与文档 token 的最大相似度再求和 |
| 速度 | 毫秒级,跟语料大小基本无关 | 每对一次前向计算 | 慢于 bi-encoder,远快于 cross-encoder |
| 精度 | 低 | 高 | 接近 cross-encoder |
| 适合规模 | 百万级切片 | 几十到几百个候选 | 十万到百万级,取决于多向量的存储预算 |
| 代表 | BGE-M3、E5 系列 | BGE-reranker-v2-m3、Qwen3-Reranker | ColBERT、ColPali / ColQwen |
单看准确率,cross-encoder 明显更高——它能看到 query 和文档之间的词级交互,而 bi-encoder 只能比较两个各自压缩好的向量。但它的代价是每一对都要跑一次完整的前向计算,没法预计算,也就没法用在百万级语料上。
中间那类 late-interaction 值得单独说一句。它把文档存成"每个 token 一个向量"而不是压成一个向量,查询时再算 MaxSim,因此文档侧仍可预计算,能用在比 cross-encoder 大得多的语料上,而精度只比 cross-encoder 低一点。代价是存储:一条切片从 1 个向量变成几十上百个。ColPali / ColQwen 把同样的思路搬到页面图像上,把一页切成 patch 各自编码——这是扫描件和复杂版式 PDF 能被直接检索的原因,详见《多模态内容怎么入库》。
所以标准做法是两段:先用便宜的办法圈出 50 到 100 条候选(见《混合检索与 RRF 融合》),再用贵的办法给这几十条精排。
2. 它修得好什么
四类失败,重排效果最明显。
"看起来像"但不对。 余弦相似度把语义接近的文档排到了第一,但那个文档并不包含答案。cross-encoder 读得更细,能把它压下去。
顺序错了。 正确的文档被召回了,但排在第 8 位。多数系统只把前几条送进上下文,第 8 位等于没有。这是重排最能体现价值的一类——召回指标没变,实际体验变好,因为第一条对了。
否定词。"哪些用户没有订阅"这种查询,向量模型基本处理不了——"订阅"和"没订阅"在语义空间里离得太近。cross-encoder 是逐词比较的,能区分。
长而具体的查询。"2024 年之前购买的欧洲客户年费订阅的退款政策是什么",里面有时间、地区、产品类型三个限定。向量检索会把这些限定平均掉,cross-encoder 能逐个对上。
3. 它修不了什么
召回缺口。 正确答案不在前 50 条候选里,重排再强也排不出来。这种情况应该先去修切片和检索(见《切片决定检索上限》),把 Recall@K 提上去,再谈重排。一个很实用的判断:如果 Recall@50 本身就低于一半,加重排大概率看不到变化。
生成层的问题。 上下文给对了、模型还是自己编,那是提示词和约束的问题(见《System Prompt 写的是约束》),跟排序无关。
语料里根本没有。 用户的知识库里没有这个答案,任何检索技术都变不出来。这种情况要靠《RAG 评测指标怎么选》里的 faithfulness 指标把它识别成"应该拒答",而不是让它硬答。
4. 落地时的四个决定
4.1 候选取多少,留几条
常见配置是候选 50 到 100,重排后留 5 到 10 条。两个数字都可以调,但方向不一样:
- 候选数决定上限。太小会把正确答案挡在门外。经验是先用 50,跑评测看 Recall@50 是不是饱和了,没饱和就往上加。
- 保留数决定上下文预算。留太多会触发 lost-in-the-middle(信息放在长上下文中间会被忽略),也会直接推高 token 成本。留 5 条通常比留 20 条效果好。
条数只是"控制进上下文内容"的一半,另一半是内容本身:重排之后再做一次 MMR(最大边际相关性)去重,把意思重复的切片剔掉;或者做抽取式压缩,只保留切片里与问题相关的句子。
这两件事解决的是不同的问题:留几条决定上下文的长度,MMR 与压缩决定同样长度里的信息密度。候选池里经常有七八条在说同一件事(尤其是父子块或带重叠的切片),不去重的话,留 5 条可能只等于留了 2 条的信息量。
4.2 用托管的还是自己部署
托管(Cohere Rerank、Voyage rerank 等):零运维,按调用付费,适合先跑起来验证收益。
自部署:数据不出内网,量大了之后成本低,代价是要管 GPU 和推理服务。选型时先看许可再看指标——BGE-reranker-v2-m3 与 Qwen3-Reranker(0.6B / 4B / 8B)是 Apache-2.0,商用没有问题;Jina Reranker v2 系列是 CC-BY-NC-4.0,只限非商业使用,商业场景要么换模型、要么走 Elastic 的商业 SKU。"能下载权重"和"能商用"是两件事。
选择上有个常见误区是"开源模型一定更便宜"。量小的时候 GPU 的固定成本远高于按次付费;只有调用量稳定且足够大时,自部署才开始划算。
4.3 延迟怎么估
重排是在线链路上最贵的一步。延迟大致等于"候选条数 × 单对打分时间",具体数值取决于模型大小、硬件、以及有没有做批处理——这类数字在不同环境里能差一个数量级,只能自己实测,不要照抄公开数据。可以拿来当参照系的是 Jina 官方公布的数字:100 篇 256 token 的文档约 156ms,同批 4096 token 约 3.5s——长文档场景下重排会从"便宜的一步"变成"最贵的一步"。
这里还藏着一个容易踩的约束:reranker 的上下文上限直接决定它能看多长的切片。Jina reranker v2 只有 1024 token,超出会自动分块再取 max-pool;Cohere v3 系是 4096,v4.0 到了 32K。切片长得离谱时要先确认这个上限,否则打分时模型只看到了切片的一部分,分数含义已经变了。
重排结果也可以缓存:同样的 query 加同样的候选集,分数不会变,重复查询直接查表——这一点和《语义缓存的命中边界》是一个思路,只是缓存的是"打过分"而不是"生成结果"。
能确定的是三条优化方向:
- 批处理。 cross-encoder 是 transformer,批量推理的吞吐远高于逐条跑。不要 for 循环里一条条 predict。
- 控制候选数。 从 100 降到 30,延迟线性下降。关键是先用评测确认 30 条够不够,别为了快砍掉召回。
- 异步化闲时计算。 对能预测的高频查询预热,或者把重排放在流式响应的第一段之前完成,让用户先看到别的反馈。
4.4 分数不要跨阶段混用
bi-encoder 的相似度分数和 cross-encoder 的相关性分数处在两个完全不同的空间,把它们加权相加是错的。
正确做法是把第一阶段纯粹当作筛选器:它只负责决定"哪些进候选",排序权完全交给第二阶段。这也正是《混合检索与 RRF 融合》里 RRF 只取排名不取分数的同一个道理——不同来源的分数不可比,就别逼它们可比。
5. 什么时候可以先不加
- 语料很小(几千条切片以内),bi-encoder 本身就能排得不错,重排的边际收益覆盖不了延迟成本
- 查询模式非常简单,基本是关键词直查
- 还没建评测集——这种情况下加了也不知道有没有用,等于盲改
除了这几种,重排基本都值得加。它在整条链路里的位置见《RAG 链路与失败模式》:排在检索之后、组装之前,是"把候选收窄成上下文"的那一步。