Skip to content

Rerank 二阶段排序:召回之后必须精排 ​

1. 本节产出 ​

接入 Rerank 模型做二阶段排序:粗排召回 20 个候选 → 精排取 Top-5,并用评测集证明答案正确率的提升,同时给出延迟与成本的权衡结论。

2. 前置依赖 ​

3. 为什么召回之后必须精排 ​

看一组真实数据(同一评测集):

方案召回率@5答案正确率
纯向量 top568%55%
混合检索 top585%63%
混合 top20 + Rerank → 585%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: 800

5.2 Rerank 客户端 ​

java
// src/main/java/com/example/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
// src/main/java/com/example/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. 生产避坑 ​

  1. Rerank 服务的可用性必须纳入降级设计。它是链路上的新增依赖,挂了不能让整个问答挂掉。做法:超时设 800ms、失败降级为粗排结果、连续失败时自动熔断并告警(熔断见 03B-09)。
  2. 候选数不是越多越好。20 个候选精排到 5 个是常见配置;送到 50 个时延迟会显著上升而精度提升很小。用评测集找拐点,通常 15~30 之间是甜点。
  3. 不要把 Rerank 后的分数当成相似度阈值用。两种分数含义不同、量纲不同,混用会导致拒答逻辑失效(比如精排分数普遍很低,按 0.6 阈值会全部拒答)。拒答判定要用精排后的 Top-1 分数单独标定,见 02-11。

8. 延伸与锚点 ​

  • 思考题:Rerank 已经把最相关的排到第一了,但如果这个「最相关」其实也不相关呢?(答案在下一课时:拒答机制)
  • 代码锚点:git checkout ch02-10-rerank
  • 下一课时:02-11 引用溯源与拒答
  • 对应课件:L02-10 Rerank 二阶段排序