RAG 检索精度上不去?混合检索 + 重排,效果立竿见影(附实现思路)
RAG 检索精度上不去?混合检索 + 重排,效果立竿见影(附实现思路)
做 RAG 的人,都经历过同一个尴尬:向量检索"看着像"但"答不对"。
问"订单 A2024-001 的状态",向量检索召回一堆无关文档——因为向量擅长语义,不擅长精确匹配编号;问"年假几天",纯关键词又召回不到同义表达。解法是行业标配的混合检索 + 重排。这篇讲原理、讲实现,附可直接参考的代码思路。

一、纯向量检索为什么不够
向量检索(Embedding + 相似度)的本质是"语义匹配":
"订单 A2024-001 的状态" → 向量 → 找语义相近的块
它有两个硬伤:
| 场景 | 向量检索表现 | 原因 |
|---|---|---|
| 语义相同、用词不同("年假" vs "带薪休假") | ✅ 擅长 | 语义空间里本来就接近 |
| 精确编号/型号/人名("A2024-001"、"OSPF") | ❌ 经常失败 | 编号没有"语义梯度",向量彼此等距 |
| 短查询、少上下文("第 3 条规则") | ❌ 召回分散 | 信息量太少,语义锚点不足 |
所以纯向量检索,是"够用但不好用"——最致命的就是精确匹配类问题,而这恰恰是企知识库、工单、法务合同里最高频的查询。
二、混合检索:两路召回,各取所长
思路:BM25(关键词)管精确匹配,向量(语义)管同义表达,两路结果融合。
BM25 是老牌关键词检索算法,按词频和逆文档频率打分,对"完整编号、专有名词、术语"命中极准,但理解不了同义词。

融合算法 RRF(Reciprocal Rank Fusion),核心就一行逻辑——不依赖两路各自的分值量纲(BM25 分和余弦分根本不可比),只信"排名":
# 常数 k 默认 60,越大表示越"平滑"两路的排名差异
def rrf_score(rank: int, k: int = 60) -> float:
return 1.0 / (k + rank) # rank 从 1 开始
def fuse(bm25_results, vector_results, k: int = 60, top_k: int = 10):
"""两路结果都是 [(doc_id, rank), ...],rank 从 1 开始"""
fused = {}
for doc_id, rank in bm25_results:
fused[doc_id] = fused.get(doc_id, 0.0) + rrf_score(rank, k)
for doc_id, rank in vector_results:
fused[doc_id] = fused.get(doc_id, 0.0) + rrf_score(rank, k)
# 按总分降序取 Top-K
return sorted(fused.items(), key=lambda x: x[1], reverse=True)[:top_k]
关键点:精确编号类问题,BM25 路命中;同义表达类问题,向量路命中;两路都命中的文档排名最高——这就是"1+1>2"。RRF 的好处是不用纠结两路分数怎么归一化,直接按排名融合,工程上最省心。
三、实现要点:BM25 路的中文问题
BM25 按词频统计,中文必须先分词,否则"订单/编号/状态"会被切成单字,命中率大降。这一步做错,混合检索直接退化成"半残"。
import jieba
def tokenize(text: str) -> list[str]:
# 索引和查询必须用同一套分词,否则词表对不上
return [w for w in jieba.cut(text) if w.strip()]
corpus_tokens = [tokenize(chunk) for chunk in chunks]
query_tokens = tokenize(question)
推荐用 bm25s 库(轻量、纯 Python,比自己写快很多),索引和检索都几行:
import bm25s
bm25 = bm25s.BM25()
bm25.index(corpus_tokens) # 建索引
results = bm25.retrieve(query_tokens, k=20) # 检索 Top-20,返回 (doc_ids, scores)
小坑:专有名词(如 "A2024-001")建议加一层"不过词"保护,或检索前对查询里的编号做正则提取、直接做子串匹配兜底,能再救回一批精确命中。
四、进阶:重排(Re-Rank)——融合后再精排
混合检索出了 Top-50,还要不要更准?加重排:用一个模型对候选逐条打分,精挑 Top-5。
混合检索 Top-50 → 重排模型打分 → 精排 Top-5 → 喂给大模型
重排的价值:先广撒网(召回多),再精挑(保精度)。推荐用交叉编码器类模型(如 bge-reranker),它对"问题-文档"这一对 jointly 编码打分,比纯向量相似度准一个档次——因为它能看到问题和文档的交互信息,而不是各算各的再比距离。
# 伪代码思路:对每个候选文档,算 (问题, 文档) 的相关性分
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
pairs = [(question, doc) for doc in candidates] # 候选通常是 Top-50
scores = reranker.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
top5 = [doc for doc, _ in ranked[:5]]
工程注意:重排模型比向量模型重,别对全库跑,只对召回后的 Top-50 跑,成本可控;中文场景优先选在中文数据上训过的 reranker。
五、完整流水线(直接参考的架构)
1. 文档切分(按语义块,500-800 字,留 10% 重叠,避免切断关键信息)
2. BM25 索引(jieba 分词)+ 向量索引(Embedding)双路建库
3. 查询:两路召回各 Top-20
4. RRF 融合 → Top-10
5. 重排模型精排 → Top-5
6. 拼进提示词 → 大模型基于资料回答
每一步都能单独评测、单独优化——这也是生产级 RAG 的标准形态。常见踩坑:切块太大会导致召回的块里噪声多、重叠不够会切断上下文,这两点先调,往往比上重排收益还大。
六、我的观点
RAG 检索的真相:没有银弹,只有组合拳。
- 数据量小、问题简单、纯同义查询 → 纯向量就够
- 有精确编号/专有名词/术语(企知识库、工单、合同)→ 必须上混合检索
- 追求极致精度、问答质量直接决定业务结果 → 混合 + 重排
别一开始就上全套——先量问题类型,再决定复杂度。我的经验是:大多数场景,混合检索(不加重排)已经是"性价比拐点",能解决 80% 的"答不对";重排是最后 20% 精度的锦上添花,等召回稳定了再上。
你的 RAG 卡在召回不准还是回答不准?评论区说说你的场景(数据量、有没有编号类查询),我帮你判断该上哪一层。
关注我,下期拆解:RAG 评估:怎么量化"回答得对不对"。
浙公网安备 33010602011771号