重排序(Rerank)是什么?为什么能提升效果?

进阶高频原理约 7 分钟读完

一句话回答

重排是检索的第二阶段:先用向量检索(加上关键词检索)快速召回几十条候选,再用更准但更慢的模型逐条给"问题-文档"的相关性打分,重新排序后只取前几条交给大模型。召回用的 Embedding 是双塔模型,问题和文档分别编码,文档向量能提前算好,所以快;重排用交叉编码器,把问题和文档拼在一起输入,模型能逐词对照两者,所以更准,但每一对都要在查询时现算。

详细解析

双塔和交叉编码器

文本
双塔(Bi-Encoder,用于召回)
  问题 → 编码器 → 向量 q ─┐
                          ├→ 计算相似度
  文档 → 编码器 → 向量 d ─┘    文档向量离线算好,存进索引

交叉编码器(Cross-Encoder,用于重排)
  [问题 + 文档] → 编码器 → 相关性分数
  每个"问题-文档"对都要在查询时跑一次模型

双塔要把整段文档压缩进一个固定长度的向量,而且编码文档时不知道会被问什么,只能表达"大概在讲什么"。交叉编码器在同一次计算里同时看到问题和文档,注意力可以在两边的词之间直接交互,更能判断"这段文档是否回答了这个问题"。比如问"怎么取消自动续费",召回结果里"自动续费的扣费规则"和"关闭自动续费的步骤"相似度可能差不多,交叉编码器更容易把后者排到前面。

交叉编码器没法直接用来检索全库:每次查询都要和库里每个文档跑一次模型,也不能提前建索引。所以两者分工:双塔负责从海量文档里快速找出候选,交叉编码器负责在少量候选里排准。

参数怎么取舍

  1. 召回数量 N:召回阶段取多少条去重排,常见是几十条。N 越大越不容易漏,但重排耗时随 N 线性增长
  2. 最终数量 K:重排后取前几条放进 Prompt,受上下文预算和噪声的影响,常见是 3~5 条
  3. 分数阈值:低于阈值的直接丢弃;全部低于阈值时,可以回答"没有找到相关资料"。不同重排模型的分数范围不同(有的在 0~1 之间,有的没有归一化),阈值要看评测集上的分数分布来定

验证方法:在评测集上对比"不重排"和"重排"的 Recall@K、MRR(K 为最终放进上下文的条数),同时记录端到端的延迟。

用大模型重排

把问题和候选列表交给大模型,让它给每条打相关性分,或者直接输出排好序的编号列表。好处是理解能力强,还能结合业务规则(比如"同一主题优先最新版本的文档");缺点是慢、贵,候选多时输入很长,输出格式要校验,候选的先后顺序也可能影响判断。多数场景先用专门的重排模型,例如 bge-reranker 这类开源模型,或者模型厂商提供的 Rerank 接口。

成本和延迟

  • 重排的计算量随候选数和每对文本的长度增长,可以限制每块的长度、批量推理、部署在 GPU 上
  • 给重排设超时,超时就退回召回阶段的顺序,保证服务可用
  • 重排模型有最大输入长度,块太长时后半部分会被截断,不参与打分

代码示例

用 sentence-transformers 加载交叉编码器打分(模型名仅为示例):

Python
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 是通用写法,各家接口的参数名不同):

TypeScript
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 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。