3104 字
约 10 分钟
14
S27-X11 · Reranker 与重排模型:检索质量的第二道闸

向量检索的问题不是"不准",而是它用一次相似度计算替你做完了整个判断。把一段几百字的文档压成一个定长向量,再和问题算一个余弦距离,这个过程中必然会丢信息:否定词被平均掉了、数字与专有名词被"语义相近"糊过去、长文档里只出现一次的关键句被整体稀释。

重排(rerank)就是给这一步加一道复核:召回放宽,再用一个更贵但更准的模型精挑。


1. 为什么只靠向量检索不够

问题 机制原因 典型错例
单向量表达有损 整段文本压成一个点,细节被平均 长文档里一句关键限制被淹没
长文本相似度失真 向量对长度敏感,短文更容易高分 短摘要挤掉真正详细的那篇
否定与条件难区分 "支持退款"与"不支持退款"向量极近 召回结论完全相反的两条
专有名词/编号 语义相近但实体不同 订单 A12 与 订单 A21
同义改写 向量擅长,但排序不分层 十条都相关,无法排优先级
多条件组合 单向量无法表达"既要又要" 只命中其中一个条件

关键词检索(BM25)能补一部分(编号、精确词),但对"语义相关但字面不同"无能为力。所以不是换掉向量,而是加一层能同时看 query 和 doc 的判断。

2. 两阶段架构

用户问题
   │
   ├─ 阶段一 召回(宽):向量 / BM25 / 混合 → topK 50~200
   │        目标:不漏(recall 优先)
   │
   └─ 阶段二 重排(精):Cross-Encoder 逐对打分 → topN 3~8
            目标:排对(precision 优先)
阶段 模型形态 输入 耗时特征 调参重点
召回 Bi-Encoder / 倒排 query 与 doc 分别编码 可预计算,与库大小近似无关 topK 别太小
重排 Cross-Encoder (query, doc) 拼在一起 与候选数线性相关 候选数与阈值

架构上最关键的一个数字是召回的 topK。 重排只能从候选里挑,候选里没有正确答案,重排再强也救不回来。这一点下面还会再提。

3. Cross-Encoder 与 Bi-Encoder 的机制差异

维度 Bi-Encoder(召回) Cross-Encoder(重排)
编码方式 query、doc 各自独立编码 拼成一个序列联合编码
是否交互 只在最后算向量距离 每个 token 之间全交互
能否预计算 能,doc 向量可离线建库 不能,每次都要现算
精度 中 高
延迟 低(一次向量检索) 高(候选数 × 一次前向)
规模上限 百万级文档 几十到几百条候选
# 机制示意:Cross-Encoder 的输入是拼接后的序列
pair = f"{query} [SEP] {doc_text}"
score = model.predict(pair)      # 输出一个相关性分数

这也解释了为什么重排不能替代召回:它没法预计算索引。百万文档逐对打分,成本不可接受。

最小可用的两段式调用,把召回与重排拼成一个函数:

def retrieve_and_rerank(query, top_k=100, top_n=5, thr=0.0):
    cands = hybrid_search(query, top_k)          # 阶段一:宽召回
    pairs = [(query, c["text"][:2000]) for c in cands]
    scores = reranker.predict(pairs)             # 阶段二:逐对打分
    ranked = sorted(zip(cands, scores), key=lambda x: -x[1])
    return [c for c, s in ranked if s >= thr][:top_n]

4. 输入输出与阈值

项 说明 实践要点
输入 (query, passage) 对 passage 要截断,超长部分会被丢弃
输出 相关性分数 通常是 logit 或归一化分,不是概率
排序 按分数降序 同分时用召回分或时间做二级排序
阈值 低于阈值视为不相关 必须用自有数据校准,不能照搬默认值
截断 每条候选的最大长度 截断位置错了会丢掉关键句

分数不能当置信度直接对外。 一个 0.82 的重排分,含义是"这条比那条更相关",不是"有 82% 概率能回答问题"。两者差得很远,把前者当后者会导致"高置信度但答错"。

阈值校准的做法很朴素:拿一批标注过的样本,画出分数分布,选一个能同时满足"该拒的拒掉、该留的留住"的位置。

def gate(results, thr: float, top_n: int):
    kept = [r for r in results if r["score"] >= thr]
    if not kept:
        return []                       # 全部低于阈值 → 走"无法回答"分支
    return sorted(kept, key=lambda r: -r["score"])[:top_n]

"全部低于阈值"这个分支必须显式处理,否则系统会硬着头皮用最不相关的那条去生成,产生看起来很像真的胡说。

5. 可选的重排模型类别

这里只讲类别与选型维度,具体型号与规模按当前版本核对:

类别 形态 适合 选型要看
开源重排模型系列(如 bge-reranker 一类) 可本地部署,有不同参数规模 数据不出内网、成本敏感 语言覆盖、最大长度、显存占用
托管重排 API(如 Cohere Rerank 一类) 一次调用返回排序结果 不想运维、候选数不大 多语言效果、单价、并发限制
通用大模型打分 用 prompt 让 LLM 给相关性分 需要解释理由、候选很少 稳定性、成本、延迟
自建微调 在自己的标注数据上训练 领域术语重、通用模型效果差 标注量、过拟合、迭代成本

选型的四个维度,按优先级排:

  1. 语言与领域匹配:中文/中英混排效果差别很大,先小样本试;
  2. 最大输入长度:文档块比模型上限长,就会被截断;
  3. 延迟与吞吐:本地小模型单条几十毫秒,API 多一次网络往返;
  4. 可运维性:本地要管显存与扩缩容,API 要管配额与降级。

6. 部署与延迟

方案 单次延迟量级 吞吐扩展方式 注意
本地小模型(CPU) 数十毫秒/条 加进程、批量 候选多时线性叠加
本地模型(GPU) 更低的单条延迟 批处理 + 多卡 显存决定 batch 上限
托管 API 含网络往返 并发请求 要限流,避免拖垮整体
大模型打分 最高 并发 只适合候选极少

批量重排的吞吐技巧,按收益排序:

技巧 效果 代价
先把候选截断到固定长度 减少无效计算 可能丢尾部关键句
动态 batch(按长度分桶) 显著提升 GPU 利用率 实现复杂度上升
两段式:先粗筛再精排 减少精排条数 多一层模型
缓存 (query, doc) 分数 高频 query 命中 缓存键设计要稳
降 topK 到刚好够用 线性降低耗时 召回风险

延迟预算的算法应该是倒推的:总预算减去生成耗时与网络开销,剩下的才是重排可用的时间;用它除以单条耗时,得到能精排的候选条数上限。

7. 怎么量化重排的收益

指标 衡量什么 怎么算
recall@k(召回阶段) 正确答案是否进了候选 标注集上统计命中率
nDCG@n / MRR 排序质量 需要相关性等级标注
端到端答案正确率 最终用户感知 同一份问题集,人工或模型判分
拒答准确率 该拒的是否拒了 负样本集上的通过率
P95 延迟 体验 压测或线上采样
单位查询成本 经济性 token/调用次数折算
def compare(baseline, reranked, labeled):
    """在同一份标注集上对比:只看排序变化是否真的帮到最终答案"""
    out = {"recall@k": 0, "ndcg@n": 0, "answer_ok": 0}
    for q, gold in labeled.items():
        b = baseline[q][:5]
        r = reranked[q][:5]
        out["recall@k"] += int(any(d in gold for d in r)) / len(labeled)
        out["ndcg@n"] += ndcg(r, gold) / len(labeled)
    return out

对比实验有三条纪律:同一份数据集、同一个生成模型、同一套 prompt。只换重排,其他全固定,否则测出来的差异归因不到重排头上。

8. 什么时候不需要重排

情况 判断依据
召回本来就准 标注集上 recall@5 已经很高,重排提升空间小
延迟预算极紧 倒推出来的候选条数小于 10,重排收益有限
库很小且结构化 几百条 FAQ,关键词匹配就够
查询以精确匹配为主 订单号、错误码查询,BM25 更合适
生成阶段会自行过滤 上下文窗口足够大且模型能忽略噪声

反过来,这些情况重排收益最明显:文档块长且内容杂、query 含多个条件、领域术语多、召回用混合检索且分数不可比。


9. 排错:七种"加了重排反而更差"

现象 原因 处理
重排后正确答案消失 召回 topK 太小,候选里本来没有 提高 topK 再重排
排序和召回分完全相反 两套分词/归一化口径不同 统一预处理,别一边去停用词一边不去
长文档总是排后面 输入被截断到很短 换更大长度的模型,或先切块
中文效果差 模型主要面向英文 换多语言/中文模型,小样本验证
阈值一设就全被过滤 用了别人的默认阈值 在自有标注集上重新校准
P95 延迟翻了几倍 候选数 × 单条耗时没算进预算 降 topK、加 batch、加缓存
分数当置信度对外 把排序分解释成概率 单独训练校准层,或干脆不对外

10. 边界:什么时候重排不是答案

场景 该做什么 理由
召回完全跑偏 先修召回(分块、嵌入、混合检索) 重排救不了没进候选的答案
文档切块切坏了 调分块策略 块内语义不完整,任何模型都判不准
需要多跳推理 用查询改写/多轮检索 单轮重排解决不了缺信息
库只有几十条 直接全量喂给模型 重排是多余的中间层
对可解释性要求极高 规则 + 关键词加权 重排分数难解释

一句话定位:重排是把"召回够宽"变成"排序够准"的那一步。它提升的是排序质量,不是召回质量——这两件事经常被混为一谈。


11. 自测

  1. 向量检索为什么难以区分"支持退款"与"不支持退款"?
  2. Cross-Encoder 为什么不能替代召回阶段?
  3. 重排的 topK 设得太小会发生什么?
  4. 重排分数为什么不能直接当置信度对外?
  5. 量化重排收益时,为什么要固定生成模型与 prompt?
  6. 什么情况下加一层重排反而没有意义?
S27-X11 · Reranker 与重排模型:检索质量的第二道闸
https://www.clxhxhhr.top/posts/5789/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。