大模型 RAG(检索增强生成) 已成为企业落地大模型的标配,但很多团队仍停留在「文本切块 + 向量检索」的朴素方案,面对专有名词、代码、长尾问题时召回准确率上不去,最终答案答非所问。本文讲透 混合检索(Hybrid Search) 与 重排序(Rerank) 两大进阶手段,并给出可直接抄的代码与评估方法,帮你把 RAG 的命中率从「能用」拉到「好用」。
一、为什么朴素向量检索不够用
纯向量检索把 query 和文档都映射成语义向量,靠余弦相似度召回。它的短板非常具体:
- 专有名词与 ID 不敏感:模型容易把「订单号 80231」「error_code=5099」这类强关键字语义化掉,导致完全匹配却排不到前面。
- 长尾 query 噪声大:低频但精确的问题,向量空间里可能离一堆语义相近但事实不符的文档更近。
- 可解释性差:召回结果没法用关键词命中来解释,排查 badcase 时两眼一抹黑。
解决思路不是抛弃向量,而是「向量负责语义泛化,关键词负责精确命中」,两者融合,再用重排序做最后一道精排。
二、混合检索:BM25 + 向量的双路召回
1. BM25 关键词召回
BM25 是基于词频与逆文档频率的经典算法,对精确关键词命中极其敏感。用 rank_bm25 几行就能跑起来:
from rank_bm25 import BM25Okapi
docs = ["如何在 Nginx 配置反向代理", "PostgreSQL 慢查询优化方法", ...]
tokenized = [d.split() for d in docs]
bm25 = BM25Okapi(tokenized)
query = "Nginx 反向代理 配置".split()
scores = bm25.get_scores(query)
top_bm25 = [docs[i] for i in sorted(range(len(scores)), key=lambda x: -scores[x])[:10]]
2. 向量语义召回
向量路负责「换个说法也能找到」的泛化能力,常用 BGE、bge-m3 等中文 embedding 模型,配合 PG 向量字段或 Chroma 做 ANN 检索:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-base-zh")
q_emb = model.encode("Nginx 怎么把请求转发到后端服务")
# 在向量库里做最近邻检索(PG /v2 或 Chroma 的 query API)
# 返回 top_k 语义相似文档
3. 分数融合:RRF 与加权归一
两路分数量纲不同,不能直接相加。工业界最稳的是 RRF(Reciprocal Rank Fusion):只看排名不看原始分数,避免量纲冲突。
def rrf(rank_lists, k=60):
score = {}
for ranks in rank_lists: # 每个元素是某路召回的 doc 排名列表
for rank, doc_id in enumerate(ranks):
score[doc_id] = score.get(doc_id, 0) + 1.0 / (k + rank + 1)
return sorted(score.items(), key=lambda x: -x[1])
# 两路召回的 top 列表融合
fused = rrf([bm25_top_ids, vector_top_ids])
三种策略的取舍一目了然:
| 召回策略 | 精确关键词 | 语义泛化 | 长尾稳健性 |
|---|---|---|---|
| 纯向量检索 | 弱 | 强 | 中 |
| 纯 BM25 | 强 | 弱(同义词失效) | 强 |
| 混合检索 + RRF | 强 | 强 | 强 |
三、重排序 Rerank:让 Top-K 真正相关
Cross-Encoder 原理
召回阶段为了快,用的是 Bi-Encoder(query 和 doc 各自编码)。而 Cross-Encoder 会把 query 和 doc 拼在一起过一次模型,能捕捉细粒度交互,排序质量远高于向量余弦,但慢,所以只放在召回之后对 Top-50/100 做精排。
代码实战:BGE Reranker
from FlagEmbedding import FlagReranker
reranker = FlagReranker("BAAI/bge-reranker-base", use_fp16=True)
# candidate_docs 是混合检索融合后的候选
pairs = [[query, doc] for doc in candidate_docs]
scores = reranker.compute_score(pairs)
ranked = [d for _, d in sorted(zip(scores, candidate_docs), reverse=True)]
final_top3 = ranked[:3] # 送入大模型上下文
经验值:召回 100 条、Rerank 取前 5~8 条喂给 LLM,能在「上下文长度」与「相关度」之间取得最佳平衡。算力紧张时可只对前 30 条重排。
四、工程落地:在 Dify 中接入混合检索
如果你用 Dify + Ollama 自建本地知识库,可以在知识库的召回设置里开启「混合检索」与「重排序」开关,无需自己写融合代码。底层正是 BM25 与向量双路 + Rerank 的组合,调参重点是:向量检索 Top-K、关键词权重、Rerank 模型选型。自建场景下建议优先用本地 reranker,避免文档出公网。
五、效果评估:Recall@K 与 MRR
优化不能靠「感觉」,要量化。准备一批带标准答案的 query,统计:
- Recall@K:正确答案是否出现在前 K 条召回里,衡量召回覆盖。
- MRR(平均倒数排名):正确答案排第一的位置的倒数,衡量精排质量。
def recall_at_k(relevant, retrieved, k):
return 1.0 if relevant in retrieved[:k] else 0.0
def mrr(retrieved_list, relevant_ids):
for i, doc in enumerate(retrieved_list):
if doc.id in relevant_ids:
return 1.0 / (i + 1)
return 0.0
混合检索 + Rerank 上线前后,对比这组指标通常能把 Recall@10 提升 15%~30%。
六、生产实践 Checklist
- 切块策略按语义边界切,而非固定字数,保留标题上下文。
- 关键词路用分词后的 BM25,中文务必先切词。
- 融合优先 RRF,再考虑加权;加权需要在线标定。
- Rerank 只对召回 Top-N 跑,控制时延与成本。
- 每个版本变更都要跑 Recall@K / MRR 回归,防止「越改越差」。
混合检索与重排序是 RAG 从 demo 走向生产的分水岭。先把双路召回搭起来,再用 Rerank 收口,最后用指标驱动迭代,你的知识库问答准确率会肉眼可见地往上走。




