demo 阶段”向量检索 top-5 拼 prompt”能跑通,一到生产环境就露馅:专有名词搜不到、口语问句匹配不上书面语、top-5 里混进看似相关实则无用的段落。召回质量 = 检索召回率 × 排序精度,单靠向量检索两个都拿不满。本文用”BM25 + 向量双路召回 → RRF 融合 → rerank 精排”三段式把召回质量做上去。

前置准备

在已有向量库 RAG 的基础上加装关键词检索与重排模型:

pip install rank-bm25 flag-reranker
# flag-reranker 提供 bge-reranker 系列交叉编码器
  • rank-bm25:纯 Python 的 BM25 实现,小规模语料足够
  • reranker:交叉编码器,把”问题-段落”拼在一起打分,比双塔向量更准但更慢,所以只排候选集

步骤一:双路召回

向量检索抓语义,BM25 抓字面(型号、错误码、人名这类词向量经常糊掉):

from rank_bm25 import BM25Okapi
import jieba

corpus_tokens = [list(jieba.cut(c)) for c in chunks]
bm25 = BM25Okapi(corpus_tokens)

def recall(question, k=20):
vec_hits = vector_store.query(embed(question), top_k=k) # 语义路
kw_ids = bm25.get_top_n(list(jieba.cut(question)), range(len(chunks)), n=k)
kw_hits = [chunks[i] for i in kw_ids] # 字面路
return vec_hits, kw_hits

两路各取 top-20 而不是 top-5:召回阶段宁可冗余,把裁减交给后面的精排。

步骤二:RRF 融合

不用加权分数相加(两路分数量纲不同),用 Reciprocal Rank Fusion 只认排名:

def rrf_fuse(vec_hits, kw_hits, k=60):
score = {}
for rank, doc in enumerate(vec_hits):
score[doc] = score.get(doc, 0) + 1 / (k + rank + 1)
for rank, doc in enumerate(kw_hits):
score[doc] = score.get(doc, 0) + 1 / (k + rank + 1)
return sorted(score, key=score.get, reverse=True)[:10]

同时出现在两路的段落分数叠加,自然浮到前面——这就是”混合”的价值。

步骤三:rerank 精排

交叉编码器对融合后的 10 条逐一打分,取 top-3 进 prompt:

from FlagEmbedding import FlagReranker
reranker = FlagReranker("BAAI/bge-reranker-base", use_fp16=True)

pairs = [[question, doc] for doc in fused]
scores = reranker.compute_score(pairs)
top3 = [doc for _, doc in sorted(zip(scores, fused), reverse=True)[:3]]

注意设置分数阈值(如 >0):全部低分的宁可不注入,避免”没有依据硬答”变成”拿着错依据答”。

步骤四:上线召回率回归

准备 50~100 条”问题-应命中段落”标注集,对比改造前后:

def recall_at_k(pred_docs, gold_doc, k=3):
return gold_doc in pred_docs[:k]

before = sum(recall_at_k(old_pipeline(q), g, 3) for q, g in labeled) / len(labeled)
after = sum(recall_at_k(new_pipeline(q), g, 3) for q, g in labeled) / len(labeled)
print(f"recall@3: {before:.2%} -> {after:.2%}")

recall@3 提升 10 个百分点以上再上线;同时记录端到端延迟增量(rerank 约 +50~150ms)。

常见问题

  • BM25 路毫无贡献:中文没分词或停用词没处理,检查 jieba 分词与索引一致性
  • rerank 成为延迟瓶颈:候选从 10 砍到 8、换 reranker-small、或 fp16/INT8 推理
  • 融合后反而变差:某一路噪声太大(如 BM25 命中大量模板句),给该路加 IDF 过滤或降 k
  • 阈值怎么定:在标注集上看 rerank 分数分布,取”金标准段落最低分”再留 10% 余量

小结

生产级召回是一条漏斗:双路宽召回保覆盖,RRF 融合做无量纲合并,rerank 精排保精度,最后用 recall@k 标注集做上线门禁。每一段都可独立替换(换向量模型、换 ES、换 reranker),但”宽召回 → 融合 → 精排 → 度量”的骨架不变——召回质量的提升来自漏斗设计,而不是某个单点模型。