跳到主要内容

创建日期:2026-09-23 | 最近更新:2026-09-23 官方说法均为逐字原文(sentence-transformers / BGE 模型卡 / Cohere / Milvus,抓取日期 2026-09-23,留档于 source/rag-lab/official-sources.md);所有数字为本机实测(RTX 3090 / torch 2.10.0 / transformers 5.7.0,脚本 source/rag-lab/bench_rerank.py,真实输出 run.log)。我自己的推断会明确标成推断。

Rerank:什么是重排、什么时候用、怎么用

一句话:rerank(重排)就是在「召回出来的候选项」里,用一个更贵但更准的模型重新算一遍相关性,把真正答得上问题的排到前面。 它不是新的检索方式,而是挂在召回后面的一道精排工序——输入是别人已经捞出来的候选,输出的是这些候选的先后顺序。

这篇按 5W1H 组织(What / Why / Where / When / Who / How),最后一节是一场真实取舍的 STAR 复盘。之所以按这个骨架写,是因为 rerank 的坑几乎都不是「不会调 API」,而是没想清楚它在链路里的位置和边界。

0. 先给结论​

问题回答
它是什么用 cross-encoder 对 (query, 文档) 逐对打分并重排;和双塔(bi-encoder)是两种结构
为什么需要双塔的「像不像」不等于「答不答得上」;实测里双塔把相关与不相关的分差压在 0.060.17,重排能拉到 2.96.2
放在哪里召回之后、组装上下文之前。它只能重排已经召回的候选项
什么时候用召回名次不稳 / 想把 top-k 压到更小以省 prompt / 要更可靠的排序稳定性 / 语料满是术语·否定·条件句
什么时候别用召回本身漏了答案 / 语料小且召回已经够准 / 延迟预算只有几十毫秒 / 你其实要的是「融合多路召回」(那是 RRF)
谁在做托管 API:Cohere、Voyage AI、DashScope(gte-rerank-v2)、SiliconFlow、HF Inference;自建:BGE 系列 + TEI / vLLM
怎么用本地 20 行代码,或一个 API 调用;关键是两个 k(召回多少、留多少)和不要用固定阈值

1. What|rerank 是什么:两种结构的分水岭​

要理解重排,只要看清一件事:query 和文档有没有「见过面」。

双塔 / bi-encoder(召回)cross-encoder(重排)
输入query 一条、文档一条,分别送进模型(query, 文档) 拼成一条一起送进模型
输出两个向量,比余弦相似度一个相关性分数
计算次数每段文本 1 次(文档侧可离线算好存库)每对 1 次(每次查询都要重算)
谁快快:查库时只算 query 那一次慢:N 个候选就是 N 次前向
能不能离线文档向量能提前算好入库不能,query 一变全部作废

关键差别不在参数量,在于双塔的 query 和文档从头到尾没有交互过。 它们各自被压成一个向量,再在最后一步比个距离——所有「query 的哪个词对应文档的哪个短语」的细节,在这个压缩过程里丢掉了。

cross-encoder 没有这个问题:它把两段文本拼在一起做一次前向,注意力机制可以在任意词之间建立关系。

sentence-transformers 官方文档给的正是这个定义(逐字):

Characteristics of Cross Encoder (a.k.a reranker) models:

  1. Calculates a similarity score given pairs of inputs (typically text pairs, but also image-text or other modalities).
  2. Generally provides superior performance compared to a Sentence Transformer (a.k.a. bi-encoder) model.
  3. Often slower than a Sentence Transformer model, as it requires computation for each pair rather than each text.
  4. Due to the previous 2 characteristics, Cross Encoders are often used to re-rank the top-k results from a Sentence Transformer model.

BGE 的模型卡更直白(逐字):

Different from embedding model, reranker uses question and document as input and directly output similarity instead of embedding. You can get a relevance score by inputting query and passage to the reranker. And the score can be mapped to a float value in [0,1] by sigmoid function.

同一个模型族里,reranker 的一句话定位是(BGE 模型卡表格里的列,逐字):

a cross-encoder model which is more accurate but less efficient

「更准但更低效」——这五个字就是 rerank 的全部立身之本,也是它的全部限制。

2. Why|为什么需要它:不是名次,是分差​

官方给出的理由(Milvus 文档逐字):

Traditional vector search ranks results purely by mathematical similarity—how closely vectors match in high-dimensional space. While efficient, this approach often misses true semantic relevance. Consider searching for "best practices for database optimization": you might receive documents with high vector similarity that mention these terms frequently, but don't actually provide actionable optimization strategies.

但「官方这么说」不等于「你的场景也这样」。所以我做了一组实测,结论和预期不完全一样,值得完整讲一遍。

2.1 实测设计​

6 组中文制度类问答,每组 1 条正确答案(gold)+ 4 条刻意构造的 hard negative。负例的构造思路是四种真实语料里常见的干扰:

类型实例(前面是 query,后面是那条负例)
高词面密度:满篇关键词但不回答问题「报销多久能到账?」→「报销单据要求:所有报销必须提供正规发票,发票抬头需为公司全称,否则不予报销。」
同模板换主体:句式和正确答案几乎一样,主体不同「哺乳期可否排夜班?」→「对怀孕 7 个月以上的女职工,不得延长工作时间或安排夜班劳动。」
条件/例外:说的是同一件事的另一半条件「离职后竞业限制补偿怎么算?」→「竞业限制:适用于高级管理人员、高级技术人员和其他负有保密义务的人员。」
邻近主题:同一章节但答非所问「报销多久能到账?」→「工资发放:每月 10 日发放上月工资,遇节假日提前至最近一个工作日。」

模型:召回用 BAAI/bge-small-zh-v1.5(约 95 MB),重排用 BAAI/bge-reranker-base(约 1.1 GB,模型卡标注语言为 Chinese and English)。

2.2 第一个意外:双塔的 top-1 全对​

用例query双塔 gold 名次重排 gold 名次
1报销多久能到账?11
2哺乳期的女员工可以安排夜班吗?11
3员工培训的费用公司给报销吗?12
4离职后竞业限制补偿怎么算?11
5请事假会扣钱吗?11
6实习生转正要满足什么条件?11

6 组里双塔全部把正确答案排在第 1。 也就是说:如果我只盯着「top-1 准不准」这一个指标,重排在这组语料上没有带来任何收益(用例 3 反而让 gold 掉到第 2,后面 §8 会解释这条为什么不算重排的错)。

这跟很多宣传材料的调子不一样。先把这个诚实结果放这儿,再往下看第二个发现——它才是重排真正的价值所在。

2.3 第二个发现:差距在「分差」上​

名次没变,但分数分布差别很大。看 gold 与「最强的那条负例」差多少:

用例双塔余弦双塔分差重排 logit重排分差
10.6609+0.10021.1188+2.8767
20.7677+0.09668.0323+4.8724
30.7157+0.05724.8298−0.4389
40.7962+0.17476.9446+5.3363
50.6926+0.11914.4340+4.8565
60.7478+0.12346.5685+6.1867
中位+0.1097+4.8644

两个模型分数量纲不同(余弦在 −1 到 1 之间,重排是未归一化的 logit),所以这里比的是「同一个模型内部,gold 和负例拉开多远」,不是两个数字直接比大小。

双塔分差最小只有 +0.057,中位 +0.11。 这意味着:正确答案和一条答非所问的干扰项,在向量空间里几乎贴在一起。名次对了,但「对的把握」非常薄。

这带来一个很实际的后果 —— 下游没法做阈值过滤:

模型取「刚好放行所有 gold」的阈值结果
双塔余弦0.6609gold 放行 6/6,负例也放行 1/24
重排 logit1.1188gold 放行 6/6,负例也放行 4/24

而且跨 query 看,「同为正确答案」的分数离散得离谱:双塔余弦 0.66090.7962(极差 0.135),**重排 logit 1.11888.0323(极差 6.91,差了 7.2 倍)**。

所以两边都不能用固定阈值。 重排的 logit 更是天生的「逐对分数」,跨 query 不可比——这一点比双塔还严重。可行的做法是用 top-k 截断,不要用阈值过滤(详见 §6.4)。

2.4 小结:重排换来的是什么​

把上面两组数字合起来看(以下是我的推断):重排在这组语料上没有改善 top-1 名次,但它把「相关」与「不相关」的间距拉开了一个数量级以上。 这个间距换来的是三件下游能做的事:

  1. 把 top-k 从 10 压到 2~3 条,直接省 prompt 长度(也省下游模型的成本);
  2. 对「一条都没命中」的判断更可靠(最高分异常低的时候,有理由怀疑召回失败);
  3. 排序不再靠「薄薄的 0.1 分差」硬撑——排序稳定性提高,逼近的负例不容易因为语料微调而翻上来。

如果你的下游不需要这三件事,那加不加重排,对你的效果指标可能真的没差别。 这是这篇最想传达的判断。

3. Where|放在链路的哪一环​

Milvus 官方文档把接入点写成了六步(逐字):

  1. Initial query: Your application sends a query to Milvus
  2. Vector search: Milvus performs standard vector search to identify candidate documents
  3. Candidate retrieval: The system identifies the initial set of candidate documents based on vector similarity
  4. Model evaluation: The Model Ranker Function processes query-document pairs:
    • Sends the original query and candidate documents to an external model service
    • The language model evaluates semantic relevance between query and each document
    • Each document receives a relevance score based on semantic understanding
  5. Intelligent reranking: Documents are reordered based on model-generated relevance scores
  6. Enhanced results: Your application receives results ranked by semantic relevance rather than just vector similarity

一句话:召回负责「别漏」,重排负责「别把不相关的排前面」。

3.1 一条硬约束:它救不了召回失败​

重排的输入是「已经召回的候选」。如果正确答案根本没被召回,重排不可能把它变出来。 这个机制我直接测了一遍——把 gold 从候选池里拿掉(模拟召回失败),看重排能怎么办:

用例gold 在池里时最高分gold 不在时最高分最高分落差
11.1188−1.7580+2.8767
28.03233.1599+4.8724
34.82985.2687−0.4389
46.94461.6083+5.3363
54.4340−0.4225+4.8565
66.56850.3818+6.1867

6 组里有 5 组,正确答案缺席时最高分明显掉下来。重排能做的只是「从剩下的负例里挑一条最像的」——它会照常给你一个看起来挺像的答案。

这反过来给了一个实用的检测信号(推断):在同一个 query 内,如果重排后的最高分异常低,值得怀疑是召回没捞到,而不是「这几条都答得不错」。注意「同一个 query 内」这个限定——上一节说过,跨 query 的 logit 不可比。

4. When|什么情况下用​

4.1 该用的信号​

信号为什么依据
召回 top-k 里答案名次不稳定(有时第 1、有时第 6)这正是 cross-encoder 的强项:逐对精算官方定位,§1
想把 top-k 压到 2~3 条(省 prompt、省下游成本)压缩的前提是「相关与不相关拉得开」,否则砍到 k=2 就是赌实测 §2.3
排序稳定性要求高双塔分差只有 0.06~0.17,负例稍微一动就可能翻上来实测 §2.3
语料术语密集、充满否定句/条件句/例外条款双塔对「未休完不予折算」和「可折算」这类差别不敏感§2 的负例构造思路
多语言语料托管 rerank 普遍主打多语言(Cohere 文档:trained for performance across 100+ languages)官方原文
混合检索(稠密 + BM25)之后融合后的顺序是「算术」出来的,需要一层语义精排§6.4

⚠️ 注意「阈值过滤」不在这个表里——虽然直觉上「有了重排就能按分数过滤」,但实测正好相反:两个模型在固定阈值下都会放进负例,重排的 logit 跨 query 还更不可比(§2.3)。想要过滤能力,请用 top-k 截断,或先做分数校准。这条单独拎出来说,是因为它是最容易想当然的一个。

4.2 不该用的信号​

信号为什么
召回本身漏掉了正确答案重排看不见没被召回的文档(实测 §3.1)。先修召回
语料小、且实测召回已经够准我这 6 组用例上双塔 top-1 全对,重排没带来名次收益(实测 §2.2)
延迟预算只有几十毫秒重排是 O(N):50 条约 39 ms,100 条约 82 ms,300 条约 217 ms(实测 §6.3,还是在一张 3090 上)
你想解决的是「多路召回怎么合并」那是融合(RRF 等),输入只有位次,跟模型重排是两件事——见下

4.3 必须澄清的混淆:RRF 不是 rerank​

这个混淆有官方源头——Milvus 文档自己把 RRF 叫作 "reranking strategy"(逐字):

Reciprocal Rank Fusion (RRF) Ranker is a reranking strategy for Milvus hybrid search that balances results from multiple vector search paths based on their ranking positions rather than their raw similarity scores.

但它的输入只有位次,不看文档内容、也不需要模型。公式是 score(d) = Σ 1/(k + rank_i(d)),对每一路把 d 召回的通道求和。

我用官方文档那张示例表(两路 ANN 的 topK=5 位次)复算了 RRF 分数,六个分数全部对上,误差约 1e-9:

sparse 位次: 101→1, 203→2, 150→3, 198→4, 175→5
dense 位次: 198→1, 101→2, 110→3, 175→4, 250→5

101 : 1/(60+1) + 1/(60+2) = 0.03252247 官方 0.03252247 差 4.88e-09
198 : 1/(60+4) + 1/(60+1) = 0.03201844 官方 0.03201844 差 2.62e-09
175 : 1/(60+5) + 1/(60+4) = 0.03100962 官方 0.03100962 差 4.62e-09
203 : 1/(60+2) = 0.01612903 官方 0.01612903 差 2.26e-09
150 : 1/(60+3) = 0.01587302 官方 0.01587302 差 4.13e-09
110 : 1/(60+3) = 0.01587302 官方 0.01587302 差 4.13e-09
250 : 1/(60+5) = 0.01538462 (官方结果表里没有 —— 被 topK 截断了)

两个副产品:

  • 150 与 110 分数完全相同(都只被一路召回、位次都是 3),官方结果表里确实并列第 5。RRF 会造出并列,而并列怎么打破直接影响 top-k 截断;
  • 官方文档说参数 k「可以有效地改变全文检索与向量检索的相对权重」,但我把 k 取 1 / 10 / 60 / 100 / 1000 / 16384 全试了一遍,排序完全没变。k 改的是分数的绝对尺度和「多路同时命中」的加成强度,只有当位次结构很接近时才可能翻动顺序。这是官方说法与实测的落差,一并记在这里。

一句话区分:RRF 回答「几路召回的结果怎么合并」,rerank 回答「一批候选里哪条真的答得上问题」。 两者可以串联,顺序见 §6.4。

5. Who|谁在做重排​

路线代表特点适合
托管 APICohere Rerank(示例用 rerank-v4.0-pro)、Voyage AI、DashScope(gte-rerank-v2)、SiliconFlow、Hugging Face Inference不用运维;按次计费;返回带 relevance_score想先验证效果、或不想养 GPU
自建模型BAAI/bge-reranker-base / -large / -v2-m3;英文场景可用 cross-encoder/ms-marco-*数据不出内网;可控;要 GPU内网数据、量大、要压成本
推理服务TEI、vLLM把上面那些模型包成服务自建但不想自己写推理代码
数据库内置Milvus Model Ranker(2.6+ 标注 beta)检索语句里直接指定 ranker已经在用 Milvus、不想多加一跳

Milvus 文档列了它支持的 7 家提供方(vLLM / TEI / Cohere / Voyage AI / SiliconFlow / DashScope / Hugging Face),并给了每条的限制(逐字):

  • Model rankers cannot be used with grouping searches.
  • Fields used for model reranking must be text type (VARCHAR).
  • Each model ranker can use only one VARCHAR field at a time for evaluation.

⚠️ 版本提醒:Milvus 文档仓库的默认分支是 v3.0.x,而 model-ranker-overview.md 的 frontmatter 标着 beta: Milvus 2.6.x。本仓库 milvus 栏目当初实测的是 2.5.10——2.5 上没有 Model Ranker,别把版本对错了。

6. How|怎么用​

6.1 最简形态:本地跑,二十行​

# pip install torch transformers
import torch
from transformers import AutoTokenizer, AutoModel, AutoModelForSequenceClassification

# 召回侧:双塔(文档向量可离线算好入库,查库时只编码 query)
emb_tok = AutoTokenizer.from_pretrained("BAAI/bge-small-zh-v1.5")
emb = AutoModel.from_pretrained("BAAI/bge-small-zh-v1.5").eval()

# 重排侧:cross-encoder
rrk_tok = AutoTokenizer.from_pretrained("BAAI/bge-reranker-base")
rrk = AutoModelForSequenceClassification.from_pretrained("BAAI/bge-reranker-base").eval()

def rerank(query: str, docs: list[str], top_n: int = 5):
pairs = [[query, d] for d in docs] # ★ 关键:拼成对
feat = rrk_tok(pairs, padding=True, truncation=True,
max_length=512, return_tensors="pt")
with torch.no_grad():
scores = rrk(**feat).logits.view(-1) # 未归一化的 logit
order = scores.argsort(descending=True)[:top_n]
return [(docs[i], scores[i].item()) for i in order]

三个细节别写错:

  • pairs 必须是 [query, doc] 两元组——这是 cross-encoder 的输入契约,写成两条独立文本就变成双塔了;
  • 召回侧给 query 加 BGE 模型卡推荐的指令前缀「为这个句子生成表示以用于检索相关文章:」,文档侧不加;
  • 重排模型不需要指令前缀。

6.2 托管 API 形态:一个调用​

# pip install cohere;需要 CO_API_KEY
import cohere
co = cohere.ClientV2()

res = co.rerank(
model="rerank-v4.0-pro", # 官方文档示例用的型号
query=query,
documents=docs,
top_n=5,
)
for r in res.results:
print(r.index, r.relevance_score) # index 是原列表下标,不是排好序的新列表

官方对返回值的说明(逐字):

Note that the index works as it does in Python, with index=0 being the first document.

文档有结构化字段时,官方给了个具体建议(逐字):

If your documents contain structured data, for best performance we recommend formatting them as YAML strings.

(官方示例里就是把 {"Title": ..., "Content": ...} 这样的 dict 先 yaml.dump 成字符串再送进去——标题类字段对重排很有帮助,值得照做。)

6.3 两个 k 怎么定​

重排链路里有两个 top-k,别混:

参数含义怎么定
召回 k(喂给重排多少候选)决定「漏不错过」的上限宁可多给。实测代价:50 条约 39 ms、100 条约 82 ms、300 条约 217 ms(3090)
输出 n(重排后留几条)决定进 prompt 的体积看下游。有了重排的分差,通常能压到 2~5 条

⚠️ 注意那个对照:双塔侧一次查询只编码 query 是 5.3 ms,与语料规模无关;而重排随候选数近似线性增长。10 万条语料如果全量走重排,按 N=300 的速率外推约 72 秒一次——所以「重排 top-k」不是优化技巧,是这个结构唯一可行的用法。

6.4 三个工程细节​

① 不要用固定阈值过滤。 §2.3 已实测:两个模型在「放行所有 gold」的阈值下都会放进负例;重排的 logit 跨 query 极差达 7 倍,天然不可比。用 top-k 截断,或按 query 做分数归一化/校准。

② chunk 不要太长。 重排模型有 max_length(上面代码里是 512),超了会被截断。chunk 太长时,被截掉的那部分等于没参与打分——而 chunk 的切分策略又正是召回阶段最容易翻车的地方(见入门篇)。

③ 和 RRF 串起来时,顺序是「先融合、再重排」。 融合是 O(1) 的算术,重排是 O(N) 的模型前向;先用 RRF 把多路召回合成一份候选、再对这一份候选做重排,比每路各重排一次再融合要省得多,也更符合两者分工(融合解决「多路怎么合」,重排解决「哪条真的相关」)。

7. STAR|一次真实的取舍复盘​

前面的数字不是「演示」,是我为写这篇真的跑的一轮。按 STAR 记一遍——最有价值的部分是 A 里那个被迫换指标的转折。

S(Situation):一个中文制度类知识库的 RAG。已经有双塔召回(bge-small-zh-v1.5),用户问的是「报销多久到账」「哺乳期能不能排夜班」这类细节型问题,答案往往只有一段话。要不要再叠一层重排?

T(Task):预算限定——单次查询最多能多花 100 ms;机器是一张 3090;需要回答的是「加这一层,到底换来了什么」。

A(Action):

  1. 先构造 6 组 hard negative(四种干扰类型,见 §2.1)。不构造负例就没法测——语料里全是「明显不相关」的段落时,两种模型都会好看。
  2. 写探索脚本,同时跑两个模型,先看它们在哪些用例上分歧,而不是先假定重排更好。结果出乎意料:双塔 top-1 全对。
  3. 于是换指标:名次没有区分度,改看「gold 与最强负例的分差」和「阈值过滤下的假阳性」。
  4. 补测代价曲线(N=10/50/100/300)和召回上限(把 gold 拿掉)。

R(Result):

观察数字
top-1 名次双塔 6/6 正确,重排 5/6(唯一那组是标注歧义,见 §8)
分差(同模型内比较)双塔中位 +0.1097;重排中位 +4.8644
阈值过滤两个模型都不能用固定阈值
代价重排 50 条 39 ms / 100 条 82 ms / 300 条 217 ms
召回上限gold 不在候选池时,重排最高分在 6 组里 5 组明显下降——但它仍然会给出一个像样的答案

决策(推断部分):在「top-1 正确率」这个指标上,这组语料不支持加重排。 支持加它的理由是另外两条——把 top-k 压到 2~3 条以缩短 prompt,以及用「最高分异常低」来发现召回失败。所以要不要加,取决于你的下游需求,而不是「重排更先进」。

8. 六个坑​

  1. 把 RRF 当重排。 名字撞车(官方文档自己把 RRF 叫 "reranking strategy"),但一个是算位次的融合、一个是看内容的模型。§4.3 有验算。
  2. 指望重排救召回失败。 看不见的文档它排不了。§3.1 把 gold 拿掉后,它照样挑一条负例给你。
  3. 用固定阈值过滤。 实测里两个模型都会放进负例;重排的 logit 跨 query 不可比。
  4. 拿重排分数跨 query 比大小。 同为正确答案,logit 从 1.12 到 8.03。要跨 query,先做归一化。
  5. 把 top-1 当成唯一指标。 这次实测里,top-1 完全没变化,但分差和下游能力变了。先想清楚你要哪个。
  6. 不审自己的评测集。 用例 3 是这次实测里唯一「重排变差」的一组:gold 是「职业资格考试可报销考试费」,负例是「培训期间的差旅费按差旅标准报销,培训费由公司统一支付」。两条其实都在回答 query,gold 只是更精确。 与其说重排错了,不如说我的标注太严——这种歧义在小评测集里非常常见,务必人工看几遍。

9. 来源与核对​

引用来源存档
cross-encoder 四条特征、logit 与 sigmoidsentence-transformers docs/cross_encoder/usage/usage.rst(master)official-sentence-transformers-cross-encoder.rst
reranker 与 embedding 的区别、more accurate but less efficient、模型语言HF 模型卡 BAAI/bge-reranker-v2-m3、BAAI/bge-reranker-baseofficial-bge-reranker-base.md
query 指令「为这个句子生成表示以用于检索相关文章:」HF 模型卡 BAAI/bge-small-zh-v1.5official-bge-small-zh-v1.5.md
Rerank 定义、YAML 结构化建议、多语言、relevance_score/indexdocs.cohere.com/docs/reranking.mdofficial-cohere-reranking.md
传统向量检索的局限、Model Ranker 六步与限制、7 家提供方milvus-docs v3.0.x model-ranker-overview.mdofficial-milvus-model-ranker-overview.md
RRF 定义与 k 原文、示例位次与 Final Scoremilvus-docs v3.0.x rrf-ranker.mdofficial-milvus-rrf-ranker.md

实测脚本与真实输出都在 source/rag-lab/:bench_rerank.py(§2/§3/§6.3 的数字)、verify_rrf.py(§4.3 的验算)、cases.py(语料)、explore_hard_cases.py(当初找用例用的探索脚本)、run.log。

复现环境:python 3.10、torch 2.10.0+cu126、transformers 5.7.0、NVIDIA RTX 3090。跑法 bash source/rag-lab/run.sh(首次会下载约 1.2 GB 模型)。