主题
检索完再加一层重排,RAG 的命中率还能再涨一截
上一篇混合检索里提到交叉编码器重排(rerank),只给了一段公式。这篇展开讲:它到底在算什么,什么时候值得加,怎么加才不把接口延迟拖垮。
双塔和交叉编码器,差在哪
向量检索是双塔结构(bi-encoder)。查询和文档各自过一次编码器,算余弦相似度。文档向量化是离线做的,查询来了只算一次,快。
代价是两边编码时互相看不见。文档「错误码 ERR_SSL_PROTOCOL_ERROR 表示 TLS 握手失败」,和查询「https 打不开 报错」,分开编码后向量未必靠近。
重排模型是交叉编码器(cross-encoder)。把查询和文档拼成一对,一起送进模型,直接输出一个相关性分数。token 两两之间都做了注意力,判断准得多。
代价也要认。每个候选文档都要跑一遍前向计算,召回 50 条就是 50 次推理。
所以重排永远放在召回之后,只处理小候选集。这个架构叫两阶段检索(two-stage retrieval)。
可用的模型
自托管首选 BAAI 的 bge-reranker-v2-m3。568M 参数,基于 XLM-RoBERTa-Large,多语言(BAAI 官方文档,2026-10-08 核实),中英都好用。用 FlagEmbedding 库(注意 pip 包名是 FlagEmbedding):
python
from FlagEmbedding import FlagReranker
reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)
scores = reranker.compute_score([["查询文本", "候选段落1"], ["查询文本", "候选段落2"]])不想自己管 GPU 就用 API。Cohere Rerank 3.5 按 search unit(查询单元)计费,约 $2 / 1000 次(Cohere 官方 pricing 页,2026-10-08 核实)。一个查询单元是一个 query 配至多 100 个文档,单文档超 500 token 会被切分,每块单独计件。
Jina 的 jina-reranker-v2-base-multilingual 是 278M 参数、1024 token 上下文(HuggingFace 模型卡,2026-10-08 核实),也有托管 API。
小体量、有 GPU,选 bge-reranker-v2-m3。没 GPU 或者量不大,Cohere 的账单一般比一张卡便宜。
代码怎么接
以 LangChain 为例,假设已经有一个混合检索器 retriever,返回 top 20:
python
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
reranker = CrossEncoderReranker(model=model, top_n=5)
chain = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=retriever,
)Cohere 版就是换一个 compressor,CohereRerank(model="rerank-v3.5", top_n=5)。
召回 20 到 50 条,重排后取 top 5,是我常用的配置。候选集太小重排没发挥,太大延迟顶不住。
top_n 和相邻切片
另一个细节:重排得分高的段落,相邻段落往往也得跟着给。答案跨在两段里,只给 top 1 那段,模型看到的是半句。
我的做法是重排后不只取 top_n,而是取 top 段落加上它前后各 1 个相邻切片(去重)。切片是固定 500 token 切的话,这个操作就是按 index 取邻居。
python
def expand(chunks, picked_idx, neighbors=1):
keep = set()
for i in picked_idx:
keep.update(range(max(0, i - neighbors), i + neighbors + 1))
return [chunks[i] for i in sorted(keep)]我踩过的两个坑
一个是延迟。我用 CPU 跑过 568M 的重排模型,50 条候选要 8 秒往上,线上根本没法用。
bge-reranker-v2-m3 必须 GPU 加 fp16,再不济也得把候选数砍到 20 以内。后来我上了 T4 加 fp16,50 条压到 400ms 左右。
另一个是分数漂移。重排分数不是概率,同一对文本换个模型版本分数就变。
我一开始拿 bge 的分数做阈值过滤(低于 0.3 就丢),升级模型后整套阈值全失效。教训是分数只用来排序,过滤交给 top_n,别依赖绝对值。
重排不是每条链路都值得加
它给每条查询加一次推理,成本和延迟都上去了。
语料小(几百个切片以内)、召回本来就准的场景,加重排基本是白花钱。我记得自己试过一个内部文档库,不到 200 切片,加重排前后答案质量没差别。
反过来说,语料几千切片以上、用户查询五花八门、又能接受几十毫秒延迟的,重排基本是性价比最高的一级:不用换 embedding 模型,不用重新入库,只在查询链路末尾插一段代码。