跳到正文

第 6 章:重排(Rerank)与检索优化

能讲清为什么粗排后还要精排、Cross-Encoder 凭什么更准、精排慢了怎么提速

📊 学习时长:40-50 分钟 🎯 完成后能力:能讲清为什么粗排后还要精排、Cross-Encoder 凭什么更准、精排慢了怎么提速 🔗 关联面试题:5 道(覆盖 5 个不同角度,见 Part 3)

这一章你会学到什么

  • ✓ 零基础也能跟着做:用一段零依赖代码,亲眼看到"粗排"会把字面像、语义相反的文档排错
  • ✓ 理解"召回 → 粗排 → 精排"为什么要分三级,而不是一步到位
  • ✓ 看懂双塔(Bi-Encoder)和交叉编码(Cross-Encoder)的本质区别及各自岗位
  • ✓ 理解 Lost in the Middle,知道精排为什么要把最相关的顶到首尾
  • ✓ 学会精排的工程提速手段(减候选 / 批量推理 / 量化)及其取舍

本章按三段式组织,不同基础的读者各取所需:

段落 给谁看 内容 占比
🚀 Part 1 · 主线实战 零基础,想先跑起来 概念白话 + 粗排排错 demo ~45%
🎯 Part 2 · 面试深度 想讲透、备战面试 漏斗分工 + Bi/Cross + 提速 ~45%
🏆 Part 3 · 验收串题 检验学到位没 关联面试题 + 自检清单 ~10%

🚀 Part 1 · 主线实战 | ~45% 这部分零基础也能跟着做。先用一段不依赖任何库的代码,亲眼看到"粗排"是怎么排错的。

在开始之前

你需要:

  • ✅ 装好 Python(3.9+),能看懂基础 Python
  • ✅ Part 1 的 demo 零依赖;Part 1 末尾的"真实精排"需要 pip install sentence-transformers(可选)
  • ✅ 最好先读过第 5 章,那章讲"召回/粗排"把候选捞出来,这章讲怎么把它们"排对"

先看一个排序失败演练(这是本章的"为什么")

下面是用合成条款构造的教学场景,不是线上投诉或客户事故。假设混合检索已经把正确 chunk 召回,但候选顺序仍可能让生成阶段误判。

有个典型 case:用户问"孩子摔伤住院,意外险能赔吗?" 系统召回的候选里,「保险责任(承保意外医疗)」和「责任免除(某些情形不赔)」两条都在:正确答案明明召回到了。但喂给 LLM 的上下文里,「责任免除」那条因为字面和问题更像,被排在了最前面,LLM 顺着第一条的语气答了句偏"不赔"的话。

这个演练说明:问题不一定在召回,也可能在排序。 召回只管"把可能相关的捞回来",而 Cross-Encoder 精排可以联合读取 query 与候选,尝试把更能回答问题的证据排到前面。是否真的改善,仍要在同一评估集上测 Recall、MRR 和生成忠实度。

一句话:召回决定候选里有没有,排序决定正确证据排得是否靠前。

这一章,我们就先用 20 行代码把"排序为什么会错"亲手复现一遍。

3 个核心概念(先白话,再术语)

概念 1:什么是"重排 / 精排"?

第 5 章的混合检索能把几十上百条"可能相关"的候选捞回来,但它们的顺序不一定对重排(Rerank,也叫精排)就是再加一道关卡,用更强但更慢的模型,把这批候选重新排一次序,把最相关的几条顶到前面。

概念 2:为什么粗排会排错?

第 5 章的向量检索和 BM25 都是独立打分的,只看 query 和单个 doc 各自的相似度/词面命中,读不懂 query 和 doc 之间的"关系"。于是一个字面很像、意思却相反的文档(比如"免责条款")可能拿到高分。

概念 3:什么是 Cross-Encoder?

Cross-Encoder(交叉编码)是精排最常用的模型:它把 query 和 document 拼在一起送进同一个模型,让两者的每个词充分"交互",从而读懂它们的关系、给出更准的相关性分。代价是慢(Part 2 详解)。

🛠️ 跟着做:亲眼看粗排排错

Step 1:写一个玩具粗排

新建 rerank_demo.py(完整文件见 code-snippets/rerank_demo.py):

query = "孩子摔伤住院,意外险能赔吗?"      # 用户的问题
candidates = [                              # 第 5 章召回回来的两个候选(顺序还没排对)
    "第2条 保险责任:本保险承保意外伤害导致的医疗费用,予以赔付",  # 真正该答的(能赔)
    "第3条 责任免除:孩子在学校受伤住院,属下列情形之一的不予赔付",  # 字面更像,却是反面(不赔)
]

def coarse_score(q, doc):                   # 玩具"粗排":独立地数 query 的字在 doc 里命中多少
    return sum(ch in doc for ch in set(q))  # set(q) 去重后,逐个字看是否在 doc 中,累计命中数

# 按粗排分从高到低排序,模拟"只看字面命中"的检索
for d in sorted(candidates, key=lambda d: coarse_score(query, d), reverse=True):
    print(f"  score={coarse_score(query, d)}  {d}")

Step 2:运行

python rerank_demo.py

你应该看到(纯字面命中,数字固定):

  score=7  第3条 责任免除:孩子在学校受伤住院,属下列情形之一的不予赔付
  score=6  第2条 保险责任:本保险承保意外伤害导致的医疗费用,予以赔付

🐛 跑不通看这里

  • 两个分数一样、看不出排错?确认 candidates 两条文本和上面一字不差,这个"反面条款字面命中更多"的效果,是特意构造的两句话撑起来的。
  • 想换自己的例子?随便改,但要让"反面/无关那条"字面上和问题更像,才能复现粗排被字面骗到的现象。
  • 没报错但顺序对了?那多半是你改了文本;字面博弈很微妙,改一个词分数就翻盘。

Step 3:看懂排错

字面命中更多的**「责任免除」(免责条款)被排到了第一**,但它恰恰是反面!真正该回答"能不能赔"的「保险责任」反而被压在后面。

根因:粗排独立打分,读不懂"免责"和问题意图是相反的。 这就是为什么需要精排。

Step 4(可选):换成真实精排

装上库,用一个真正的 Cross-Encoder 重排,「保险责任」就会被顶回第一(完整文件见 code-snippets/real_rerank.py):

# pip install sentence-transformers  (首次运行会下载模型权重,需联网)
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-base")
pairs = [(query, doc) for doc in candidates]     # query 与每个候选组成一对
scores = reranker.predict(pairs)                 # 逐对打相关性分
for doc, s in sorted(zip(candidates, scores), key=lambda x: -x[1]):
    print(f"  {s:.3f}  {doc}")

Cross-Encoder 把 query 和 doc 拼在一起读,能看懂「免责」是反面,于是把「保险责任」排回第一。这,就是精排的价值。

这一节你掌握了什么?

  • ✅ 重排/精排是什么、为什么粗排会把语义相反的排错
  • ✅ 亲手跑通了一个"粗排排错"的 demo,看清了独立打分的局限

🧭 你刚才做的,等于真实系统的什么?

  • 你写的 coarse_score()(独立数字面命中) = 第 5 章那条粗排/召回腿的简化版,它和真实的向量/BM25 一样,只看"各自像不像",看不到正反关系;
  • 你看到的"免责被排第一" = 生产里最典型的误召回,光靠召回那一层修不好;
  • 而你 Step 4 装的那个 CrossEncoder = 真实 RAG 在线链路里的精排模型,放在召回之后、喂给 LLM 之前。 一句话:你已经手动复现了"为什么需要精排",下面 Part 2 讲它在工程上怎么摆、怎么不拖慢系统。

🎯 Part 2 · 面试深度 | ~45% 这部分讲透三级漏斗、双塔 vs 交叉编码、以及精排怎么提速。

2.1 召回 → 粗排 → 精排:三级漏斗

这是一个逐级收窄、各司其职的漏斗:召回+粗排用快的方法捞全(Top-N,常见 N 取几十到一两百),精排用准但慢的 Cross-Encoder 只对这 N 个深度打分,最后留 Top-K(常见 3~10 条)给 LLM。数量逐级骤降,既保住了召回,又把昂贵的精排算力只花在刀刃上。

业内行话:面试常问"既然精排更准,为什么不跳过召回、直接拿精排扫全库?",因为 Cross-Encoder 每对 query-doc 都要现跑一次模型,扫几十万条慢到不可用。所以**"快的负责捞全,准的负责排好"**,这套漏斗是搜索、推荐、RAG 通用的工程范式。

2.2 双塔 vs 交叉编码:精排为什么更准

  • 双塔 Bi-Encoder(召回用):query 和 doc 各自独立编码成向量,最后才算相似度。好处是 doc 向量能离线预存,检索极快;代价是两者编码时不交互,精度有天花板。
  • 交叉编码 Cross-Encoder(精排用):把 query 和 doc 拼在一起([CLS] query [SEP] doc [SEP]),靠 Cross-Attention 让每个词充分交互,能读懂"'孩子摔伤'≈'意外伤害'""'责任免除'与意图相反"这种深层关系。

业内行话:一句话:双塔为了快牺牲交互,交叉编码为了准牺牲速度。 所以前者扫全库做召回,后者在小集合上做精排,天生的黄金搭档。这也呼应第 4 章:双塔就是那里的 Embedding 模型。

2.3 排好序还不够:Lost in the Middle

精排还有一个常被忽略、但面试很加分的动机:LLM 读长上下文时,对放在"中间"的内容利用率最低,这个现象学界叫 Lost in the Middle

也就是说,就算你把正确 chunk 召回来了,如果它被埋在十几条候选的中间位置塞给 LLM,模型很可能"视而不见"。所以精排的价值不只是"把对的排进来",更是把最相关的几条顶到最前面(和最后面),正好避开 LLM 的注意力盲区。

业内行话:面试被问"检索都召回对了,为什么还要重排?",除了"粗排会把反面排前面",再补一句 "还有 Lost in the Middle:位置会影响 LLM 利用率,精排负责把最相关的顶到首尾",层次立刻不一样。工程上还有个配套做法:重排后按相关性做首尾交替摆放(最相关放最前,次相关放最后),进一步对冲中间盲区。

2.4 reranker 模型怎么选

  • 中文 / 中英混合:优先看 bge-reranker(base / large / v2-m3)这类在中文榜单(C-MTEB)上表现稳的,base 版精度和速度比较均衡,是大多数项目的默认起点。
  • GPU 紧张 / 要低延迟:用更小的 MiniLM 系交叉编码,配合批量推理和量化,把延迟压住。
  • 长文档场景:可考虑 ColBERT 这类"迟交互(late-interaction)"模型,它在 token 级别预存部分表示,精度接近 Cross-Encoder,速度又好一些,算是双塔和交叉编码之间的折中。

业内行话:选 reranker 和选 Embedding 是一个道理:榜单只是起点,务必在自己的业务数据上用第 3 章那套评估集实测。还有个容易忽略的点:reranker 和 Embedding 不必同源(可以 BGE 的 Embedding 配另一家的 reranker),因为它们是两道独立关卡,各挑各的最优即可。另外别忘了把 reranker 的最大输入长度和你的 chunk 大小对齐:chunk 比 reranker 能吃的还长,尾部会被悄悄截断,精排就白做了。

2.5 精排太慢怎么办:三招提速

  1. 减少候选数量:只精排真正需要的 Top-N,候选数直接决定推理次数。
  2. 批量推理:把 N 对 (query, doc) 打成一个 batch 一次推理,而不是 for 循环逐条:最容易被新人忽略、收益却最大。
  3. 模型量化:用 INT8(配合 ONNX Runtime 等),精度损失小、提速明显。

这三招里,批量推理最该先做,它常常是新手代码里最大的浪费点:

工程提示:减候选、批量推理和量化可能降低精排延迟,也可能损失排序质量。for 循环逐条 predict 与 batch 推理是值得对比的一组实验,但正文不预设"下降一个数量级"。请在自己的硬件、候选数和模型下记录吞吐、P95/P99 与排序指标。


🏆 Part 3 · 验收串题 | ~10% 学到这一步,做几道题验证一下,知道自己学到位没。

关联面试题(5 道,覆盖 5 个角度)

  1. 【为什么要重排】 RAG 中为何需要引入重排(Re-ranking)模型?
  2. 【漏斗分工】 既然精排精度更高,为什么不能跳过召回和粗排直接对所有候选精排?
  3. 【还需重排吗】 尽管初步检索已返回相关文档,为何还需引入重排?
  4. 【提升相关性的方法】 除基本向量相似度检索外,还有哪些方法能有效提升检索结果相关性?
  5. 【是否该重排】 是否应对检索到的候选文本块进行重排序?有哪些做法?

学完这一章你应该能干嘛

跟着做(Part 1):

  • 能跑通"粗排排错"demo,讲清粗排为什么排错
  • 能说清精排(Cross-Encoder)是怎么纠正的

讲深度(Part 2):

  • 能讲清"召回→粗排→精排"为什么分三级
  • 能说清 Bi-Encoder 和 Cross-Encoder 的区别及各自岗位
  • 能讲清 Lost in the Middle,以及精排为什么要把最相关的顶到首尾
  • 能列出至少 2 种精排提速手段并讲清取舍

4 项以下 → 回去 Part 1 重做;4–5 项 → Part 2 再看一遍;6 项以上 → 进入下一章。

完整代码 + 数据集

本书每章的完整可运行代码 + 测试样本,都在配套 GitHub 仓库:

🔗 github.com/MisterBooo/rag-from-zero

  • 跟着教程 clone 下来就能跑
  • 本章的"粗排排错"demo(零依赖)和真实精排示例都在里面
  • 欢迎 Star ⭐ / Issue 反馈

导航

← 上一章:检索召回 | 下一章:Query 理解与改写 → | 回到项目首页