Reranking 与 Hybrid Search:让 RAG 检索精度再上一个台阶

从工程实战角度系统讲解 RAG 两阶段检索:Reranker 模型选型、Hybrid Search 融合策略、Qdrant sparse+dense 落地、离线评估集设计,让生产级 RAG 召回率从 60% 提升到 90%。

AgentList · 2026年7月1日
RAG重排序向量检索混合检索Qdrant

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 工具链的核心节点。