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 应遵守下面六条原则:
- 第一阶段候选集决定质量上限。 正确证据没有进入 Rerank Window,第二阶段无法凭空创造它。
- Cross-Encoder 适合小候选集精排,不适合全库召回。 它需要为每个 Query–Document Pair 重新推理。
- 模型分数不是天然可迁移的概率。 阈值必须按模型、语言、领域和 Query Cohort 校准。
- 长文本策略是相关性设计,不只是 Token 截断。 标题、命中片段、Parent 和邻接上下文需要明确组合。
- Reranker 必须有超时、熔断和回退。 第二阶段失败时,第一阶段 RRF 结果仍然可用。
- 评测必须同时看候选 Recall、排序质量、上下文质量、延迟和成本。 只看最终答案无法定位问题。
推荐的基线链路是:
BM25 / Dense / Sparse Recall
→ RRF 融合
→ Chunk / Parent 回填与去重
→ Cross-Encoder Rerank
→ MMR / 业务多样性约束
→ Context Pack
→ LLM1. 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 描述的是排序任务怎样定义。
| 排序方式 | 输入 | 输出 | 优点 | 主要代价 |
|---|---|---|---|---|
| Pointwise | Query + 一个 Document | 单个相关性分数 | 易批处理、易部署 | 不直接比较候选之间的相对关系 |
| Pairwise | Query + Document A + Document B | A 与 B 谁更相关 | 学习相对偏好 | 候选多时组合数迅速增长 |
| Listwise | Query + 一组 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)
→ 改变尺度
→ 不改变 Rank7.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_rerankerRetriever;- 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_id | Rerank 推理端点 |
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 semantics8.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_pairs | GPU 利用率低 | OOM、单批耗时上升 |
max_batch_tokens | Padding 少但批量小 | 显存压力大 |
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 Length11.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 > 4RankGPT 一类工作表明,经过合适提示和滑动窗口,生成式 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 |
| Tokenization | 10 ms |
| Queue Wait | 5 ms |
| Cross-Encoder Inference | 80 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_total20.3 成本优化顺序
- 缩小无意义 Candidate Window;
- 去掉确定性重复;
- 优化输入文本长度;
- 动态批处理;
- 长度分桶;
- 缓存高频 Query;
- 小模型基线;
- 高价值 Query 路由到大模型;
- 量化、ONNX、OpenVINO 或专用推理引擎;
- 微调小模型替代昂贵教师模型。
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 不能修复第一阶段不存在的证据,也不应该让一个本可降级的增强模型变成搜索系统的单点故障。
官方资料与论文
- Elastic:Semantic reranking
- Elastic:Text similarity re-ranker retriever
- Elastic:Elastic Rerank
- Elastic 8.19:Retriever API
- Sentence Transformers:CrossEncoder
- Sentence Transformers:Cross-Encoder Usage
- Sentence Transformers:CrossEncoder Evaluation
- Sentence Transformers:CrossEncoder Training Overview
- Sentence Transformers:Hard Negative Mining
- Nogueira 与 Cho:Passage Re-ranking with BERT
- Khattab 与 Zaharia:ColBERT
- Sun 等:Is ChatGPT Good at Search?
讨论
继续讨论这篇笔记
有问题、补充案例或不同观点,可以通过 GitHub Discussions 继续交流。
Query Rewriting 生产实战:会话补全、Multi-Query、HyDE、分解与漂移控制
从独立问题改写、精确词保留、Multi-Query、Query Decomposition、Step-Back、HyDE 到结构化查询计划、权限边界、Trace 与评测,设计一条可控的生产级 RAG 查询规划链路。
RAG Context Engineering 生产实战:上下文打包、去重、冲突与引用锚定
从候选证据归一化、Parent/邻接扩展、Exact/Near 去重、MMR 多样性、冲突治理、Token Budget、压缩、排序到 Citation Anchor,构建可追溯、可评测、可回放的生产级 Context Pack。