Reranking 与 Hybrid Search:让 RAG 检索精度再上一个台阶
从工程实战角度系统讲解 RAG 两阶段检索:Reranker 模型选型、Hybrid Search 融合策略、Qdrant sparse+dense 落地、离线评估集设计,让生产级 RAG 召回率从 60% 提升到 90%。
Reranking 与 Hybrid Search:让 RAG 检索精度再上一个台阶
基础的向量检索在概念上很美——把 query 和文档都映射到同一个语义空间,用余弦相似度找出"最相关"的 top-k。但生产环境里,单纯依靠 dense embedding 经常让 RAG 系统在两个维度上失败:第一是关键词失配(query 里出现的产品型号、专有名词、缩写,embedding 模型没有见过);第二是 top-k 排序错位(前 10 个里相关文档散落在第 6、8、9 位,最相关的反而在第 12)。这两个问题的解法都是 Reranking + Hybrid Search。本文从工程实战出发,系统对比 Reranker 模型选型、Hybrid 融合策略和两阶段检索的端到端设计。
为什么 Embedding 检索不够用
Dense embedding 在语义相似度上表现优异,但在"零样本专有名词"和"高频术语"上常常失灵。原因有三层:
第一,embedding 模型的训练数据偏差。主流 embedding 模型(bge、cohere-embed、openai-text-embedding)的训练语料是通用网页和对话,对于企业内部的产品型号、API 名称、行业术语的覆盖率很低。一个"RAG-1024-Flash 存储卡"的 query 可能在 embedding 空间里和"RAG 是检索增强生成"距离很近——它们在文本上确实相似,但业务含义完全不同。
第二,top-k 排序对召回率敏感。当一个文档库里有 10000 个 chunks,query 真正相关的可能只有 5-15 个。如果只用 dense retrieval,top-10 命中率常常只有 50-70%。让 LLM 看到 10 个 chunks 里有 4-5 个不相关的内容,会严重干扰答案生成质量。
第三,长文档的 chunk 切分引入噪声。即使 query 真正相关的段落是 chunks[42],embedding 检索可能因为 chunks[42] 周围的 chunks[41] 和 chunks[43] 出现在检索结果前列而把真正相关的"埋没"。
这三类问题都不能靠"换更好的 embedding 模型"解决——Reranking 和 Hybrid Search 是结构性方案。
Reranker 模型选型
Reranker 是 cross-encoder 架构的模型:它把 query 和每个候选文档作为一对输入,输出一个 0-1 的相关性分数。Cross-encoder 比 bi-encoder(embedding 模型)慢得多(每个 query-document 对都要过一遍完整 transformer),但精度高一个数量级。
# BGE Reranker v2 推理
from FlagEmbedding import FlagReranker
reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)
query = "RAG-1024-Flash 存储卡的保修期"
candidates = [
"本产品保修期 3 年,闪存颗粒保 5 年。",
"我们提供 7x24 客服支持。",
"RAG 是检索增强生成技术。",
"公司成立于 2015 年,总部位于深圳。",
]
pairs = [[query, doc] for doc in candidates]
scores = reranker.compute_score(pairs, normalize=True)
# scores = [0.95, 0.21, 0.03, 0.08]
主流 Reranker 模型对比:
| 模型 | 上下文长度 | 多语言 | 推理速度 | 适合场景 |
|---|---|---|---|---|
| bge-reranker-v2-m3 | 8192 | 中英 | 中 | 通用 |
| bge-reranker-large | 512 | 英 | 快 | 英文短文档 |
| cohere-rerank-3 | 4096 | 多 | 快(SaaS) | 生产环境 |
| mixedbread-ai rerank | 4096 | 多 | 中 | 高精度需求 |
| Jina Reranker | 8192 | 多 | 中 | 多语言混合 |
选型原则:
- 短文档(<500 tokens):
bge-reranker-large速度优先 - 中长文档 + 多语言:
bge-reranker-v2-m3平衡 - 不想自托管:Cohere Rerank 3 / Jina Rerank(SaaS,按 query 计费)
- 极致精度:mixedbread-ai rerank 或自训练 cross-encoder
Reranking 在 RAG 流水线中的位置
两阶段检索是当前 RAG 系统的标准架构:
# 阶段 1: Bi-encoder 召回
from sentence_transformers import SentenceTransformer
import numpy as np
embedder = SentenceTransformer("BAAI/bge-m3")
query_emb = embedder.encode(query)
candidate_embs = embedder.encode(all_chunk_texts)
similarities = np.dot(candidate_embs, query_emb)
top_50_indices = np.argsort(similarities)[::-1][:50]
# 阶段 2: Cross-encoder 重排
top_50_chunks = [all_chunk_texts[i] for i in top_50_indices]
pairs = [[query, chunk] for chunk in top_50_chunks]
rerank_scores = reranker.compute_score(pairs, normalize=True)
top_5_indices = np.argsort(rerank_scores)[::-1][:5]
final_chunks = [top_50_chunks[i] for i in top_5_indices]
两阶段架构的关键设计:
- 召回阶段(Stage 1):高召回率,使用快速 bi-encoder + 向量索引
- 重排阶段(Stage 2):高精度,使用 cross-encoder 但只处理 top-50/100
- 比例选择:召回 50-100,重排 5-10。召回太少会漏掉相关文档,重排太多会拖慢响应
性能基准:
- Bi-encoder 召回 100 个候选:约 50ms(10万文档)
- Cross-encoder 重排 100 个:约 300ms
- 端到端两阶段检索:约 400ms
- 纯 LLM 答案生成:2-5s
Reranking 引入的 300ms 延迟相对于 LLM 生成的秒级响应是性价比最高的优化之一。
Hybrid Search:向量检索 + 关键词检索
单靠 dense retrieval 解决不了关键词失配问题,Hybrid Search 把 BM25 关键词检索和 dense retrieval 融合:
from rank_bm25 import BM25Okapi
tokenized_corpus = [doc.split() for doc in all_chunk_texts]
bm25 = BM25Okapi(tokenized_corpus)
bm25_scores = bm25.get_scores(query.split())
bm25_max = max(bm25_scores) if max(bm25_scores) > 0 else 1
bm25_normalized = [s / bm25_max for s in bm25_scores]
dense_max = max(similarities) if max(similarities) > 0 else 1
dense_normalized = [s / dense_max for s in similarities]
hybrid_scores = [
0.7 * d + 0.3 * b
for d, b in zip(dense_normalized, bm25_normalized)
]
融合策略对比:
| 策略 | 公式 | 优势 | 劣势 |
|---|---|---|---|
| 线性加权 | 0.7 * dense + 0.3 * bm25 | 简单直观 | 权重难调 |
| Reciprocal Rank Fusion | sum(1 / (k + rank)) | 无需归一化 | 忽略分数绝对值 |
| 倒数排名加权 | alpha / rank_dense + (1-alpha) / rank_bm25 | 平滑 | 需调 k |
RRF 是工业界最常用的融合策略:
def rrf(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking):
scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: -x[1])
Qdrant 内置的 Hybrid Search
Qdrant 是少数原生支持 hybrid search 的向量数据库,底层用 sparse vectors 实现 BM25:
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, VectorParams, SparseVectorParams, Distance
client = QdrantClient("localhost", port=6333)
client.create_collection(
collection_name="hybrid_demo",
vectors_config={
"dense": VectorParams(size=1024, distance=Distance.COSINE),
},
sparse_vectors_config={
"sparse": SparseVectorParams(),
},
)
client.upsert(
collection_name="hybrid_demo",
points=[
PointStruct(
id=1,
vector={
"dense": dense_vector,
"sparse": {"indices": [42, 108, 256], "values": [0.5, 0.3, 0.2]},
},
payload={"text": "..."},
),
],
)
from qdrant_client.models import FusionQuery, Prefetch
results = client.query_points(
collection_name="hybrid_demo",
prefetch=[
Prefetch(query=dense_vector, using="dense", limit=50),
Prefetch(query=sparse_vector, using="sparse", limit=50),
],
query=FusionQuery(fusion="rrf"),
limit=10,
)
Qdrant 的优势是 sparse + dense 检索在同一个 vector index 完成,无需额外维护 BM25 索引。
端到端两阶段 + Hybrid 检索架构
生产级 RAG 的完整检索链:
class HybridRerankRetriever:
def __init__(self, embedder, reranker, qdrant_client, collection_name):
self.embedder = embedder
self.reranker = reranker
self.qdrant = qdrant_client
self.collection = collection_name
async def retrieve(self, query, top_k_final=5, recall_k=50):
dense_query = self.embedder.encode(query).tolist()
sparse_query = self._text_to_sparse(query)
candidates = self.qdrant.query_points(
collection_name=self.collection,
prefetch=[
Prefetch(query=dense_query, using="dense", limit=recall_k),
Prefetch(query=sparse_query, using="sparse", limit=recall_k),
],
query=FusionQuery(fusion="rrf"),
limit=recall_k,
with_payload=True,
)
candidate_texts = [p.payload["text"] for p in candidates.points]
pairs = [[query, text] for text in candidate_texts]
rerank_scores = self.reranker.compute_score(pairs, normalize=True)
scored = list(zip(candidates.points, rerank_scores))
scored.sort(key=lambda x: -x[1])
return scored[:top_k_final]
性能监控:
- 召回率:top-50 召回的命中率(用离线评估集)
- 重排提升度:rerank 后 top-5 命中率比直接 top-5 提升多少
- 端到端延迟:retrieve + rerank 的总耗时
离线评估:怎么验证 Reranking 真的有效
不要凭感觉调权重或选 Reranker——用离线评估集量化效果:
eval_set = [
{"query": "RAG-1024-Flash 保修", "relevant_doc_ids": [42, 108]},
{"query": "如何重置管理员密码", "relevant_doc_ids": [201, 205, 230]},
]
def evaluate(retriever, eval_set, k=10):
hits = 0
mrr_sum = 0
for item in eval_set:
results = retriever.retrieve(item["query"], top_k_final=k)
result_ids = [r.id for r in results]
if any(rid in item["relevant_doc_ids"] for rid in result_ids):
hits += 1
for i, rid in enumerate(result_ids):
if rid in item["relevant_doc_ids"]:
mrr_sum += 1 / (i + 1)
break
return {
"recall_at_k": hits / len(eval_set),
"mrr": mrr_sum / len(eval_set),
}
metrics = evaluate(retriever, eval_set, k=5)
print(f"Recall@5: {metrics['recall_at_k']:.3f}, MRR: {metrics['mrr']:.3f}")
典型改进幅度:
- 纯 dense retrieval: Recall@5 = 0.62
- Reranker: Recall@5 = 0.81
- Hybrid + Reranker: Recall@5 = 0.88
每次 Reranker 选型或权重调整,都跑一次评估集,看到提升才上线。
实施路径
第 1 周:在现有 dense retrieval 基础上加入 Reranker 阶段,对比 recall@5 指标。第 2 周:加入 BM25 或 Qdrant sparse 检索,引入 RRF 融合。第 3 周:构建 100-200 条的离线评估集,覆盖核心业务 query。第 4 周:建立 recall 监控仪表盘,对召回率下降发出告警。第 5 周:测试多个 Reranker 模型,选定主备。第 6 周:把端到端检索延迟 P95 控制在 500ms 以内。
总结
Reranking 解决排序精度问题,Hybrid Search 解决召回完整性问题。两者叠加是当前 RAG 检索的最佳实践:用 dense + sparse 做高召回,用 Reranker 做精排序。Qdrant、Milvus 等现代向量数据库都内置了 sparse + dense 融合,让两阶段检索的实现成本大幅降低。
但所有这些优化都离不开离线评估集——没有评估集就没有量化指标,就只能凭感觉调,最终会在生产环境的长尾 query 上失守。
参考工具:Qdrant(原生支持 sparse + dense 融合的向量数据库)、FlagEmbedding (BGE)(bge-m3 embedding + bge-reranker-v2-m3 一站式)、RAGatouille(ColBERT 风格的 late interaction 检索)、TrustRAG(可解释 RAG 框架)和 Mixedbread AI(高精度多语言 Reranker)覆盖了 Reranking 工具链的核心节点。
本文涉及的项目
Qdrant
32.8k ⭐Qdrant 是一个用 Rust 编写的高性能、云原生向量数据库与向量搜索引擎,专为下一代 AI 应用提供大规模 ANN 检索能力。
FlagEmbedding
11.9k ⭐智源研究院开源的 BGE 系列嵌入模型与检索工具,提供业界领先的中英文文本嵌入与重排序模型,广泛应用于 RAG 系统和 AI Agent 检索链路。
RAGatouille
3.9k ⭐轻松使用和训练最先进的后期交互检索方法(ColBERT),模块化设计,可将 ColBERT 模型集成到任何 RAG 管道中,显著提升检索精度。
TrustRAG
1.3k ⭐TrustRAG 是一个注重可靠输入与可信输出的 RAG 框架,提供文档解析、分块、检索、重排序等完整 RAG 管线组件,支持多种检索策略和评估方法。
EmbedAnything
1.3k ⭐EmbedAnything 是一个用 Rust 构建的高性能嵌入推理和索引框架,提供模块化、内存安全的 RAG 数据摄取和索引管道,支持本地和云端部署。