本节导读:初检结果中混入了噪声文档怎么办?重排模型(Reranker)是 RAG 系统中"最后一道质量关卡"。本节讲解 Cross-Encoder 重排原理、主流模型选型、ColBERT 晚交互模型以及完整的重排 Pipeline 实现,帮你把送入 LLM 的文档质量拉满。
RAG 系统的检索通常分两阶段:初检(Retrieval) 用双编码器(Bi-Encoder)从全量文档中快速召回 Top-K 候选;重排(Reranking) 用交叉编码器(Cross-Encoder)对候选集做精细排序。这种"粗筛 + 精排"的两阶段架构是当前工业界的标准做法。
Bi-Encoder(双编码器)将查询和文档分别编码为向量,再计算相似度。这种"独立编码"的方式天然丢失了查询和文档之间的细粒度交互信息。
举个例子:查询"RAG 的检索优化方法"和文档"检索增强生成系统中提升召回率的技术手段"。Bi-Encoder 能捕获到整体的语义相似性,但很难精确判断"检索优化"和"提升召回率"之间是否存在直接的匹配关系。Cross-Encoder 则将查询和文档拼接成一个序列,通过注意力机制让每个查询词与每个文档词直接交互,能捕捉到这种细粒度的匹配。
性能代价:Cross-Encoder 的计算复杂度是 O(n × candidate_count × seq_len²),不能用于全量检索。这就是为什么需要两阶段架构——Bi-Encoder 做粗筛把候选从百万缩到几十,Cross-Encoder 在小候选集上做精排。
根据多个公开基准测试的实验数据,在标准 RAG 场景下:
| 指标 | 无重排 | 加重排 | 提升 |
|---|---|---|---|
| NDCG@5 | 0.52 | 0.71 | +37% |
| Recall@10 | 0.68 | 0.75 | +10% |
| 端到端准确率 | 64% | 78% | +14pp |
重排对 NDCG(归一化折损累积增益)的提升尤为显著,因为它直接优化了排序质量,让最相关的文档排在最前面。
# 本地重排模型 # pip install FlagEmbedding # BGE-Reranker # pip install torch>=2.0 # PyTorch 后端 # API 重排服务 # pip install cohere # Cohere Rerank API # pip install jina # Jina Reranker API # 评估工具 # pip install ragas # RAG 评估框架
前置知识:本教程 4.1 节(相似度计算基础)、Transformer 注意力机制的基本概念。
BGE-Reranker(BAAI General Embedding - Reranker)是目前开源社区最流行的重排模型之一,由北京智源研究院发布。它基于 Cross-Encoder 架构,支持中英文,且提供多个尺寸的模型。
from FlagEmbedding import FlagReranker import torch # 初始化重排模型 # bge-reranker-v2-m3: 多语言轻量版(约 560M 参数),推荐入门使用 # bge-reranker-v2-gemma: 更大更强,适合对质量要求极高的场景 reranker = FlagReranker('BAAI/bge-reranker-v2-m3', use_fp16=True) # 准备查询和候选文档 query = "RAG 系统中如何选择合适的向量数据库" candidates = [ "向量数据库是 RAG 系统的核心组件,主流选择包括 FAISS、Milvus、Weaviate 和 Qdrant", "本文介绍 RAG 系统的完整搭建流程,从文档预处理到最终部署", "Milvus 是一款开源向量数据库,支持多种索引类型,适合大规模生产部署", "大语言模型的微调方法包括 LoRA、QLoRA 和全参数微调", "FAISS 提供了多种索引算法,其中 HNSW 索引在百万级数据上表现优异", ] # 计算重排分数 pairs = [[query, doc] for doc in candidates] scores = reranker.compute_score(pairs, batch_size=8, max_length=512) # 按分数排序 ranked_results = sorted( zip(candidates, scores), key=lambda x: -x[1] ) print("重排结果:") for rank, (doc, score) in enumerate(ranked_results, 1): print(f" #{rank} [分数={score:.4f}] {doc[:60]}...") # 预期:关于向量数据库选择和对比的文档排在最前面
关键参数说明:
use_fp16=True:半精度推理,速度提升约 2 倍且精度损失可忽略batch_size:根据 GPU 显存调整,RTX 3090 可设 32-64,CPU 则设 8-16max_length:查询+文档拼接后的最大 token 数。BGE-Reranker 默认 512,对于中文文档通常够用如果你不想在本地部署 GPU 模型,Cohere 提供了高质量的 Rerank API,调用简单且无需管理基础设施。
import cohere co = cohere.ClientV2("your-cohere-api-key") # 需配置 API Key query = "RAG 系统中如何减少检索的延迟" documents = [ "使用 FAISS 的 HNSW 索引可以将百万级文档的检索延迟控制在 5 毫秒以内", "RAG 系统的端到端延迟由检索延迟和生成延迟两部分组成", "ColBERT 模型通过晚交互机制在保持精度的同时优化了重排速度", "混合检索结合 BM25 和向量检索可以提升召回率但不一定降低延迟", "缓存热门查询的检索结果是最简单有效的延迟优化手段", ] # 调用 Cohere Rerank API response = co.rerank( model="rerank-v3.5", # 最新版本 query=query, documents=documents, top_n=3, # 返回 Top-3 ) print("Cohere Rerank 结果:") for result in response.results: idx = result.index score = result.relevance_score print(f" #{idx+1} [分数={score:.4f}] {documents[idx][:60]}...")
Cohere Rerank vs 本地 BGE-Reranker 对比:
| 维度 | Cohere Rerank API | 本地 BGE-Reranker |
|---|---|---|
| 部署成本 | 无(按 API 调用计费) | 需要 GPU 服务器 |
| 延迟 | 100-300ms(含网络) | 10-50ms(本地) |
| 隐私 | 文档发送到外部 API | 文档不离开本地 |
| 中文支持 | 好 | 非常好(bge-m3 多语言) |
| 定制能力 | 不可微调 | 可微调适配领域 |
| 免费额度 | 1000 次/月 | 完全免费 |
选型建议:如果文档涉密或对延迟敏感,用本地 BGE-Reranker;如果是快速验证或中小规模项目,Cohere API 更省事。
ColBERT(Contextualized Late Interaction over BERT)提出了一种介于 Bi-Encoder 和 Cross-Encoder 之间的折中方案:晚交互(Late Interaction)。
它的核心思想是:先分别用 BERT 编码查询和文档的每个 token,得到 token 级别的嵌入向量;然后通过计算每个查询 token 与每个文档 token 之间的最大相似度之和来得到最终分数。
这种方式比 Bi-Encoder 多了 token 级别的交互(更精确),比 Cross-Encoder 少了完整的自注意力计算(更快)。
from colbert.infra import ColBERTSTARTER from colbert.modeling.checkpoint import Checkpoint # 加载 ColBERT 模型 checkpoint = Checkpoint("colbert-ir/colbertv2.0", colbert_config=None) # 编码查询和文档 query_embedding = checkpoint.queryFromText([query]) # 形状: (1, n_query_tokens, dim) doc_embeddings = checkpoint.docFromText(documents) # 形状: (n_docs, n_doc_tokens, dim) # 计算晚交互分数 scores = checkpoint.score(query_embedding, doc_embeddings) # 排序 ranked = sorted(zip(documents, scores.flatten()), key=lambda x: -x[1]) for rank, (doc, score) in enumerate(ranked[:3], 1): print(f" #{rank} [分数={score:.4f}] {doc[:60]}...")
将初检和重排组合成端到端的 Pipeline:
from typing import List, Tuple, Optional import numpy as np import time class RerankPipeline: """ RAG 重排 Pipeline:初检 → 重排 → 截断。 """ def __init__( self, retriever, # 初检检索器(Bi-Encoder) reranker, # 重排模型(Cross-Encoder) initial_top_k: int = 50, # 初检召回数量 final_top_k: int = 5, # 最终返回数量 score_threshold: float = 0.0, # 最低分数阈值 ): self.retriever = retriever self.reranker = reranker self.initial_top_k = initial_top_k self.final_top_k = final_top_k self.score_threshold = score_threshold def retrieve( self, query: str, metadata_conditions: dict = None ) -> List[Tuple[int, float, str]]: """ 完整的两阶段检索流程。 Returns: [(doc_id, rerank_score, doc_text), ...] 按重排分数降序 """ start_time = time.time() # 阶段 1:初检(快速召回) t1 = time.time() query_vector = self.retriever.embed(query) initial_results = self.retriever.search( query_vector, top_k=self.initial_top_k ) retrieve_time = time.time() - t1 # 阶段 2:重排(精细排序) t2 = time.time() if self.reranker and len(initial_results) > 0: doc_ids = [r[0] for r in initial_results] doc_texts = [self.retriever.get_doc_text(did) for did in doc_ids] pairs = [[query, text] for text in doc_texts] scores = self.reranker.compute_score( pairs, batch_size=16, max_length=512 ) # 按重排分数排序 reranked = sorted( zip(doc_ids, scores, doc_texts), key=lambda x: -x[1] ) # 过滤低分文档 if self.score_threshold > 0: reranked = [ r for r in reranked if r[1] >= self.score_threshold ] else: reranked = [ (r[0], r[1], self.retriever.get_doc_text(r[0])) for r in initial_results ] rerank_time = time.time() - t2 total_time = time.time() - start_time # 返回 Top-K final_results = reranked[:self.final_top_k] # 打印性能日志 print(f"[Pipeline] 初检: {retrieve_time*1000:.0f}ms | " f"重排: {rerank_time*1000:.0f}ms | " f"总计: {total_time*1000:.0f}ms | " f"候选: {len(initial_results)} → {len(final_results)}") return final_results # 使用示例 pipeline = RerankPipeline( retriever=dense_retriever, reranker=FlagReranker('BAAI/bge-reranker-v2-m3', use_fp16=True), initial_top_k=50, final_top_k=5, ) results = pipeline.retrieve("如何优化 RAG 系统的检索准确率") for rank, (doc_id, score, text) in enumerate(results, 1): print(f" #{rank} [doc={doc_id}, score={score:.4f}] {text[:80]}...")
Pipeline 设计要点:
initial_top_k=50 是一个经验值。太小(如 10)会丢掉可能被重排模型提上来的文档;太大(如 200)会增加重排延迟。在 10 万级文档库中,50 通常足够score_threshold 用于过滤低质量结果。建议先不加阈值,观察分数分布后再设定合理的截断点引入重排后,必须用数据验证它确实带来了提升。以下是使用 RAGAS 框架评估重排前后效果的完整流程:
from ragas import evaluate from ragas.metrics import ( context_precision, context_recall, faithfulness, answer_relevancy, ) from datasets import Dataset # 构建评估数据集(建议至少 20-50 个真实查询) eval_data = { "question": [ "RAG 系统中如何选择向量数据库", "什么是 HyDE 检索方法", "如何减少 LLM 的幻觉问题", ], "answer": ["实际生成的回答1", "实际生成的回答2", "实际生成的回答3"], "contexts": [ ["检索到的文档1", "检索到的文档2"], ["检索到的文档A"], ["检索到的文档X", "检索到的文档Y", "检索到的文档Z"], ], "ground_truth": [ "标准答案1(人工标注)", "标准答案2", "标准答案3", ], } dataset = Dataset.from_dict(eval_data) result = evaluate(dataset, metrics=[ context_precision, # 检索精度:送入 LLM 的上下文有多少是相关的 context_recall, # 检索召回:相关信息被检索到的比例 faithfulness, # 忠实度:回答是否基于检索到的上下文 answer_relevancy, # 相关性:回答是否切题 ]) for metric_name, value in result.items(): print(f" {metric_name}: {value:.4f}")
解读指标:
一个实用的 A/B 测试框架:在上线前,对同一批查询分别跑"无重排"和"有重排"两条 Pipeline,对比四个指标。只有当 context_precision 提升 >5% 或 faithfulness 提升 >3% 时,才值得在生产环境开启重排(考虑到额外的延迟开销)。这个阈值不是绝对的——如果你的业务对准确性要求极高(如医疗、法律),即使 2% 的提升也值得。
A:可以微调,且在有标注数据的情况下效果提升明显(NDCG@5 通常提升 5-15%)。微调数据格式为 (query, positive_doc, negative_doc) 三元组。建议至少准备 1000-5000 个高质量标注对。BGE-Reranker 基于 XLM-RoBERTa 架构,微调方法和普通的文本分类微调类似。
A:Top-K 设 30-50 是最佳平衡点。重排模型(Cross-Encoder)的计算复杂度是 O(K × seq_len²),100 个候选在 GPU 上约需 200ms,50 个约 100ms。如果 latency 允许,可以适当增加。但经验上超过 50 个候选后,Top-5 的质量提升迅速递减(边际效应)。
A:可以,但会很慢。BGE-Reranker-base 在 CPU 上处理一个 (query, doc) 对约需 20-50ms,50 个候选需要 1-2.5 秒。解决方案:使用 API 服务(Cohere/Jina)、用 ONNX Runtime 优化 CPU 推理、或使用 ONNX 量化后的模型(体积和速度都有改善)。
A:不需要,也不建议这样做。初检和重排是独立的两个阶段,用各自领域最好的模型即可。比如初检用 BGE-large(嵌入质量高),重排用 bge-reranker-v2-m3(重排专精)。甚至初检用 OpenAI 的嵌入,重排用 Cohere API,组合起来也能工作得很好。
本节系统讲解了 RAG 系统中的重排机制。我们从 Bi-Encoder 与 Cross-Encoder 的本质区别出发,理解了为什么需要两阶段检索架构;接着实现了三种主流的重排方案——BGE-Reranker 本地部署、Cohere Rerank API 调用和 ColBERT 晚交互模型;最后将它们整合成一个完整的重排 Pipeline。
核心认知:重排是 RAG 系统中投入产出比最高的优化手段之一。它的实现相对简单(几十行代码),但对最终答案质量的提升却非常显著。如果你只能做一个优化来提升 RAG 系统的效果,我建议优先加重排。下一节 4.4 将讲解混合检索方法,探索如何将稠密检索与稀疏检索深度融合,进一步扩大召回覆盖面。
关键词:RAG 知识库实战, 重排模型, Cross-Encoder, BGE-Reranker, Cohere Rerank, ColBERT, 检索优化, 精排
难度:进阶
预计阅读:20 分钟