Chico Notes
LLM Wiki / RAG

Reranker 生产实战:Cross-Encoder、候选窗口、批处理、降级与评测

从二阶段检索、Cross-Encoder、Elastic Text Similarity Reranker、长文本截断、动态批处理、超时降级到离线评测,设计可落地的 RAG 重排服务。

持续修订的工程笔记

在很多 RAG 系统里,Reranker 的接入方式非常简单:

先召回 50 个 Chunk
→ 调一个重排模型
→ 取前 5 个交给 LLM

这条链路能跑起来,却没有回答真正影响生产质量的问题:

  • 第一阶段只召回 10 个候选,Reranker 还能提升多少?
  • Cross-Encoder 的分数是不是概率,能不能统一使用 0.5 作为阈值?
  • 标题、章节路径、正文和业务元数据应该怎样拼成模型输入?
  • 文档超过模型上下文后,保留开头、截断末尾是否会错过真正证据?
  • 50 个候选是串行调用还是批量推理?GPU 队列应该等多久?
  • Reranker 超时以后,是返回空结果,还是回退到 RRF?
  • 模型离线 NDCG 提升,为什么线上回答仍然没有改善?
  • 什么时候应该微调 Cross-Encoder,什么时候才值得使用 LLM Listwise Rerank?

Reranker 不是一个“更精确的 Embedding 模型”,而是第一阶段召回之后的候选选择器。它决定有限的 Context Window 最终留给哪些证据,也决定一次检索增加多少延迟、GPU 成本和故障面。

资料快照:本文依据 Elastic、Sentence Transformers 官方文档以及经典重排论文核查,截止日期为 2026-08-05。Elastic 当前主文档、8.x 文档、不同订阅和不同推理服务的能力并不完全一致;模型上下文、分数范围、语言支持和服务参数也各不相同,生产使用前必须按目标版本和模型卡验证。

一句话结论

生产级 Reranker 应遵守下面六条原则:

  1. 第一阶段候选集决定质量上限。 正确证据没有进入 Rerank Window,第二阶段无法凭空创造它。
  2. Cross-Encoder 适合小候选集精排,不适合全库召回。 它需要为每个 Query–Document Pair 重新推理。
  3. 模型分数不是天然可迁移的概率。 阈值必须按模型、语言、领域和 Query Cohort 校准。
  4. 长文本策略是相关性设计,不只是 Token 截断。 标题、命中片段、Parent 和邻接上下文需要明确组合。
  5. Reranker 必须有超时、熔断和回退。 第二阶段失败时,第一阶段 RRF 结果仍然可用。
  6. 评测必须同时看候选 Recall、排序质量、上下文质量、延迟和成本。 只看最终答案无法定位问题。

推荐的基线链路是:

BM25 / Dense / Sparse Recall
→ RRF 融合
→ Chunk / Parent 回填与去重
→ Cross-Encoder Rerank
→ MMR / 业务多样性约束
→ Context Pack
→ LLM

1. Reranker 在完整检索链路中的位置

这条链路中,每一层目标不同:

阶段核心目标更关注什么
Candidate Generation不要漏掉正确证据Recall@K、权限、覆盖率
Fusion合并互补召回信号MRR、NDCG、来源贡献
Rerank判断候选是否真正可回答Answerability、Precision、NDCG
Diversity避免重复证据占满 Context冗余率、来源覆盖
Context Pack在 Token 预算内组织证据Context Precision、引用完整性

Reranker 不能替代前面的 BM25、向量检索和过滤,也不能替代后面的去重、Context Pack 和引用组织。


2. Cross-Encoder 为什么通常比 Bi-Encoder 更准

2.1 Bi-Encoder

Bi-Encoder 分别编码 Query 和 Document:

query_embedding = Encode(query)
doc_embedding   = Encode(document)
score           = Similarity(query_embedding, doc_embedding)

文档向量可以离线预计算,在线只需要计算 Query 向量并执行近邻搜索,因此适合百万、千万甚至更大规模的第一阶段召回。

代价是:Query 和 Document 在编码阶段互相不可见,细粒度词项对应、否定关系、条件组合和数字差异可能被压缩进一个固定向量。

2.2 Cross-Encoder

Cross-Encoder 把 Query 与 Document 作为一个联合输入:

[CLS] Query [SEP] Document [SEP]

Transformer 的注意力可以直接建模:

  • Query 中的实体对应 Document 中的哪一处;
  • 文档是否包含问题要求的完整条件;
  • 否定词、时间、版本和数量是否一致;
  • 内容只是主题相似,还是确实能够回答问题。

但它无法为 Document 预计算一个可复用的最终相关性分数。每次 Query 变化,都要重新计算所有 Query–Document Pair。

2.3 Late Interaction

ColBERT 一类 Late Interaction 模型位于两者之间:

  • 文档 Token 表示可以离线计算;
  • 查询时保留 Token 级交互;
  • 比单向量更细粒度;
  • 通常比完整 Cross-Encoder 更适合较大候选集;
  • 索引体积、实现和服务复杂度更高。

它可以是高级检索或重排方案,但不是所有团队的默认第一步。

2.4 Generative / LLM Reranker

LLM 可以通过自然语言判断候选是否满足复杂条件,也可以一次对多个候选进行 Listwise 排序。它的优势是:

  • 零样本理解复杂约束;
  • 可以输出解释;
  • 可以处理多条件和跨段落推理;
  • 适合低频、高价值或高风险查询。

代价包括:

  • 输入 Token 成本高;
  • 延迟明显高于专用 Cross-Encoder;
  • 输出可能格式错误;
  • 候选顺序可能引入位置偏差;
  • 结果不完全确定;
  • 长候选列表受上下文窗口和 Lost-in-the-Middle 影响。

因此,专用 Cross-Encoder 通常是生产默认基线,LLM Rerank 更适合作为高级通道或离线教师模型。


3. 不要混淆模型架构与排序目标

“Cross-Encoder”描述的是 Query 与 Document 如何进入模型;Pointwise、Pairwise 和 Listwise 描述的是排序任务怎样定义。

排序方式输入输出优点主要代价
PointwiseQuery + 一个 Document单个相关性分数易批处理、易部署不直接比较候选之间的相对关系
PairwiseQuery + Document A + Document BA 与 B 谁更相关学习相对偏好候选多时组合数迅速增长
ListwiseQuery + 一组 Documents整体排列或排序分布能建模候选间关系上下文、延迟、位置偏差和输出解析更复杂

多数生产 Cross-Encoder 服务在线采用 Pointwise Scoring:

(query, doc_1) → score_1
(query, doc_2) → score_2
...
(query, doc_n) → score_n

然后按 Score 降序排列。

训练时则可以使用 Pointwise、Pairwise 或 Listwise Loss。不要因为在线接口返回一个分数,就推断模型一定使用 Pointwise Loss 训练。


4. Candidate Window 决定 Reranker 的质量上限

假设正确证据在第一阶段的排名是 37:

Rerank Top-20
→ 正确证据没有进入窗口
→ 无论 Reranker 多强都无法提升

Rerank Top-50
→ 正确证据进入窗口
→ 第二阶段才有机会把它提升到前 5

因此应先测:

First-stage Recall@20
First-stage Recall@50
First-stage Recall@100

再决定 Rerank Window,而不是先拍脑袋选 20 或 100。

4.1 候选窗口的基本权衡

Window可能的收益主要代价
10–20延迟低,适合 CPU 或高并发容易被第一阶段排序限制
30–50常见生产折中需要批处理和稳定模型服务
50–100更高候选覆盖Pair 数、Token、GPU 时间明显上升
100+适合离线实验或高价值查询在线成本和尾延迟通常较高

这不是通用推荐值。窗口需要按以下维度分组评测:

  • Query 类型;
  • 第一阶段 Retriever;
  • 语料规模;
  • 语言;
  • 文档长度;
  • 模型吞吐;
  • P95 延迟目标;
  • 每次请求成本。

4.2 动态窗口

不同 Query 不一定使用同一窗口:

Exact ID / Error Code
→ BM25 高置信命中
→ Rerank Top-10 或直接跳过

自然语言概念问题
→ Hybrid 候选较宽
→ Rerank Top-50

多条件、高风险问题
→ Rerank Top-80
→ 必要时进入更强模型

动态窗口可以由 Query Classifier、第一阶段置信度、候选分布和业务风险共同决定。

4.3 Oracle Upper Bound

离线评测还应计算:

如果窗口中的相关文档全部被完美排到顶部,理论最高指标是多少?

如果 Rerank Top-50 的 Oracle Recall 已经很低,问题在第一阶段,不应该继续只调 Reranker。


5. Reranker 的输入应该包含什么

最简单的输入是:

Query
+
Chunk.Content

但孤立 Chunk 可能失去标题与章节语义。更常见的生产输入是:

Document Title
Section Breadcrumb / Context Header
Chunk Content
少量稳定业务上下文

例如:

Title: Elasticsearch 一致性设计
Section: 删除、Tombstone 与旧事件复活
Content: index.gc_deletes 只在有限时间内保留删除版本信息……

5.1 推荐单独维护 rerank_text

展示内容、第一阶段检索内容和 Rerank 内容可以不同:

content
→ 原始展示 Chunk

retrieval_text
→ Title + ContextHeader + Content
→ 用于 BM25 与 Embedding

rerank_text
→ 更紧凑的 Title + Section + Evidence Passage
→ 用于 Cross-Encoder

如果不希望增加 ES 字段,也可以在 Hydrator 中确定性生成 rerank_text

要求是:

  • 同一个 Chunk 每次生成结果稳定;
  • 投影版本可追踪;
  • 不混入实时变化但与相关性无关的字段;
  • 输入模板进入 Trace 和评测版本。

5.2 不要把所有 Metadata 都拼进去

下面这些字段通常不应该直接进入语义输入:

  • Worker ID;
  • 内部存储路径;
  • 调试状态;
  • 无意义 UUID;
  • 与 Query 无关的完整 ACL;
  • 大量动态标签;
  • 每次都会变化的时间戳。

它们可能消耗 Token、稀释正文,并让模型学到错误偏好。

权限、租户和状态应该在召回前过滤,而不是靠 Reranker 识别。

5.3 标题重复问题

如果 retrieval_text 已经包含标题,Parent 内容又再次包含标题,直接拼接可能重复:

Title
Title
Section
Title
Content

应在投影阶段规范化,避免重复标题被模型误认为更重要。


6. 长文本与截断不是一个小细节

Cross-Encoder 通常有固定最大输入长度。Sentence Transformers 的 max_length 超出后会截断;具体 Token 上限和截断策略由模型决定。

如果 Query 与 Document 总长度超过限制,最简单的 Head Truncation 会优先保留文档开头:

保留:背景介绍
丢失:正文末尾真正回答问题的限制条件

6.1 常见策略

策略一:在 Chunking 阶段控制长度

让进入 Reranker 的 Passage 本身不超过模型上限。

优点:

  • 实现简单;
  • 与第一阶段 Chunk 一致;
  • 批处理长度更稳定。

风险:

  • Chunk 太小会失去上下文;
  • 正确证据可能跨 Chunk;
  • 标题和 Query 也会占 Token。

策略二:命中 Child,Rerank Child + Parent 摘要

Title
+ Context Header
+ 命中 Child
+ Parent 的短上下文

它比直接把完整 Parent 塞给模型更节省 Token。

策略三:长文档分段打分

把一个长候选拆成多个 Passage:

Document
→ Passage 1
→ Passage 2
→ Passage 3

分别与 Query 计算分数,再聚合:

Max Passage Score
Top-N Average
Softmax Aggregation
Learned Aggregation

基线通常从 Max 开始,因为只要一个片段能够完整回答问题,文档就可能相关。

6.2 Elastic chunk_rescorer

当前 Elastic text_similarity_reranker 支持 chunk_rescorer,可以把长字段拆成片段,只把最佳片段送给重排模型,并控制推理 Token。

POST kb_chunks/_search
{
  "size": 8,
  "retriever": {
    "text_similarity_reranker": {
      "retriever": {
        "rrf": {
          "retrievers": [
            {
              "standard": {
                "query": {
                  "match": {
                    "retrieval_text": "QUERY_TEXT"
                  }
                }
              }
            },
            {
              "knn": {
                "field": "embedding",
                "query_vector": [0.1, 0.2, 0.3],
                "k": 50,
                "num_candidates": 200
              }
            }
          ],
          "rank_window_size": 60
        }
      },
      "field": "rerank_text",
      "inference_id": "my-rerank-endpoint",
      "inference_text": "QUERY_TEXT",
      "rank_window_size": 50,
      "chunk_rescorer": {
        "size": 1
      }
    }
  }
}

这是专家能力。Elastic 官方明确提醒:如果模型本身不是简单截断式处理,额外分块可能轻微降低相关性;显式配置的 Chunk 超过模型 Token 限制时,仍可能被截断并明显伤害质量。

6.3 长度分桶

服务端可以按 Token 长度分桶:

0–128 Tokens
129–256 Tokens
257–512 Tokens

让同一 Batch 内样本长度接近,减少 Padding 浪费,提高 GPU 吞吐。


7. Reranker Score 不是天然概率

很多 Cross-Encoder 输出的是 Logit 或任意连续相关性分数。

Sentence Transformers 官方示例中,MS MARCO Cross-Encoder 可以输出:

8.607
1.133

这不是自动校准后的概率。对 Logit 应用 Sigmoid 可以把数值映射到 0–1,但不会改变排序:

sigmoid(score)
→ 改变尺度
→ 不改变 Rank

7.1 为什么不能复制统一阈值

下面这些变化都会影响分数分布:

  • 模型;
  • 训练 Loss;
  • 激活函数;
  • 语言;
  • Query 长度;
  • 文档领域;
  • 正负样本难度;
  • 输入模板;
  • 截断方式。

因此:

model_A 的 0.5
不等于
model_B 的 0.5

更不能把一个英文 MS MARCO 模型的阈值直接复制到中文企业文档。

7.2 Threshold 的三种用途

用途建议
排序通常不需要先校准,只要分数单调可靠
过滤低相关候选在标注集上校准 min_score
置信度展示需要单独做概率校准和可靠性评估

7.3 Elastic Score 也要看模型

Elastic 的 text_similarity_reranker 会对重排分数做非负转换,但 min_score 的有效范围仍依赖模型。生产中应显式指定 inference_id,并把 Model Key、Score Transform 和阈值版本一起记录。

7.4 推荐记录原始分数与标准化分数

{
  "rerank_model_key": "model-x|revision-3",
  "raw_score": 7.82,
  "normalized_score": 0.9996,
  "threshold_version": "zh-rag-v2",
  "passed_min_score": true
}

不要只留下一个无法追溯来源的 score: 0.91


8. Elasticsearch 内置语义重排

当前 Elastic 提供两种主要语义重排入口:

  • text_similarity_reranker Retriever;
  • ES|QL 的 RERANK 命令。

对于普通 RAG Search API,Retriever Tree 更容易与 BM25、kNN 和 RRF 串联。

8.1 基本 DSL

POST kb_chunks/_search
{
  "size": 8,
  "retriever": {
    "text_similarity_reranker": {
      "retriever": {
        "rrf": {
          "retrievers": [
            {
              "standard": {
                "query": {
                  "multi_match": {
                    "query": "QUERY_TEXT",
                    "fields": [
                      "title^4",
                      "context_header^2",
                      "content"
                    ]
                  }
                }
              }
            },
            {
              "knn": {
                "field": "embedding",
                "query_vector": [0.1, 0.2, 0.3],
                "k": 60,
                "num_candidates": 240
              }
            }
          ],
          "rank_window_size": 60,
          "rank_constant": 60
        }
      },
      "field": "rerank_text",
      "inference_id": "my-rerank-endpoint",
      "inference_text": "QUERY_TEXT",
      "rank_window_size": 50,
      "min_score": 0.1
    }
  }
}

参数含义:

参数作用
retriever产生初始候选的子 Retriever
field与 Query 进行文本相似度比较的字段
inference_idRerank 推理端点
inference_text通常为原始或改写后的 Query
rank_window_size进入第二阶段的候选数量,当前默认值为 10
min_score低于阈值的候选被排除,必须按模型校准
chunk_rescorer长文本分块与 Passage 级重打分

8.2 显式指定 inference_id

当前高层文档会推荐特定预配置端点,Retriever Reference 又定义了省略 inference_id 时的默认端点。为了避免默认模型随环境或版本变化,生产请求更适合显式指定:

inference_id
model revision
language capability
score semantics

8.3 Elastic Rerank 的边界

当前 Elastic Rerank 模型页面将该功能标记为 Technical Preview,并列出英语和上下文长度限制。这个限制只描述该具体模型,不代表所有第三方或自托管 Reranker 都只能处理英语。

因此不要把“Elastic 支持 Rerank”误解为“任何语言、任何长度、任何版本都能直接使用同一端点”。


9. 自托管 Sentence Transformers CrossEncoder

最小 Python 实现:

from __future__ import annotations

from dataclasses import dataclass
from typing import Sequence

import numpy as np
from sentence_transformers import CrossEncoder


@dataclass(frozen=True)
class Candidate:
    candidate_id: str
    text: str
    first_stage_rank: int


@dataclass(frozen=True)
class RankedCandidate:
    candidate_id: str
    score: float
    first_stage_rank: int
    rerank_rank: int


class CrossEncoderReranker:
    def __init__(
        self,
        model_id: str,
        *,
        max_length: int = 512,
        device: str | None = None,
        batch_size: int = 32,
    ) -> None:
        if batch_size <= 0:
            raise ValueError("batch_size must be positive")

        self._model = CrossEncoder(
            model_id,
            max_length=max_length,
            device=device,
        )
        self._batch_size = batch_size

    def rerank(
        self,
        query: str,
        candidates: Sequence[Candidate],
        *,
        top_n: int,
    ) -> list[RankedCandidate]:
        normalized_query = query.strip()
        if not normalized_query or not candidates or top_n <= 0:
            return []

        pairs = [
            (normalized_query, candidate.text)
            for candidate in candidates
        ]

        scores = self._model.predict(
            pairs,
            batch_size=self._batch_size,
            show_progress_bar=False,
            convert_to_numpy=True,
        )
        score_array = np.asarray(scores, dtype=np.float64).reshape(-1)

        if len(score_array) != len(candidates):
            raise RuntimeError("reranker returned an unexpected score count")

        ordered = sorted(
            zip(candidates, score_array, strict=True),
            key=lambda item: (
                -float(item[1]),
                item[0].first_stage_rank,
                item[0].candidate_id,
            ),
        )[:top_n]

        return [
            RankedCandidate(
                candidate_id=candidate.candidate_id,
                score=float(score),
                first_stage_rank=candidate.first_stage_rank,
                rerank_rank=index + 1,
            )
            for index, (candidate, score) in enumerate(ordered)
        ]

9.1 生产实现还需要补什么

  • Token 长度统计与截断标记;
  • 请求级 Deadline;
  • 动态批处理;
  • 并发和队列上限;
  • 模型热加载;
  • GPU OOM 保护;
  • 空候选和超长候选校验;
  • Score 与 Model Revision 记录;
  • 熔断与回退;
  • Metrics 与 Trace;
  • 多模型路由;
  • 灰度和版本回滚。

10. Rerank Service API 契约

推荐让 Search Service 与模型实现解耦:

{
  "request_id": "req_001",
  "model_key": "rerank-zh-v3",
  "query": "为什么旧事件会覆盖新索引",
  "top_n": 8,
  "candidates": [
    {
      "candidate_id": "chunk_001",
      "text": "...",
      "first_stage_rank": 1,
      "first_stage_score": 0.0327,
      "source": [
        "bm25",
        "dense"
      ]
    }
  ]
}

响应:

{
  "request_id": "req_001",
  "model_key": "rerank-zh-v3|2026-08-01",
  "input_count": 50,
  "output_count": 8,
  "truncated_count": 3,
  "queue_ms": 4,
  "inference_ms": 58,
  "results": [
    {
      "candidate_id": "chunk_017",
      "raw_score": 7.81,
      "rerank_rank": 1,
      "first_stage_rank": 12,
      "truncated": false
    }
  ]
}

关键约束:

  • 只通过 candidate_id 关联结果,不依赖数组位置;
  • 服务返回稳定 Model Key;
  • 记录是否截断;
  • 区分 Queue Time 与 Inference Time;
  • 结果必须支持稳定 Tie-break;
  • 候选数量和单候选长度有硬上限;
  • 服务不自行绕过权限或重新读取无权文档。

11. 动态批处理:吞吐与尾延迟的核心

一个 Query 有 50 个候选,天然形成 50 个 Pair。多个并发 Query 又会产生更多 Pair。

如果每个 Pair 单独进入 GPU:

  • Kernel 启动开销高;
  • GPU 利用率低;
  • 吞吐不稳定;
  • 尾延迟容易受并发影响。

动态批处理会在短窗口内聚合多个 Pair:

11.1 需要调的参数

参数太小太大
max_batch_pairsGPU 利用率低OOM、单批耗时上升
max_batch_tokensPadding 少但批量小显存压力大
max_queue_delay_ms尾延迟低但吞吐差排队放大 P95
max_inflight_batches吞吐不足GPU 争用和显存峰值
request_pair_limit单请求稳定过大请求拖慢所有人

11.2 Batch Size 不能只看平均值

相同 Pair 数下:

32 × 64 Tokens

32 × 512 Tokens

显存、计算量和延迟完全不同。更合理的是同时限制:

Pair Count
+
Total Token Count
+
Max Sequence Length

11.3 公平调度

一个 500 候选的大请求不应长期阻塞普通 30 候选请求。可以采用:

  • 单请求 Pair 上限;
  • Pair 分片;
  • 每个请求每轮最多占用 N 个 Batch Slot;
  • 高低优先级队列;
  • 超大请求转离线或高级通道。

12. 模型服务的生产架构

12.1 Gateway 负责

  • Model Key 路由;
  • 输入校验;
  • Deadline;
  • 熔断;
  • 重试策略;
  • 灰度;
  • 结果缓存;
  • 失败分类;
  • Trace。

12.2 Model Worker 负责

  • Tokenization;
  • Length Bucketing;
  • Batch Inference;
  • OOM 保护;
  • 模型与 Tokenizer 版本;
  • 原始 Score;
  • 推理 Metrics。

12.3 Search Service 负责

  • 候选选择;
  • 权限范围;
  • Rerank Window;
  • 输入文本构建;
  • RRF 回退;
  • Parent / Document 去重;
  • Context Pack。

不要让 Model Worker 自己决定查询范围或访问业务数据库。


13. Timeout、重试与熔断

Reranker 是增强组件,不应该成为单点故障。

13.1 为什么不建议盲目重试

一次请求包含大量 Pair,重试会重复昂贵推理,并可能放大拥塞。

建议:

  • 连接建立失败可以短重试;
  • 明确 429 按 Retry-After 或退避;
  • 超时只在总 Deadline 仍充足时重试一次;
  • OOM、模型不存在和非法输入不自动重试;
  • 重试仍携带同一个 request_id,便于去重和 Trace。

13.2 熔断

连续失败超过阈值后:

Circuit Open
→ 一段时间直接跳过 Reranker
→ 返回 RRF
→ 后台健康探测
→ Half Open 小流量试探
→ 成功后恢复

搜索响应应明确:

{
  "retrieval_mode": "hybrid",
  "rerank": {
    "requested": true,
    "applied": false,
    "degraded": true,
    "reason": "reranker_timeout"
  }
}

14. Go Gateway:失败时回退到第一阶段顺序

package rerank

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"
)

type Candidate struct {
	ID             string  `json:"candidate_id"`
	Text           string  `json:"text"`
	FirstStageRank int     `json:"first_stage_rank"`
	FirstStageScore float64 `json:"first_stage_score"`
}

type Request struct {
	RequestID  string      `json:"request_id"`
	ModelKey   string      `json:"model_key"`
	Query      string      `json:"query"`
	TopN       int         `json:"top_n"`
	Candidates []Candidate `json:"candidates"`
}

type Result struct {
	CandidateID string  `json:"candidate_id"`
	RawScore    float64 `json:"raw_score"`
	RerankRank  int     `json:"rerank_rank"`
}

type Response struct {
	ModelKey string   `json:"model_key"`
	Results  []Result `json:"results"`
}

type Client struct {
	endpoint   string
	httpClient *http.Client
}

func NewClient(endpoint string, timeout time.Duration) (*Client, error) {
	if endpoint == "" {
		return nil, errors.New("rerank endpoint is required")
	}
	if timeout <= 0 {
		return nil, errors.New("timeout must be positive")
	}

	return &Client{
		endpoint: endpoint,
		httpClient: &http.Client{
			Timeout: timeout,
		},
	}, nil
}

func (c *Client) Rerank(ctx context.Context, input Request) (Response, error) {
	payload, err := json.Marshal(input)
	if err != nil {
		return Response{}, fmt.Errorf("marshal rerank request: %w", err)
	}

	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		c.endpoint+"/v1/rerank",
		bytes.NewReader(payload),
	)
	if err != nil {
		return Response{}, fmt.Errorf("create rerank request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Request-ID", input.RequestID)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return Response{}, fmt.Errorf("call reranker: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return Response{}, fmt.Errorf("reranker returned status %d", resp.StatusCode)
	}

	var output Response
	if err := json.NewDecoder(resp.Body).Decode(&output); err != nil {
		return Response{}, fmt.Errorf("decode rerank response: %w", err)
	}

	if len(output.Results) == 0 {
		return Response{}, errors.New("reranker returned no results")
	}
	return output, nil
}

调用方:

response, err := rerankClient.Rerank(ctx, request)
if err != nil {
    metrics.Inc("rerank_fallback_total", "reason", classify(err))
    return firstStageCandidates, nil
}

return applyRerankOrder(firstStageCandidates, response.Results), nil

回退不是吞掉错误。需要同时记录错误类型、请求规模、模型、Deadline 和第一阶段结果。


15. Rerank 之后仍然需要 Dedup 与 MMR

Cross-Encoder 对每个候选独立打分,可能把以下内容都排在前面:

  • 同一文档的相邻 Chunk;
  • 同一 Parent 的多个 Child;
  • 同一 FAQ 的不同问法;
  • 同一内容的新旧索引副本;
  • 多个来源复制的相同文档。

这些候选单独看都很相关,但一起进入 Prompt 会造成:

  • Token 浪费;
  • 单一来源被重复计数;
  • LLM 误以为同一结论有多份独立证据;
  • 其他有价值来源被挤出。

15.1 推荐顺序

RRF
→ 基础 Revision 去重
→ Rerank
→ Parent / Document 限额
→ MMR 或语义去重
→ Context Pack

为什么不在 Rerank 前完成所有去重?

因为相邻 Child 可能携带不同证据,过早合并会降低 Reranker 判断精度。可以先做确定性重复清理,再在 Rerank 后做语义多样性控制。

15.2 规则基线

  • 相同 chunk_id 只保留最高 revision;
  • 相同 projection_hash 去重;
  • 同一 parent_chunk_id 最多保留 1–2 个;
  • 同一 knowledge_id 最多保留 N 个;
  • 相邻 Chunk 合并;
  • 相同来源 URL 去重。

15.3 MMR

MMR(candidate)
=
lambda × relevance(candidate, query)
-
(1 - lambda) × max_similarity(candidate, selected)

lambda 越高越偏相关性,越低越强调多样性。它应在 Rerank 之后小规模执行,并进入离线评测。


16. 多语言与领域迁移

一个在英文 Web Passage 上训练的模型,不一定适合:

  • 中文企业制度;
  • 代码和错误日志;
  • 短弹幕;
  • 医疗、法律或金融文本;
  • 中英文混合 Query;
  • 内部产品缩写。

16.1 先做分组评测

至少按以下 Cohort 切分:

  • 中文;
  • 英文;
  • 中英混合;
  • Exact ID / Error Code;
  • 实体;
  • 概念;
  • 长问题;
  • 短 Query;
  • 表格与结构化内容;
  • 代码与配置。

总 NDCG 提升可能掩盖某个关键 Query Type 的退化。

16.2 模型路由

可以采用:

中文知识库
→ multilingual / Chinese reranker

代码与技术文档
→ code-aware 或技术语料微调模型

高风险复杂问题
→ 更强模型或 LLM Rerank

但模型数量越多,阈值、版本、容量和评测矩阵也越复杂。没有足够流量和标注时,优先选择一个覆盖主要语种的模型并做好 Cohort 评测。


17. Hard Negative 是微调的核心资产

普通负样本很容易:

Query:Elasticsearch 删除后旧事件为什么会复活
Negative:一篇完全无关的 TTS 教程

模型不需要真正理解问题就能区分。

Hard Negative 更有价值:

Delete API 的基本用法
Update API 的版本控制
索引 Alias 切换
一般性的消息重试

它们主题相近,却不能回答“删除版本保留窗口与旧 UPSERT 复活”的核心条件。

17.1 Hard Negative 来源

  • BM25 Top-K 中未标相关的候选;
  • Dense Top-K 中语义相似但不可回答的候选;
  • 线上 Reranker 误排;
  • LLM 引用错误的 Chunk;
  • 同文档不同章节;
  • 旧版本文档;
  • 权威度低但内容相似的来源。

17.2 防止 False Negative

最难的候选也最容易其实是未标注正例。处理方式:

  • 人工复核高相似候选;
  • 允许多正例;
  • 使用来源和时间判断;
  • 对不确定样本降权;
  • 避免仅因不在标注列表就自动设为负例。

17.3 训练数据版本化

至少记录:

query_id
query_type
query
positive_ids
negative_ids
labels
source_revision
annotation_version
retriever_version
mining_model

否则无法复现模型为什么在新版本上提升或退化。


18. LLM Rerank:什么时候值得使用

18.1 Pointwise LLM Judge

每个候选分别询问:

该文档是否包含回答问题所需的证据?
输出 0–3 级相关性。

优点:

  • 易并行;
  • 输出可解释;
  • 可以加入业务 Rubric。

缺点:

  • 每个候选都消耗一次推理;
  • 分数校准不稳定;
  • 模型可能过度相信表面相关性。

18.2 Pairwise

让模型比较 A 与 B:

对于这个 Query,A 和 B 哪个更能回答?

它更直接学习相对偏好,但 N 个候选完全两两比较需要近似 O(N²) 次判断。实际系统通常使用锦标赛、局部比较或蒸馏。

18.3 Listwise / RankGPT

一次给模型多个候选并要求输出排序:

Query
Candidate 1
Candidate 2
...
Candidate N

输出:3 > 1 > 5 > 2 > 4

RankGPT 一类工作表明,经过合适提示和滑动窗口,生成式 LLM 可以产生很强的零样本排序能力。

生产风险包括:

  • 候选顺序偏差;
  • 输出漏 ID、重复 ID 或格式错误;
  • 长上下文中的 Lost-in-the-Middle;
  • 滑动窗口边界导致局部最优;
  • Prompt 和模型版本变化引起排序漂移;
  • 成本和 P95 显著增加。

18.4 推荐使用边界

适合:

  • 低 QPS、高价值查询;
  • 复杂法律、合规或研究问题;
  • 需要显式 Rubric;
  • 作为离线教师标注或蒸馏;
  • Cross-Encoder 无法覆盖的新领域探索。

不适合默认用于:

  • 所有普通搜索请求;
  • 候选集很大;
  • 严格低延迟 Voice Agent;
  • 没有输出校验、回退和成本预算的链路。

19. 评测:先测候选,再测重排

19.1 第一阶段上限

必须先测:

指标回答的问题
Candidate Recall@K正确证据是否进入 Rerank Window
Oracle NDCG@K窗口内理论最高排序质量
Positive Missing Rate有多少 Query 根本没有正例可排
Filter Recall Loss正例是否被权限或范围错误过滤

19.2 重排指标

Sentence Transformers 的 CrossEncoder Evaluator 常用:

  • MRR@K;
  • NDCG@K;
  • MAP;
  • Base → Reranked Delta。

还应增加:

  • Precision@K;
  • Recall@K;
  • Top-1 Answerability;
  • Score Separation;
  • Query Cohort Delta;
  • Threshold Precision / Recall。

19.3 不要偷偷把正例塞回候选集

某些评测器可以保证所有 Positive 都进入 Rerank 候选,这能更纯粹地测模型排序能力,但会高估真实端到端效果。

真实生产评测应同时保留两套结果:

Model-only Rerank Quality
→ 正例保证在候选中
→ 测模型本身

End-to-end Two-stage Quality
→ 只重排第一阶段实际召回结果
→ 测真实系统

Sentence Transformers 文档中可以通过 always_rerank_positives=False 使用更现实的行为。

19.4 RAG 还要看 Context

排序指标提升,不一定等于回答提升。还要评测:

  • Context Precision;
  • Context Recall;
  • Answerability;
  • Faithfulness;
  • Citation Accuracy;
  • Redundancy Ratio;
  • Token Efficiency;
  • Refusal Accuracy。

19.5 评测漏斗


20. 延迟与成本预算

一次 Rerank 请求的成本近似与下面因素相关:

候选数量
×
每个 Pair 的 Token 数
×
模型计算量
÷
Batch 并行效率

20.1 不要只记录模型 Inference Time

端到端至少拆分:

Candidate Hydration
Input Construction
Tokenization
Queue Wait
Batch Scheduling
Model Inference
Result Merge
Dedup / MMR

示例预算:

阶段P95 示例
候选回填与文本构建15 ms
Tokenization10 ms
Queue Wait5 ms
Cross-Encoder Inference80 ms
结果合并与去重10 ms
Rerank 总链路120–160 ms

这只是示例,不是通用基准。CPU/GPU、模型大小、候选长度、并发、Batch 和部署地域都会改变结果。

20.2 监控指标

rerank_request_total
rerank_pair_total
rerank_input_tokens
rerank_truncated_pair_total
rerank_queue_depth
rerank_queue_wait_ms_p50/p95/p99
rerank_inference_ms_p50/p95/p99
rerank_end_to_end_ms_p50/p95/p99
rerank_batch_pairs
rerank_batch_tokens
rerank_gpu_utilization
rerank_gpu_memory_bytes
rerank_timeout_total
rerank_fallback_total
rerank_zero_result_total
rerank_model_error_total

20.3 成本优化顺序

  1. 缩小无意义 Candidate Window;
  2. 去掉确定性重复;
  3. 优化输入文本长度;
  4. 动态批处理;
  5. 长度分桶;
  6. 缓存高频 Query;
  7. 小模型基线;
  8. 高价值 Query 路由到大模型;
  9. 量化、ONNX、OpenVINO 或专用推理引擎;
  10. 微调小模型替代昂贵教师模型。

21. 缓存应该缓存什么

Rerank 结果依赖:

Query
Candidate IDs
Candidate Revisions
Candidate Text Hash
Model Key
Input Template Version
Threshold Version

缓存 Key 可以包含:

hash(
  normalized_query
  + ordered_candidate_id_revision_pairs
  + model_key
  + template_version
)

只用 Query 作为 Key 是错误的,因为候选集和文档版本会变化。

适合缓存:

  • 高频固定问题;
  • 搜索建议;
  • 文档版本稳定的 FAQ;
  • 离线评测;
  • 多个下游共享同一候选集。

不适合长缓存:

  • 高频更新知识;
  • 权限范围随用户变化;
  • Query 含个性化上下文;
  • 候选集依赖实时状态。

22. 分阶段落地方案

P0:建立可靠的二阶段 Baseline

  • 先保证 Hybrid Recall@50 / @100;
  • 使用一个主 Rerank 模型;
  • 明确 rerank_text 模板;
  • Rerank Window 固定为一个可承受值;
  • 记录 First-stage Rank 与 Rerank Rank;
  • 超时回退 RRF;
  • 建立 MRR、NDCG、MAP 与 P95;
  • 按语言和 Query Type 分组。

验收:

能证明 Reranker 在真实候选集上稳定优于 RRF,
且失败不会让搜索不可用。

P1:质量、吞吐与成本优化

  • 动态批处理;
  • Token 长度分桶;
  • 动态 Candidate Window;
  • Parent-Child 输入策略;
  • 文档与 Parent 去重;
  • min_score 分 Cohort 校准;
  • Hard Negative 微调;
  • 结果缓存;
  • 灰度和 Model Key 版本化。

验收:

主要 Query Cohort 的 NDCG 与 Context Precision 提升,
P95、GPU 成本和回退率在预算内。

P2:高级排序

  • 多模型路由;
  • Late Interaction;
  • LLM Pointwise / Listwise;
  • Teacher–Student 蒸馏;
  • 在线点击与人工反馈;
  • Listwise Loss;
  • 复杂业务 Feature;
  • 高风险 Query 独立策略。

前提:

  • 稳定 Golden Set;
  • 线上 Trace;
  • 可复现候选集;
  • 严格成本预算;
  • 明确回滚路径。

23. 十二个常见反模式

反模式一:第一阶段只召回 5 个,再让 Reranker 排 5 个

候选覆盖太低,第二阶段没有提升空间。

反模式二:把 Reranker 当成全库检索器

Cross-Encoder 需要逐 Pair 推理,不适合直接对全库计算。

反模式三:统一使用 score > 0.5

模型分数不天然可比,阈值必须校准。

反模式四:只把 Chunk 开头送给模型

真正证据可能在中间或末尾。长文本策略必须显式设计。

反模式五:所有 Metadata 都拼进输入

无关字段消耗 Token,并可能引入错误偏好。

反模式六:候选越多越好

Window 增大同时放大延迟、成本和噪声,需要用 Recall 曲线决定。

反模式七:Reranker 超时返回 500 或空结果

第一阶段 RRF 候选仍然可用,应该降级。

反模式八:只看模型推理平均延迟

Queue、Tokenization 和长尾 Batch 才可能决定 P95/P99。

反模式九:离线评测总是把正例塞回候选

这会掩盖第一阶段漏召回,夸大真实效果。

反模式十:Rerank 后不去重

多个重复 Chunk 会占满 Context,并制造虚假证据多数。

反模式十一:英文公开模型直接上线中文内部知识库

必须做语言和领域 Cohort 评测。

反模式十二:默认用 LLM Listwise 替代专用模型

没有成本、格式校验、位置偏差测试和回退时,系统会更不稳定。


24. 上线检查清单

  • 第一阶段 Recall@RerankWindow 是否足够?
  • 是否计算了 Oracle Upper Bound?
  • Rerank Window 是否来自评测而不是拍脑袋?
  • 是否按 Query Type 和语言分组?
  • rerank_text 是否稳定、版本化、无重复标题?
  • 模型最大长度与截断策略是否明确?
  • 是否记录每个候选的 Token 数和截断状态?
  • min_score 是否按模型和领域校准?
  • 是否同时保留 Raw Score、Model Key 和 Threshold Version?
  • Search Service 与 Model Worker 的职责是否分离?
  • 是否限制单请求候选数、单候选长度和总 Token?
  • 是否支持动态批处理和长度分桶?
  • Queue Wait、Tokenization 和 Inference 是否分别监控?
  • 是否有 Deadline、熔断、限流和 OOM 保护?
  • Reranker 失败是否回退 RRF?
  • 响应是否标记降级原因?
  • Rerank 后是否进行 Revision、Parent 和语义去重?
  • 是否评测 Context Precision、Redundancy 和 Citation Accuracy?
  • 是否使用真实候选集做端到端评测?
  • Hard Negative 是否经过 False Negative 检查?
  • 模型、数据、输入模板和阈值是否都能回滚?
  • 是否按目标 Elasticsearch 版本和订阅确认内置 Rerank 能力?
  • LLM Rerank 是否只用于明确的高价值通道?

总结

Reranker 的价值,不是给检索结果再加一个模型,而是把第一阶段的“主题相似候选”推进成“真正能够回答问题的证据”。

一条可靠的生产链路应该是:

第一阶段扩大 Recall
→ 用足够但受控的候选窗口进入 Reranker
→ 规范化 Query–Document 输入
→ 批量执行 Cross-Encoder
→ 记录 Model、Score、截断和延迟
→ 超时或故障回退到 RRF
→ 再做 Parent、Document 与语义去重
→ 在 Token 预算内组织 Context
→ 用真实候选集和最终答案共同评测

最重要的工程判断是:

Reranker 不能修复第一阶段不存在的证据,也不应该让一个本可降级的增强模型变成搜索系统的单点故障。

官方资料与论文

讨论

继续讨论这篇笔记

有问题、补充案例或不同观点,可以通过 GitHub Discussions 继续交流。

On this page

一句话结论1. Reranker 在完整检索链路中的位置2. Cross-Encoder 为什么通常比 Bi-Encoder 更准2.1 Bi-Encoder2.2 Cross-Encoder2.3 Late Interaction2.4 Generative / LLM Reranker3. 不要混淆模型架构与排序目标4. Candidate Window 决定 Reranker 的质量上限4.1 候选窗口的基本权衡4.2 动态窗口4.3 Oracle Upper Bound5. Reranker 的输入应该包含什么5.1 推荐单独维护 rerank_text5.2 不要把所有 Metadata 都拼进去5.3 标题重复问题6. 长文本与截断不是一个小细节6.1 常见策略策略一:在 Chunking 阶段控制长度策略二:命中 Child,Rerank Child + Parent 摘要策略三:长文档分段打分6.2 Elastic chunk_rescorer6.3 长度分桶7. Reranker Score 不是天然概率7.1 为什么不能复制统一阈值7.2 Threshold 的三种用途7.3 Elastic Score 也要看模型7.4 推荐记录原始分数与标准化分数8. Elasticsearch 内置语义重排8.1 基本 DSL8.2 显式指定 inference_id8.3 Elastic Rerank 的边界9. 自托管 Sentence Transformers CrossEncoder9.1 生产实现还需要补什么10. Rerank Service API 契约11. 动态批处理:吞吐与尾延迟的核心11.1 需要调的参数11.2 Batch Size 不能只看平均值11.3 公平调度12. 模型服务的生产架构12.1 Gateway 负责12.2 Model Worker 负责12.3 Search Service 负责13. Timeout、重试与熔断13.1 为什么不建议盲目重试13.2 熔断14. Go Gateway:失败时回退到第一阶段顺序15. Rerank 之后仍然需要 Dedup 与 MMR15.1 推荐顺序15.2 规则基线15.3 MMR16. 多语言与领域迁移16.1 先做分组评测16.2 模型路由17. Hard Negative 是微调的核心资产17.1 Hard Negative 来源17.2 防止 False Negative17.3 训练数据版本化18. LLM Rerank:什么时候值得使用18.1 Pointwise LLM Judge18.2 Pairwise18.3 Listwise / RankGPT18.4 推荐使用边界19. 评测:先测候选,再测重排19.1 第一阶段上限19.2 重排指标19.3 不要偷偷把正例塞回候选集19.4 RAG 还要看 Context19.5 评测漏斗20. 延迟与成本预算20.1 不要只记录模型 Inference Time20.2 监控指标20.3 成本优化顺序21. 缓存应该缓存什么22. 分阶段落地方案P0:建立可靠的二阶段 BaselineP1:质量、吞吐与成本优化P2:高级排序23. 十二个常见反模式反模式一:第一阶段只召回 5 个,再让 Reranker 排 5 个反模式二:把 Reranker 当成全库检索器反模式三:统一使用 score > 0.5反模式四:只把 Chunk 开头送给模型反模式五:所有 Metadata 都拼进输入反模式六:候选越多越好反模式七:Reranker 超时返回 500 或空结果反模式八:只看模型推理平均延迟反模式九:离线评测总是把正例塞回候选反模式十:Rerank 后不去重反模式十一:英文公开模型直接上线中文内部知识库反模式十二:默认用 LLM Listwise 替代专用模型24. 上线检查清单总结官方资料与论文