RAG 重排(Rerank)提升召回精度
向量检索的 ANN(近似最近邻)是粗排,它快但不精:Top-20 里可能有一半是无关文档。重排(Reranking)是 RAG 流水线的”第二道过滤”——用计算成本更高但精度更好的模型,对粗排结果做精排,把真正有价值的文档推到前面。
重排在 RAG 流水线中的位置
用户问题
│
▼
向量检索(ANN)── 快速召回 Top-K(K=20~50)
│
▼
重排模型(Reranker)── 对 Top-K 精排,取 Top-N(N=3~5)
│
▼
LLM 上下文拼装 → 生成回答
重排的输入是”问题 + 候选文档”,输出是每个候选文档的相关性分数。精排后只把分数最高的 N 篇注入上下文,噪声大幅减少。
我见过最典型的翻车现场是这样的:知识库切了几千个 300 字左右的块,向量检索 Top-5 送进 LLM,回答经常答非所问。排查下来发现,Top-5 里有 2 篇是标题相似但内容无关的文档——比如问”退款政策”,检索出来的文档里混进了”退货流程”(关键词重叠但语义不同)。这就是纯向量检索的天生弱点:Bi-Encoder 把问题和文档分别编码成向量,再算点积或余弦相似度,两个向量在训练时从未”见过面”,只能捕捉粗粒度的语义靠近,细粒度的区分能力不够。加一层重排之后,同样的知识库同样的问题,Top-3 里那两篇不相关的文档全部被压到分数末尾,答案立刻准了。
两种重排方案对比
| 方案 | 原理 | 精度 | 延迟 | 成本 | 推荐场景 |
|---|---|---|---|---|---|
| Cross-Encoder | 问题与文档拼接后整体编码打分 | 最高 | 高(逐条) | 较高(本地 GPU 或 API) | 精度要求高、Top-K 不太大 |
| Bi-Encoder 粗排 | 问题/文档独立编码,点积计算 | 中 | 低 | 低 | 粗排阶段,已被 ANN 检索覆盖 |
| Rerank API | 托管 Cross-Encoder 服务 | 高 | 中(批量并发) | 按量付费 | 无 GPU、快速上线 |
实践建议:先检索 Top-20,重排取 Top-3 注入上下文,精度比直接检索 Top-3 高 15%~30%。
为什么 Cross-Encoder 更准但更慢?关键在于”问题和文档是否一起过模型”。Bi-Encoder 阶段问题和文档各自独立编码,二者的向量在整个知识库构建时就已经算好并存进向量库,检索时只需要算一次点积,所以快,能撑住百万级文档的实时检索。Cross-Encoder 反过来,把”问题 [SEP] 文档”拼成一个序列整体喂给模型,靠 Transformer 的自注意力机制让问题和文档里的每个 token 互相”看见”对方,这样才能判断出”退款”和”退货”在这个具体问题里到底算不算相关。代价是这个计算没法预先缓存,每来一个新问题就要对候选文档重新算一遍,所以只能用在候选集已经收窄到几十篇的重排阶段,绝对不能拿来做全量检索。
代码实现
方案一:Cohere Rerank API
import cohere
co = cohere.Client("YOUR_COHERE_API_KEY")
def rerank(query: str, docs: list[str], top_n: int = 3) -> list[str]:
response = co.rerank(
model="rerank-multilingual-v3.0", # 支持中文
query=query,
documents=docs,
top_n=top_n
)
# 按重排分数返回原始文档
return [docs[r.index] for r in response.results]
model 参数选 rerank-multilingual-v3.0 而不是英文版是有讲究的:英文版对中文文档打分会明显失准,因为它的训练语料以英文为主,遇到中英混排或纯中文的技术文档时,相关性分数经常和实际相关性倒挂。这个坑我在早期联调时踩过——同一批中文知识库文档,换成英文版模型后 Top-3 的准确率肉眼可见地下降,排查了半天才想起来看模型名字。
另外两个真实会遇到的报错,提前说清楚怎么处理:
import time
import cohere
co = cohere.Client("YOUR_COHERE_API_KEY")
def rerank_with_retry(query: str, docs: list[str], top_n: int = 3, max_retries: int = 3):
for attempt in range(max_retries):
try:
response = co.rerank(
model="rerank-multilingual-v3.0",
query=query,
documents=docs,
top_n=top_n
)
return [docs[r.index] for r in response.results]
except cohere.errors.TooManyRequestsError:
# 429:免费额度或 QPS 超限,指数退避重试
time.sleep(2 ** attempt)
except cohere.errors.UnauthorizedError:
# 401:key 填错或者失效,重试没用,直接抛出让上层报警
raise
raise RuntimeError("rerank 重试耗尽仍失败,检查配额或降级为纯向量检索排序")
401 一般是 key 复制漏了字符或者环境变量没生效,重试没有意义,第一次遇到就应该直接抛出让告警系统介入,不要吞掉。429 是免费额度或者并发超过限速阈值,指数退避(2 ** attempt 秒)通常能扛过去,如果连续 3 次还失败,说明配额是真的用完了,这时候与其一直重试拖慢响应,不如降级——直接用向量检索的原始排序顶上,牺牲一点精度换可用性,比整个接口超时强。
方案二:本地 BGE-Reranker(无 API 成本)
from FlagEmbedding import FlagReranker
reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)
def rerank_local(query: str, docs: list[str], top_n: int = 3) -> list[str]:
pairs = [[query, doc] for doc in docs]
scores = reranker.compute_score(pairs, normalize=True)
ranked = sorted(zip(scores, docs), key=lambda x: x[0], reverse=True)
return [doc for _, doc in ranked[:top_n]]
bge-reranker-v2-m3 中英双语均衡,单卡 A10 可达 100 对/秒,生产环境可接受。
本地部署有几个绕不开的坑记一下。第一,compute_score 默认会把整个 pairs 列表一次性喂进模型,候选文档一多(比如 Top-50 且每篇上千字),显存直接 OOM,报错类似 CUDA out of memory. Tried to allocate xxx MiB,解决办法是显式传 batch_size 参数分批算:
scores = reranker.compute_score(pairs, batch_size=16, normalize=True)
第二,use_fp16=True 能省将近一半显存、提速也很明显,但极少数场景下(分数非常接近的两篇文档)fp16 的精度损失会导致排序发生微小反转,如果你的业务对分数的绝对值有依赖(比如前面提到的置信度过滤阈值),建议先用 fp32 跑一遍基线,确认阈值设定后再切 fp16 上线,两者排序结果的相对顺序基本一致,只是分数会有细微飘移。第三,normalize=True 会把原始 logit 分数过一层 sigmoid 映射到 01 区间,如果你看到分数全部挤在 0.40.6 之间分不出高低,大概率是文档本身和问题相关性都不高(比如切块粒度太粗,一个块里塞了好几个话题),这时候该反思的是切块策略,而不是重排模型选错了。
重排分数的工程用途
除了排序,重排分数还可以做置信度过滤:
results = co.rerank(model="rerank-multilingual-v3.0", query=query, documents=docs, top_n=10)
# 分数低于阈值的文档不注入上下文,防止引入噪声
threshold = 0.3
filtered = [docs[r.index] for r in results.results if r.relevance_score >= threshold]
if not filtered:
# 没有高置信文档时,让模型明确回答"未找到相关信息"
context = "(未检索到相关参考资料)"
else:
context = "\n---\n".join(filtered[:3])
设置阈值可以有效减少”有关联但不准确”的文档污染 LLM 输出。
阈值 0.3 不是一个可以照抄的万能数字,不同知识库、不同 Reranker 模型打出来的分数分布完全不一样。靠谱的定法是先跑一批真实历史问题(哪怕只有几十条),把每条问题对应的”人工判断相关”和”人工判断不相关”的文档分数都记下来,画出两组分数的分布,取两组重叠区间的中间值作为阈值。如果你发现相关文档和不相关文档的分数区间大面积重叠、根本分不开,说明问题不在阈值上,而是知识库本身的切块粒度或者文档质量有问题,调阈值治标不治本。
延迟权衡
重排会增加 50~200ms 延迟(取决于 Top-K 大小和方案)。优化建议:
- 粗排 Top-K 控制在 20 以内,避免重排计算量过大。
- 异步并发:用户问题到达后,向量检索与 Rerank 串行,但 Rerank 可以与其他请求并发处理。
- 对实时性要求极高的场景,考虑把 Top-K 降到 10,牺牲少量精度换速度。
如果你的系统同时服务多个用户、多路 RAG 请求并发进来,串行调用 Rerank API 会把延迟直接拖成排队时间。这时候应该用异步并发把多个用户的重排请求并行发出去,而不是一个接一个等:
import asyncio
import cohere
co_async = cohere.AsyncClient("YOUR_COHERE_API_KEY")
async def rerank_async(query: str, docs: list[str], top_n: int = 3) -> list[str]:
response = await co_async.rerank(
model="rerank-multilingual-v3.0",
query=query,
documents=docs,
top_n=top_n
)
return [docs[r.index] for r in response.results]
async def batch_rerank(queries_and_docs: list[tuple[str, list[str]]]):
tasks = [rerank_async(q, docs) for q, docs in queries_and_docs]
return await asyncio.gather(*tasks)
asyncio.gather 让多路请求同时发出,总耗时约等于单次请求的耗时而不是请求数的累加,这在高并发场景下是延迟优化里性价比最高的一步,比换更快的模型见效快得多。本地部署 BGE-Reranker 同样适用这个思路,只是并发的瓶颈从网络 I/O 换成了 GPU 显存和算力,并发数要按显存实际余量来定,盲目加并发只会把 OOM 提前触发。
成本上也值得提前算一笔账:托管 Rerank API 通常按处理的”文档-查询对”数量计费,Top-K 越大、调用越频繁,账单涨得越快,具体单价以官方定价页为准(截至 2026-06 各家价格仍在调整,别拿旧文章里的数字直接套)。粗略估算方法是:日调用量 × 平均 Top-K × 单价,算出来的月成本如果和自建 GPU 跑 BGE-Reranker 的服务器成本量级接近,就该认真评估自建的长期性价比了——尤其是调用量已经稳定、不再是验证阶段的项目。
常见问题
什么时候必须用重排? 当知识库超过 10 万文档、或用户问题与文档措辞差异较大(口语问题 vs 正式文档)时,重排收益明显。小知识库(<1 万文档)+ 切块质量好的场景,直接检索 Top-5 也够用。
重排和 HyDE(假设文档嵌入)能叠加吗? 可以叠加且效果更好:HyDE 用 LLM 生成”假设答案”扩展查询语义,弥补向量检索的语义漂移;重排进一步精排结果。两者作用于不同阶段,互相增强。
Cohere Rerank 与 BGE-Reranker 怎么选? 无 GPU 资源或快速验证阶段选 Cohere API;中文精度要求高或需要控制 API 依赖选 BGE-Reranker 本地部署。二者在中文 MTEB 重排榜上得分接近,优先考虑运维成本。
← 返回 应用模式总览:从 Prompt 到 Agent | 应用模式专题
相关阅读:RAG Embedding 选型 · 混合检索:关键词+向量 · RAG 怎么做:架构与落地
重排 API 多家切换麻烦?力达云聚合 API 统一接口,Cohere Rerank 与国内替代方案无缝切换。