混合检索与重排序:让 RAG 召回更精准

混合检索与重排序:让 RAG 召回更精准

向量检索很强,但它有一个致命短板:精确关键词搜不到

用户问”订单号 2026012345 的物流状态”,向量检索返回的是一堆物流规则,而不是这条具体订单的结果。

混合检索解决了这个问题。

纯向量检索的三大短板

问题一:精确关键词丢失

用户问”订单号 2026012345 的物流状态”,向量检索可能返回一堆物流规则,但丢失了精确的订单号。

问题二:专有名词和缩写

“RMA 流程”(退货授权)是专有缩写,向量检索可能无法精确匹配。

问题三:数字和编号

“2026 年春节”中的”2026”被理解成语义,丢失精确匹配。

混合检索架构

1
用户提问 → [向量检索 + 关键词检索] → RRF 融合 → 最终排序

向量检索 vs 关键词检索

维度 向量检索 关键词检索
擅长场景 语义理解、同义词 精确关键词
典型 query “买了一周的东西还能退吗” “订单号 2026012345”
短板 精确关键词不敏感 无法理解语义

两者互补,缺一不可。

BM25 算法

核心思想

统计词频(TF)和逆文档频率(IDF),计算关键词重要性:

  • 词频(TF):出现越多越相关,但有上限(避免长文档占优)
  • 逆文档频率(IDF):越稀有的词越有区分度
  • 长度归一化:长短文档公平竞争

BM25 是关键词检索的事实标准,效果好且计算高效。

RRF(倒数排名融合)

核心公式

$$RRF(d) = \sum_{i=1}^{n} \frac{1}{k + rank_i(d)}$$

其中 k=60(通常固定),rank_i(d) 是第 i 路检索中文档 d 的排名。

为什么有效

  • 不依赖分数本身,只看排名(避免两路分数不可比的问题)
  • 在两路检索中排名都靠前的结果分数更高
  • 对异常高分不敏感(不会因为某一路分数特别高而被主导)

示例

1
2
3
4
5
6
7
向量检索 Top 5:  [文档A, 文档B, 文档C, 文档D, 文档E]
BM25检索 Top 5: [文档B, 文档F, 文档A, 文档G, 文档H]

RRF 得分:
文档A: 1/(60+1) + 1/(60+3) = 0.0321 ← 两路都靠前
文档B: 1/(60+2) + 1/(60+1) = 0.0317 ← 两路都靠前
文档C: 1/(60+3) + 1/(60+∞) = 0.0159 ← 只在一路出现

两阶段检索策略

粗检索:向量 + BM25 → Top 20~50

快速从百万文档中召回候选集,速度优先。

精排序:Reranker → Top 5

对候选集进行精细排序,精度优先。

Reranking:重排序

Bi-Encoder vs Cross-Encoder

类型 特点 适用场景
Bi-Encoder query 和 chunk 分别独立编码,速度快 初检阶段
Cross-Encoder 拼接后一起编码,精度高,能建模交互关系 重排序阶段
1
2
3
4
5
6
7
8
9
Bi-Encoder:
query → [向量A]
chunk → [向量B]
相似度 = cosine(向量A, 向量B)

Cross-Encoder:
[query + chunk] → 直接输出相似度分数
✓ 可以建模 query 中每个词与 chunk 中每个词的交互
✓ 更准确,但无法预先计算索引

主流 Reranker 模型

模型 中文效果 适用场景
BGE-reranker-v2-m3 稳定 预算敏感
Qwen3-Reranker-8B 优秀 复杂意图排序
Cohere rerank-v4.0 一般 国际化场景

中文推荐:BGE-reranker-v2-m3(性价比)或 Qwen3-Reranker-8B(复杂场景)

完整检索流程

1
2
用户问题 → 向量化 → 混合检索(向量+BM25)→ RRF融合 
→ Top-20候选 → Reranker重排序 → Top-5最终结果 → LLM生成回答

每一步都有明确的工程价值:混合检索扩大召回面,RRF 融合平衡两路,Reranker 做最终精细排序。

实战:Python 实现混合检索 + Reranker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pymilvus import MilvusClient
from sentence_transformers import CrossEncoder

client = MilvusClient(uri="http://localhost:19530")
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

def hybrid_search(query: str, top_k: int = 5):
# 1. 向量检索
query_vector = embedding_model.encode([query])
vector_results = client.search(
collection_name="rag_docs",
data=query_vector,
limit=20,
output_fields=["text", "doc_name"]
)

# 2. BM25 关键词检索(简化实现,实际用 Elasticsearch/Whoosh)
bm25_results = bm25_engine.search(query, top_k=20)

# 3. RRF 融合
rrf_scores = {}
for rank, doc in enumerate(vector_results[0]):
doc_id = doc["id"]
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (60 + rank + 1)
for rank, doc in enumerate(bm25_results):
doc_id = doc["id"]
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (60 + rank + 1)

# 取 Top-20 RRF 候选
top20_ids = sorted(rrf_scores, key=rrf_scores.get, reverse=True)[:20]

# 4. Reranker 精排
top20_texts = [get_text_by_id(id) for id in top20_ids]
pairs = [[query, text] for text in top20_texts]
rerank_scores = reranker.predict(pairs)

# 最终 Top-5
final_results = sorted(zip(top20_ids, rerank_scores), key=lambda x: x[1], reverse=True)[:top_k]
return final_results

RAG 系列总结

7 篇文章,覆盖 RAG 全链路:

环节 关键技术
文档解析 Apache Tika
数据分块 递归分块、语义分块
向量化 BGE-M3、Qwen3-Embedding
向量存储 Milvus、HNSW 索引
混合检索 BM25 + 向量 + RRF
重排序 BGE-reranker、Cross-Encoder
元数据 引用生成、权限控制

相关阅读: