← 返回资讯

RAG 重排(Rerank)提升召回精度

2026-07-03

向量检索的 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 与国内替代方案无缝切换。