Rerank 与 Query Rewriting¶
一句话:Rerank 用 cross-encoder 对召回结果精排,Query Rewriting 用 LLM 把用户口语化问句改写成检索友好的查询——两者配合,是把召回率从 65% 拉到 92% 的关键两环。
概念¶
混合检索 + RRF 解决了"召回得够",但召回的 Top-K 里仍可能混入噪声(语义沾边但答非所问)。原因是向量检索用的是 bi-encoder(query 和文档各自 Embedding 再算相似度),它快但粗。Rerank(重排序)用更精确但更慢的 cross-encoder 对召回候选逐一精排,把真正最相关的顶到前面。
但还有一道坎:用户问句本身质量参差不齐。"那个报错了怎么办"、"续费怎么搞"这类口语化、缺关键词、指代不明的 query,检索系统很难命中正确文档。Query Rewriting(查询改写)用 LLM 在检索之前先把 query 变成检索友好的形式——补全关键词、标准化术语、拆解子问题——从源头提升召回质量。
报告标杆(彭超,§5.3 / 项目一):自研混合检索 RAG 链路(BM25+向量+Rerank(cross-encoder)+ Query Rewriting(LLM 改写)),通过 RRF 融合,召回准确率由 65% 提升至 92%(300 条标注测试集 Hit Rate@5 评测)。
原理¶
Rerank:cross-encoder vs bi-encoder¶
理解 Rerank,先要分清两种编码方式:
| 维度 | bi-encoder(双塔,检索阶段) | cross-encoder(交叉,精排阶段) |
|---|---|---|
| 机制 | query 和文档分别独立编码成向量,再算相似度 | query 和文档拼接成一个序列 [CLS] query [SEP] doc [SEP] 一起送入模型,直接输出相关性分数 |
| 是否交互 | 编码阶段无交互,靠后续点积 | 编码时 token 级深度交互(attention) |
| 精度 | 粗,语义沾边即可召回 | 精,能判断"是否真的相关" |
| 速度 | 极快——文档向量可离线预计算,查询只需算 query 向量 + ANN | 慢——每个候选都要和 query 拼接送模型一次,无法预计算 |
| 用途 | 从百万文档中召回 Top-K 候选 | 对召回的几十个候选精排 |
所以标准链路是:bi-encoder 负责快速召回(百万级 → 几十个),cross-encoder 负责慢速精排(几十个 → 最终 Top-5)。Rerank 不是替代检索,而是在检索结果之上做二次排序,把 cross-encoder 的高精度用在少量候选上,规避它的慢。常用 Rerank 模型:bge-reranker、Cohere Rerank、jina-reranker。
Query Rewriting:检索前的查询优化¶
用户原始 query 往往不是检索系统的最优输入。LLM 改写的四大策略:
| 策略 | 做什么 | 示例 |
|---|---|---|
| 术语标准化 | 把口语/简称换成文档里的规范术语 | "续费怎么搞" → "会员套餐续费流程" |
| 意图补全 | 补上 query 缺失但检索必需的关键词 | "那个报错了" → "支付接口调用报错 500 如何排查" |
| 指代消解 | 把多轮对话里的代词还原 | (承接上文)"它的价格" → "Pro 版会员的价格" |
| 子查询分解 | 复杂问题拆成多个子查询,分别召回再合并 | "对比 A 和 B 哪个好" → 拆成"A 的优缺点"+"B 的优缺点"两路召回 |
改写显著提升召回,因为:标准化后 BM25 能精确命中术语、补全后向量能召回更精确语义、分解后每个子问题都能独立检索到对应文档。
全链路:65% → 92% 怎么来的¶
把召回率从 65% 提升到 92% 不是单一环节的功劳,而是全链路叠加:
原始 query
│
├─ Query Rewriting(LLM 改写/标准化/补全) ← 提升检索输入质量
▼
改写后 query
│
├─ 并行:BM25 召回 Top-20 + 向量召回 Top-20 ← 语义+关键词互补
▼
RRF 融合 → Top-N 候选(如 50)
│
├─ cross-encoder Rerank 精排 ← 高精度排序去噪
▼
最终 Top-5 → 拼进 Prompt 喂给 LLM 生成
各环节的贡献是叠加的:Query Rewriting 从源头减少"问得不好导致的漏召回",混合检索减少"单一检索的盲区漏召回",Rerank 减少"召回到了但排序错导致 Top-5 没进"。少任一环都到不了 92%。
// LangChain4j 链路:EmbeddingStore 检索 + Rerank
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
// 1. Query Rewriting:用 LLM 把用户 query 改写(术语标准化/补全)
String rewritten = chatModel.generate(
"请把以下用户问题改写为更适合知识库检索的查询,补全关键词、标准化术语,只输出改写结果:\n" + userQuery
);
// 2. 检索:用改写后的 query 召回候选(这里示意单路向量检索,生产配 BM25 混合)
var retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(store).embeddingModel(model).maxResults(20).build();
List<Content> candidates = retriever.retrieve(rewritten);
// 3. Rerank:用 cross-encoder 对候选精排(LangChain4j 接 bge-reranker 等服务)
List<Content> reranked = rerankModel.rerank(rewritten, candidates, 5); // 取 Top-5
# LangChain / LlamaIndex 链路示例
from langchain.retrievers import EnsembleRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers import ContextualCompressionRetriever
# 1. Query Rewriting:LLM 改写(标准化/补全/分解)
rewritten = llm.invoke(
f"把下面用户问题改写为更适合检索的查询,补全关键词、标准化术语,只输出结果:\n{user_query}"
).content
# 2. 混合检索:BM25 + 向量,EnsembleRetriever 内部用 RRF 融合
ensemble = EnsembleRetriever(retrievers=[bm25_retriever, vector_retriever], weights=[0.5, 0.5])
candidates = ensemble.invoke(rewritten) # 召回候选
# 3. Rerank:cross-encoder 精排(如 bge-reranker-large)
compressor = CrossEncoderReranker(
model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-large"), top_n=5
)
reranker = ContextualCompressionRetriever(base_compressor=compressor, base_retriever=ensemble)
top5 = reranker.invoke(rewritten) # 最终 Top-5
实战要点¶
-
Rerank 只对召回候选(几十个)做,别对全库做:cross-encoder 无法预计算、每个候选都要跑一次模型,对百万文档全量 Rerank 是灾难。正确姿势是 bi-encoder 召回 Top-20~50,cross-encoder 精排到 Top-5。
-
Query Rewriting 是低成本高回报的一环:一个 LLM 调用把 query 改写,对长尾、口语化问句的召回提升立竿见影。注意加缓存(相同 query 不重复改写)和控制延迟(改写加了一跳 LLM 调用,要卡在 P95 内)。
-
召回率 92% 是全链路叠加的结果,不是单点魔法:面试要能拆解"65→72 靠混合检索、72→85 靠 Rerank、85→92 靠 Query Rewriting"这类口径。报告明确把"无 Rerank、无 Query 改写"列为已落后红旗,只会调框架默认链路的人到不了这个数。
-
Rerank 模型要和 Embedding 模型区分开:Embedding(bi-encoder)负责召回,Rerank(cross-encoder)负责精排,是两个不同模型,不能混用。常见组合:bge-large-zh(召回)+ bge-reranker-large(精排)。
-
子查询分解用于复杂对比类问题:遇到"对比 A 和 B"这类,单次检索很容易偏向一方,拆成多个子查询分别召回再合并,比硬塞一个 query 准确得多。
本节相关题目¶
| 难度 | 题目 | 链接 |
|---|---|---|
| 基础 | 什么是 Rerank、与检索阶段的关系 | → 题库 |
| 进阶 | cross-encoder Rerank 为何比 bi-encoder 更准但更慢 | → 题库 |
| 进阶 | Query Rewriting 策略、术语标准化 vs 子查询分解 | → 题库 |
| 深度 | "召回率 65%→92%"的 Hit Rate@5 评测集怎么建、各环节贡献 | → 题库 |