两阶段检索系统通常把召回和排序拆开。召回阶段用BM25、向量检索或混合检索从海量文档中快速取出候选集合,这个集合可能有一百到上千条结果;如果直接在召回结果上做复杂语义建模,延迟会很高,因此引入Reranker对候选集进行重新打分。Reranker不负责从零召回,而是专注于提升候选结果之间的相对顺序。Cross-Encoder是当前实现Reranker最直接有效的一类结构,它接收查询和单条文档的组合输入,输出一个相关性分数,最终按分数重排。下面以搜索排序为场景,说明Cross-Encoder为什么能把排序精度做上去。

在搜索、问答以及RAG(检索增强生成)链路中,Reranker的价值非常明显。召回阶段的目标是保证相关文档不漏掉,所以通常会放宽匹配条件,让候选集合尽可能大。但候选集合一大,必然混入很多表面相似但实际不相关的内容。如果把这个集合直接扔给大模型或展示给用户,效果会很差。Reranker的职责就是在这个候选集合内部做一次更精细的语义排序,把真正相关的文档提到最前面。
一、Bi-Encoder与Cross-Encoder的本质区别
双塔模型也叫Bi-Encoder,是召回阶段最常用的结构。它的思路是把查询和文档分别输入编码器,各自得到一个固定长度的向量,再用余弦相似度或点积计算两者距离。查询向量可以实时计算,文档向量则能提前离线生成并存入向量数据库,因此召回效率非常高。双塔模型的缺陷在于,查询和文档在编码过程中没有任何交互。模型无法知道查询中的某个词究竟在文档里扮演什么角色,也就难以处理词序变化、否定语义和细粒度匹配。
Cross-Encoder则换了一种思路。它把查询和文档拼接成一条完整序列,通常形式为[CLS]查询[SEP]文档[SEP],然后一次性输入一个Transformer编码器。由于Transformer内部使用自注意力机制,查询中的每个token都能直接看到文档中的每个token,两层信息在每一层都充分交互。输出层的表示会被映射为一个标量分数,用来表示查询和文档的相关程度。这种深度交互能显著提升排序精度,但代价是每对查询和文档都要完成一次完整的前向计算,无法像双塔那样预计算文档向量,因此推理成本要高得多。
下面这段代码可以直观看到两种模型在使用上的差异。Bi-Encoder分别编码查询和文档,然后计算余弦相似度;Cross-Encoder则直接把查询和文档组成文本对进行打分。
from sentence_transformers import SentenceTransformer, CrossEncoder
import numpy as np
query = "如何给手机更换电池"
docs = [
"手机电池更换教程",
"手机充电口维修方法",
"笔记本电脑电池鼓包怎么处理"
]
# Bi-Encoder:分别编码后计算余弦相似度
bi_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
q_vec = bi_model.encode(query)
d_vecs = bi_model.encode(docs)
sims = np.dot(d_vecs, q_vec) / (np.linalg.norm(d_vecs, axis=1) * np.linalg.norm(q_vec))
print(sims)
# Cross-Encoder:拼接后直接输出相关分
ce_model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
pairs = [[query, doc] for doc in docs]
scores = ce_model.predict(pairs)
print(scores)
双塔模型对“手机充电口维修方法”可能给出较高分数,因为它和查询之间存在明显的字面重叠。但Cross-Encoder能结合上下文理解“更换电池”和“充电口维修”属于不同意图,更有可能把“手机电池更换教程”排到第一。这就是交互式精排带来的精度提升。
二、Cross-Encoder Reranker的工作流程与实现
Reranker通常工作在召回之后。假设第一阶段使用向量检索或BM25召回了一百条候选文档,Reranker会把这批文档逐一和查询组成文本对,送进Cross-Encoder打分,再根据分数从高到低排序,截取前十条或前二十条作为最终结果。候选数量的选择需要在覆盖面和延迟之间做权衡。搜索场景一般取50到100条,RAG场景因为更依赖最终输入给生成模型的内容质量,往往只取Top 10到Top 20。
使用sentence-transformers库可以快速搭建一个Reranker。核心逻辑是把候选文档列表构造成查询-文档对,调用predict获取分数,再排序。以下是一个完整示例。
from sentence_transformers import CrossEncoder
def rerank(query, candidate_docs, model, top_k=10):
pairs = [[query, doc] for doc in candidate_docs]
scores = model.predict(pairs)
scored = sorted(zip(candidate_docs, scores), key=lambda x: x[1], reverse=True)
return [doc for doc, _ in scored[:top_k]]
model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
candidates = [
"Python列表推导式用法介绍",
"Python字典排序的三种方式",
"如何给Python函数添加类型标注"
]
top_docs = rerank("Python列表怎么排序", candidates, model, top_k=2)
print(top_docs)
这个例子中,查询是“Python列表怎么排序”,候选文档里只有第二条直接讨论排序问题。Cross-Encoder能够识别“列表推导式”和“列表排序”之间的差异,把最相关的“Python字典排序的三种方式”或者进一步检索到的“Python列表排序方法”排到前面。实际使用时,predict方法支持批量处理,可以通过设置batch_size参数提高吞吐。对于长文本,需要在初始化CrossEncoder时指定max_length,模型会根据设定对输入序列进行截断。
在搜索系统中,Reranker的前置召回源可以很灵活。既可以用Elasticsearch的BM25查询,也可以使用向量数据库的相似度检索,甚至可以把两者结果合并去重后再送入Cross-Encoder。这样做的好处是,召回阶段不需要做太多策略上的调优,只要保证候选集合不太小、不太偏,精排阶段就能大幅改善最终排序。
三、训练专用Cross-Encoder Reranker
通用Cross-Encoder模型大多基于MS MARCO或大规模多语言数据训练,在通用搜索任务上表现不错,但遇到垂直领域时往往不够精准。比如医药、法律、工业文档等场景,术语和相关性判断标准与通用语料差异很大。这时需要用自己的数据微调一个专用Reranker。
训练数据的基本形式是查询、文档、标签三元组。标签可以是二分类的1和0,表示相关和不相关;也可以是细粒度分数,比如0到4的等级。对于排序任务,二分类标签已经能够满足大部分需求。更关键的是负样本的选择。随机从一个静态文档库中抽取负样本,模型很容易就能区分,学不到难样本的判别能力。更好的方式是从召回结果中挖掘困难负样本,例如把BM25排名靠前但实际不相关的文档作为负样本,或者把Bi-Encoder相似度高但人工标注不相关的文档加入训练集。负样本和正样本的比例通常控制在3:1到5:1之间。
使用HuggingFace的Transformers库可以方便地微调基于BERT的Cross-Encoder。下面是一个简化但可运行的训练示例。
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
from datasets import Dataset
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=1)
data = {
"query": ["北京旅游景点", "北京旅游景点", "手机电池更换", "手机电池更换"],
"document": ["故宫博物院开放时间", "上海外滩夜景", "更换手机电池教程", "手机贴膜步骤"],
"label": [1, 0, 1, 0]
}
dataset = Dataset.from_dict(data)
def preprocess(example):
tokenized = tokenizer(example["query"], example["document"], truncation=True, max_length=128)
tokenized["labels"] = example["label"]
return tokenized
dataset = dataset.map(preprocess, remove_columns=["query", "document", "label"])
training_args = TrainingArguments(
output_dir="./reranker_model",
per_device_train_batch_size=8,
num_train_epochs=3,
logging_steps=10,
save_total_limit=2
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer
)
trainer.train()
这段代码使用回归损失来训练模型,模型输出一个标量分数。也可以把num_labels设为2,将标签转换为0和1,用交叉熵损失训练分类头。训练完成后,模型可以接回sentence-transformers的CrossEncoder或直接用Transformers做推理。数据量越大、负样本越困难,模型对排序边界的判断就越准确。
四、部署Cross-Encoder Reranker的性能优化
Cross-Encoder的推理成本远高于双塔模型。假设召回阶段返回50条文档,Reranker需要执行50次完整的Transformer前向计算。如果使用中等规模的BERT模型,单条推理耗时可能从双塔的几毫秒增加到几十甚至上百毫秒。对于在线服务,这个延迟不能被忽略,尤其是在高峰期。
降低延迟可以从多个层面入手。首先,控制候选数量是最直接的手段。很多场景里,召回Top 20和Top 50经过Reranker后的最终结果差距并不大,但计算量却相差2.5倍。其次,可以使用更小的教师模型进行蒸馏,例如把12层BERT蒸馏到6层或4层,精度损失通常可以接受。第三,开启批量推理并设置合理的batch size,能够提升GPU利用率,降低单条平均延迟。第四,将模型导出为ONNX或TensorRT格式,配合量化技术,在CPU或GPU上都能获得明显的加速。
| 优化方法 | 原理 | 适用场景 |
|---|---|---|
| 减少候选数量 | 降低前向计算次数 | 对延迟敏感、候选质量较高的场景 |
| 小模型蒸馏 | 减少Transformer层数和参数量 | 精度要求允许小幅下降的通用搜索 |
| 批量推理 | 提高硬件吞吐利用率 | GPU部署、可接受一定排队延迟 |
| ONNX/TensorRT | 图优化和算子融合 | 追求极致推理性能的生产环境 |
还有一个容易忽略的优化点是输入长度。Cross-Encoder的计算复杂度与序列长度强相关,很多长文档实际上只需要标题、摘要或关键段落就能判断相关性。在送入模型前先对文档做截断或摘要压缩,可以在不明显降低精度的前提下大幅减少计算量。综合使用这些策略,Cross-Encoder Reranker完全可以在几十毫秒内完成小规模候选集的精排,成为搜索和RAG链路中稳定提升排序精度的关键组件。
Reranker模型Cross-Encoder排序精度修改时间:2026-09-29 03:02:16