Appearance
Rerank 二阶段排序:召回之后必须精排
1. 本节产出
接入 Rerank 模型做二阶段排序:粗排召回 20 个候选 → 精排取 Top-5,并用评测集证明答案正确率的提升,同时给出延迟与成本的权衡结论。
2. 前置依赖
- 02-09 混合检索与 RRF 融合
- 一个支持 Rerank 的模型服务(国内厂商或开源 cross-encoder)
3. 为什么召回之后必须精排
看一组真实数据(同一评测集):
| 方案 | 召回率@5 | 答案正确率 |
|---|---|---|
| 纯向量 top5 | 68% | 55% |
| 混合检索 top5 | 85% | 63% |
| 混合 top20 + Rerank → 5 | 85% | 81% |
注意:召回率没变(都是 85%),但答案正确率从 63% 涨到 81%。
原因:正确答案就在那 20 个候选里,但排在靠后位置,被截断在 top5 之外。Rerank 的作用不是召回更多,而是把对的排到前面。
这也解释了为什么「加大 topK」不是解法:topK 加大后,更多无关片段被塞进上下文,反而干扰模型(这就是 02-08 提到的 Lost in the Middle 效应)。正确做法是「大 K 召回 + 精排截断」。
4. 核心原理
4.1 两阶段架构
查询
│
▼ 阶段一:粗排(Recall)
向量 + BM25 → Top 20~50
特点:快、便宜、用预计算的向量 / 倒排索引
│
▼ 阶段二:精排(Precision)
Rerank 模型 → Top 5
特点:慢、贵、但判断准
│
▼
生成答案为什么不能只用 Rerank:Rerank 要对「查询 + 每个文档」做一次完整的模型前向计算,如果对数百万文档全做一遍,成本和时间都不可接受。
为什么不能只用向量检索:向量是「文档单独编码」的,编码时不知道查询是什么;Rerank 是「查询和文档一起编码」,能看到二者的交互——这就是它更准的根本原因。
4.2 两种 Rerank 机制
| 类型 | 机制 | 速度 | 精度 | 成本 |
|---|---|---|---|---|
| Cross-Encoder | 查询+文档拼接后一起过模型,输出一个分数 | 慢 | 高 | 高 |
| Bi-Encoder 相似度 | 分别编码后算相似度(就是向量检索) | 快 | 中 | 低 |
| LLM-based | 让 LLM 给候选打分/排序 | 最慢 | 高 | 最高 |
Cross-Encoder 是主流选择。LLM-based 排序效果不错但成本太高,一般只用于离线评测,不用于在线链路。
4.3 延迟与成本的权衡
典型数据(20 个候选精排):
| 项 | 数值 |
|---|---|
| Rerank 模型延迟 | 80~300ms |
| 单次成本 | 约 0.001~0.005 元 |
| 相比不精排的额外延迟 | +100~300ms |
| 相比不精排的额外成本 | +10%~20% |
值不值:答案正确率从 63% 到 81%,意味着约三成的错误回答被消除。多数业务场景下,这个收益远超那 20% 的成本。
权衡策略:
| 场景 | 建议 |
|---|---|
| 高精度要求(客服、医疗、法务) | 必做 Rerank |
| 高频低价值查询 | 只对 topK 较小的情况做,或只对语义类查询做 |
| 实时性要求极高 | 缓存 Rerank 结果,或只对低置信度查询触发 |
5. 代码走查
5.1 依赖与配置
yaml
ai:
rerank:
enabled: true
base-url: ${RERANK_BASE_URL}
model: ${RERANK_MODEL}
top-n: 5 # 精排后保留数量
candidates: 20 # 送入精排的候选数
timeout-ms: 8005.2 Rerank 客户端
java
// ch02-rag/src/main/java/com/aitech/rag/retrieval/Reranker.java
@Service
public class Reranker {
private final RerankApi api;
private final RerankProps props;
/** 返回按相关性降序排列的 Top-N */
public List<RetrievalResult> rerank(String query,
List<RetrievalResult> candidates) {
if (!props.enabled() || candidates.size() <= props.topN()) {
return candidates.stream().limit(props.topN()).toList(); // 无需精排
}
List<String> docs = candidates.stream()
.map(RetrievalResult::text)
// 长文本截断,避免超出 Rerank 模型输入上限
.map(t -> t.length() > 2000 ? t.substring(0, 2000) : t)
.toList();
try {
List<Scored> scored = api.rerank(query, docs, props.topN());
return scored.stream()
.map(s -> candidates.get(s.index()).withScore(s.score()))
.toList();
} catch (Exception e) {
// 精排失败不能让整个检索失败——降级为粗排结果
log.warn("rerank 失败,降级为粗排结果", e);
return candidates.stream().limit(props.topN()).toList();
}
}
public record Scored(int index, double score) {}
}两个关键设计:
- 候选数 ≤ topN 时跳过精排(省一次调用);
- 精排失败降级为粗排结果,绝不抛异常。
5.3 接入检索链
java
// ch02-rag/src/main/java/com/aitech/rag/chain/RetrievalPipeline.java
@Service
public class RetrievalPipeline {
public List<RetrievalResult> retrieve(String query, String tenantId,
List<String> acl) {
// 阶段一:粗排,召回候选
List<RetrievalResult> candidates =
hybrid.retrieve(query, tenantId, acl, props.candidates());
// 阶段二:精排
List<RetrievalResult> top = reranker.rerank(query, candidates);
// 记录两阶段排名变化,用于效果分析
log.debug("rerank: {} → {}", ids(candidates), ids(top));
return top;
}
}5.4 用 Spring AI 的 Rerank 能力
java
// 若使用框架提供的 RerankingModel 抽象
@Bean
public RerankingModel rerankingModel(RerankProps props) {
// 国内厂商的 Rerank 端点;不同厂商包路径不同,以实际 starter 为准
return new CohereRerankingModel(...) // 或自研适配
}提示:Rerank 的标准化程度不如 Chat/Embedding,各厂商接口差异较大。建议自己封装一个
RerankApi接口,内部按需适配,业务层只认接口。
6. 跑起来
bash
git checkout ch02-10-rerank
mvn -q test -Dtest=RerankEvaluationTest期望输出:
=== 评测集 87 条 ===
方案 召回率@5 答案正确率 P95延迟 单次成本
混合 top5 0.85 0.63 58ms 0.0041
混合 top20 + Rerank→5 0.85 0.81 312ms 0.0049
提升 — +18pt +254ms +20%| 检查项 | 通过标准 |
|---|---|
| 正确率提升 | ≥ 10 个百分点 |
| 延迟可接受 | P95 < 500ms |
| 降级可用 | 停掉 Rerank 服务,检索仍返回结果 |
| 长文本保护 | 超长候选被截断,不报错 |
| 成本可查 | 日志记录每次精排的候选数与耗时 |
7. 生产避坑
- Rerank 服务的可用性必须纳入降级设计。它是链路上的新增依赖,挂了不能让整个问答挂掉。做法:超时设 800ms、失败降级为粗排结果、连续失败时自动熔断并告警(熔断见 03B-09)。
- 候选数不是越多越好。20 个候选精排到 5 个是常见配置;送到 50 个时延迟会显著上升而精度提升很小。用评测集找拐点,通常 15~30 之间是甜点。
- 不要把 Rerank 后的分数当成相似度阈值用。两种分数含义不同、量纲不同,混用会导致拒答逻辑失效(比如精排分数普遍很低,按 0.6 阈值会全部拒答)。拒答判定要用精排后的 Top-1 分数单独标定,见 02-11。
8. 延伸与锚点
- 思考题:Rerank 已经把最相关的排到第一了,但如果这个「最相关」其实也不相关呢?(答案在下一课时:拒答机制)
- 代码锚点:
git checkout ch02-10-rerank - 下一课时:02-11 引用溯源与拒答
- 对应课件:L02-10 Rerank 二阶段排序