RAG 为什么要做混合检索?BM25、向量检索和 RRF 如何配合?
简化版
BM25 擅长精确关键词、编号、专有名词(字面匹配),向量检索擅长语义相近但字面不同的表达(同义改写),两者短板互补。混合检索并行召回两路结果再融合。RRF(Reciprocal Rank Fusion) 按文档在各结果列表中的名次累加倒数分数,不依赖两套原始分数在同一尺度——这是它比「直接加分」稳健的关键。
详细版
- BM25:基于词频、逆文档频率、长度归一化,适合产品型号、错误码、人名等精确匹配;
- Dense Retrieval:用 Embedding 比语义,适合同义改写、自然语言意图、跨字面匹配。
混合检索并行取两路 Top-n,再用加权分数或 RRF 融合。RRF 的典型形式:
score(d) = Σ_i 1 / (k + rank_i(d))
rank_i 是文档在第 i 个列表的名次,k 是平滑常数。文档在多路都靠前,融合分更高。融合后的候选还可交给 Cross-Encoder 重排。
完整版教学
一、纯向量检索为什么不够
向量模型压缩语义,但对罕见编号、版本号、精确短语、拼写细节可能不敏感:
查询"错误码 E1047":
向量检索可能召回 E1052、E1039(语义都是"错误码",但用户要的是字面完全命中的 E1047)
→ 语义相近的其他错误码反而是噪声
向量还可能把「如何开启功能」和「如何关闭功能」判得很近(主题相同、动作相反)。关键词和结构化过滤能补这些细粒度差异。
二、BM25 为什么仍然重要
BM25 不是简单数关键词次数,它有三个机制:逆文档频率(IDF)提高稀有词权重、饱和函数限制重复词收益、按文档长度归一化。它可解释、成熟、无需训练,在专有词密集场景经常非常强。
但 BM25 不理解同义词——「退款步骤」和「如何把钱退回来」若无共同词就可能匹配不到,所以需要向量召回补充。二者是天生互补关系。
三、为什么不能直接相加原始分数
这是 RRF 存在的理由,也是高频考点:
BM25 分数范围可能是 0~30,余弦相似度是 0~1
甚至同一算法在不同查询上尺度也变
→ 直接相加:BM25 的 25 分会碾压向量的 0.9,某一路"无意中支配"结果
可以先归一化再加权,但归一化对异常值敏感。RRF 只看排名、不依赖分数尺度,工程上稳健又好实现。
四、RRF 的计算与权衡(带数字)
看一个例子(取 k=60):
文档 D 在 BM25 列表排第 1,在向量列表排第 3:
score(D) = 1/(60+1) + 1/(60+3) = 0.0164 + 0.0159 = 0.0323
文档 E 只在向量列表排第 1,BM25 未召回:
score(E) = 1/(60+1) = 0.0164
→ 两路都靠前的 D 胜出,但只在一路靠前的 E 也被保留
平滑常数 k 越大,不同名次的分差越小。RRF 不学习业务相关性——有充足标注数据时可训练 Learning-to-Rank 融合更多特征。要点:融合前各路要召回比最终 Top-k 更多的候选(否则融合空间太小),去重用稳定的文档/chunk ID。
五、典型生产链路
1. 查询规范化 + 提取权限/时间等过滤条件
2. BM25 与向量索引【并行】召回各自 Top-n
3. RRF 或校准后的加权分数融合
4. 去重、按元数据过滤
5. Cross-Encoder Reranker 精排
6. 选满足 token 预算且有多样性的上下文
混合检索的价值来自互补召回,不来自“两个分数相加”;融合前必须先明确排名口径、候选去重和验证集查询分布。
六、常见误区与追问
- 误区:有了向量检索就不需要 BM25。 向量对编号/版本号/精确短语不敏感,BM25 在专有词场景常更强,二者互补。
- 误区:两路分数直接相加就行。 BM25 和余弦尺度/分布不同,直接加会让一路支配结果,要用 RRF 或校准加权。
- 误区:混合检索就是把两路 Top-k 都塞进 prompt。 还要融合、去重、重排、上下文去重,不是简单拼接。
- 误区:RRF 会学业务相关性。 它只按名次投票、不学习,要更强融合得上 Learning-to-Rank。
- 追问:BM25 的三个核心机制? IDF 提稀有词权重、饱和函数限重复词收益、长度归一化。
- 追问:RRF 为什么比加分稳健? 只看排名不依赖分数尺度,避免某一路因尺度大而支配。
- 追问:融合前各路要召回多少? 比最终 Top-k 更多的候选,否则融合空间太小。
- 追问:混合检索一定更好吗? 不一定每类查询都更好,可按查询类型(编号/实体/自然语言/多跳)分桶评估甚至做查询路由。
七、检索信号判断表
| 信号 | 擅长 | 容易漏掉 |
|---|---|---|
| BM25 | 精确词、编号、专名 | 同义改写 |
| 向量检索 | 语义近似、自然语言改写 | 稀有精确词 |
| RRF 融合 | 合并不同排名尺度 | 不直接利用原始分数 |
| 学习排序 | 按业务训练融合 | 需要可靠标注 |
八、加强记忆
混合检索记「字面准 + 语义近,互补短板」:BM25 擅精确关键词/编号/专有名词(可解释、免训练、IDF+饱和+长度归一化),向量擅同义改写/自然语言意图。融合用 RRF(Σ 1/(k+rank)),只看名次投票、不依赖分数尺度——因为 BM25 和余弦尺度不同,直接加分会让一路支配。核心链路:两路并行召回(比 Top-k 更多)→ RRF 融合 → 去重过滤 → Reranker 精排。混合不保证每类查询都更好,必要时按查询类型分桶评估或路由。
项目实战落地
项目里怎么做的
在《AI Agentic RAG高级企业知识库平台》里,两路召回和融合都由检索策略表 retrieval_strategy 控制:vector_enabled / vector_top_k 管向量路,bm25_enabled / bm25_top_k 管关键词路,rrf_k 是 RRF 的平滑常数,默认 60。管理员在页面上拨开关就能组合出不同策略,检索代码不用改。
- 向量路:问题用 Embedding 模型编码后,去 PostgreSQL + PGVector 里取最近的若干条片段。
- BM25 路:没有引入 Elasticsearch。检索时从 MySQL 的
knowledge_chunk表读出候选片段(按知识库和文档先收窄,一次最多 5000 条),用 jieba 分词、去停用词后交给rank_bm25在内存里打分,分数不大于 0 的直接丢掉。分词和打分是纯 CPU 计算,放进线程执行,不阻塞 FastAPI 的事件循环。 - 型号分词:文档里写
GTE-3000,jieba 切成gte、-、3000;用户输入GTE3000,jieba 切成一个词gte3000,两边一个词都对不上。项目在分词时把字母数字连写的词额外拆一份(gte3000→gte、3000),原词也保留,型号才能命中。 - RRF 融合:每一路的结果各算一份榜单,融合后保留的条数取两路 top_k 中较大的那个。同一片段被两路同时召回时只保留一份内容,来源标成「BM25检索+向量检索」,页面上一眼能看出哪些结果是两路都认可的。
项目预置了三套策略用来对比:「基线-纯向量检索」(向量 top 5)、「进阶-混合检索」(两路各 8 条)和「完整-混合检索加重排加改写」。同一批评测用例上,基线的综合分是 0.7988,完整策略是 0.9616。
为什么这样取舍
- BM25 放在内存里算,而不是上搜索引擎。 项目是本地运行的学习项目,单个知识库的片段量可控,少一个中间件,部署和排错都简单。代价是每次检索都要读候选、临时建索引,所以一定先按知识库和文档收窄范围,并设置候选上限。
- 融合用 RRF,不做加权求和。 向量分是 0 到 1 的余弦相似度,BM25 分没有上界,同一个知识库里可能是 2.3,也可能是 47.8,两个不可比的数加权求和,权重怎么调都说不清道理。
rrf_k做成可配置字段,默认用论文经验值 60,想对比「多路共识优先」和「单路头名优先」时直接在策略里改。
面试官还会追问
- 开了多查询改写之后,一个问题会产生几份榜单一起做 RRF?其中某一份是空的,要不要占融合位置?
- 知识库用了父子分块,BM25 应该对哪些片段建索引,父块要不要参与打分?
- 你怎么证明加了 BM25 这一路真的有用,而不是凭感觉?
学完《AI Agentic RAG高级企业知识库平台》,上面这些追问你都会迎刃而解。