重排序(Rerank)是什么?为什么能提升效果?
一句话回答
重排是检索的第二阶段:先用向量检索(加上关键词检索)快速召回几十条候选,再用更准但更慢的模型逐条给"问题-文档"的相关性打分,重新排序后只取前几条交给大模型。召回用的 Embedding 是双塔模型,问题和文档分别编码,文档向量能提前算好,所以快;重排用交叉编码器,把问题和文档拼在一起输入,模型能逐词对照两者,所以更准,但每一对都要在查询时现算。
详细解析
双塔和交叉编码器
双塔(Bi-Encoder,用于召回)
问题 → 编码器 → 向量 q ─┐
├→ 计算相似度
文档 → 编码器 → 向量 d ─┘ 文档向量离线算好,存进索引
交叉编码器(Cross-Encoder,用于重排)
[问题 + 文档] → 编码器 → 相关性分数
每个"问题-文档"对都要在查询时跑一次模型
双塔要把整段文档压缩进一个固定长度的向量,而且编码文档时不知道会被问什么,只能表达"大概在讲什么"。交叉编码器在同一次计算里同时看到问题和文档,注意力可以在两边的词之间直接交互,更能判断"这段文档是否回答了这个问题"。比如问"怎么取消自动续费",召回结果里"自动续费的扣费规则"和"关闭自动续费的步骤"相似度可能差不多,交叉编码器更容易把后者排到前面。
交叉编码器没法直接用来检索全库:每次查询都要和库里每个文档跑一次模型,也不能提前建索引。所以两者分工:双塔负责从海量文档里快速找出候选,交叉编码器负责在少量候选里排准。
参数怎么取舍
- 召回数量 N:召回阶段取多少条去重排,常见是几十条。N 越大越不容易漏,但重排耗时随 N 线性增长
- 最终数量 K:重排后取前几条放进 Prompt,受上下文预算和噪声的影响,常见是 3~5 条
- 分数阈值:低于阈值的直接丢弃;全部低于阈值时,可以回答"没有找到相关资料"。不同重排模型的分数范围不同(有的在 0~1 之间,有的没有归一化),阈值要看评测集上的分数分布来定
验证方法:在评测集上对比"不重排"和"重排"的 Recall@K、MRR(K 为最终放进上下文的条数),同时记录端到端的延迟。
用大模型重排
把问题和候选列表交给大模型,让它给每条打相关性分,或者直接输出排好序的编号列表。好处是理解能力强,还能结合业务规则(比如"同一主题优先最新版本的文档");缺点是慢、贵,候选多时输入很长,输出格式要校验,候选的先后顺序也可能影响判断。多数场景先用专门的重排模型,例如 bge-reranker 这类开源模型,或者模型厂商提供的 Rerank 接口。
成本和延迟
- 重排的计算量随候选数和每对文本的长度增长,可以限制每块的长度、批量推理、部署在 GPU 上
- 给重排设超时,超时就退回召回阶段的顺序,保证服务可用
- 重排模型有最大输入长度,块太长时后半部分会被截断,不参与打分
代码示例
用 sentence-transformers 加载交叉编码器打分(模型名仅为示例):
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-base")
query = "怎么取消自动续费?"
docs = ["自动续费的扣费规则……", "在账号设置里关闭自动续费的步骤……", "会员权益说明……"]
scores = reranker.predict([(query, doc) for doc in docs]) # 每个"问题-文档"对跑一次模型
ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)
在线链路里把召回、重排、阈值和降级串起来(reranker.score 是通用写法,各家接口的参数名不同):
async function retrieve(query: string, { recallN = 50, topK = 5, minScore = 0.5 } = {}) {
const candidates = await hybridSearch(query, recallN) // 召回:混合检索
try {
const scores = await reranker.score(query, candidates.map((c) => c.text), {
signal: AbortSignal.timeout(800), // 重排超时就放弃
})
return candidates
.map((c, i) => ({ ...c, rerankScore: scores[i] }))
.filter((c) => c.rerankScore >= minScore) // 阈值按所用模型在评测集上的分数分布来定
.sort((a, b) => b.rerankScore - a.rerankScore)
.slice(0, topK) // 可能为空:没有足够相关的资料,由上层决定是否拒答
} catch {
return candidates.slice(0, topK) // 重排失败或超时:降级为召回阶段的顺序
}
}
面试官可能追问
加了重排,效果一定会变好吗?
不一定。重排只能调整候选的顺序,召回阶段漏掉的文档它找不回来,所以召回阶段的 Recall@N 是重排效果的上限。重排模型和领域、语言不匹配时(比如只在英文数据上训练的模型用在中文专业文档上),效果甚至可能变差。要在自己的评测集上对比。
召回多少条去重排合适?
看召回阶段的 Recall@N 曲线:N 从 10、20、50、100 逐步增大,等正确的文档基本都已经在候选里了,再增大 N 只会增加重排耗时。选曲线趋于平稳、延迟又能接受的 N。
重排分数能用来判断"有没有找到相关资料"吗?
可以作为信号。最高分都低于阈值时,可以让模型回答"没有找到相关资料"或者转人工,减少基于不相关资料的编造。但分数不是概率,不同查询之间也不完全可比,阈值要用评测数据定,换了模型要重新定。
可以针对自己的业务微调重排模型吗?
可以。训练数据是"问题、相关文档、不相关文档"的组合,其中最关键的是难负例:看起来很相关、其实没有回答问题的文档,可以从召回结果里挑。微调前要先有评测集,确认微调后的模型确实比通用模型好。
易错点
- 以为重排能弥补召回:召回阶段没找到的文档,重排救不回来
- 以为交叉编码器可以替代向量检索:它没法对全库预先计算,只适合给少量候选打分
- 换了重排模型,还沿用旧模型的分数阈值
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。