RAG 系列(十):混合检索——让召回更全面
向量检索的一个盲区
假设你的知识库里有一篇文档,内容包含这样一句话:
“中文场景推荐使用BAAI/bge-large-zh-v1.5,向量维度为 1024。”
用户问:“BAAI/bge-large-zh-v1.5 的向量维度是多少?”
你以为这是送分题——完全一样的词,向量检索应该能轻松找到。
实际上不一定。向量检索依赖语义相似度,当查询和文档的用词高度重叠时,它并不比 BM25 更有优势,有时甚至更差。BM25 算法是专门为精确词频匹配设计的,处理这类问题是它的主场。
真正的问题是:你的 RAG 系统一定会同时遇到两类查询:
- 关键词查询:包含精确的型号、参数、公式、人名——“BAAI/bge-large-zh-v1.5 维度”
- 语义查询:换了一种说法的概念性问题——“AI 助手总是给出过时答案,怎么解决”
纯向量检索擅长后者,但对前者力不从心。纯 BM25 恰好相反。
混合检索(Hybrid Search)的思路很简单:两个都用,再融合结果。
BM25 原理速览
BM25(Best Match 25)是搜索引擎领域的经典排名算法,Elasticsearch、Lucene 都在用它。
核心公式:
score(D, Q) = Σ IDF(qi) × (f(qi, D) × (k1 + 1)) / (f(qi, D) + k1 × (1 - b + b × |D|/avgdl))人话版本:
- IDF(逆文档频率):一个词在所有文档里越罕见,它在匹配时越有价值。"的"不值钱,“BGE-large-zh-v1.5” 非常值钱。
- TF(词频):这个词在文档中出现越多,分数越高,但收益递减。
- 文档长度惩罚:长文档不因词数多而自动获得高分。
BM25 的优势:完全基于词汇,查询词和文档词只要有重叠,就能精准命中。精确型号、产品名、函数名——这是它的主场。
BM25 的劣势:不理解语义。"知识截止问题"和"AI 不知道最新信息"在 BM25 看来毫无关系,尽管它们说的是同一件事。
RRF 融合算法
有了 BM25 和向量检索两份结果,怎么合并?
最简单的思路是把两个分数加权平均,但两种算法的分数尺度完全不同,直接相加没有意义。
RRF(Reciprocal Rank Fusion)的做法更优雅:只看排名,不看分数。
公式:
RRF_score(d) = Σ 1 / (k + rank(d))rank(d):文档 d 在某个检索器中的排名(第 1 名、第 2 名…)k:常数,通常取 60,防止最高排名的文档独占分数- 对每个检索器的排名求和
举例:
| 文档 | BM25 排名 | Vector 排名 | RRF 分数(k=60) |
|---|---|---|---|
| doc-006 | 1 | 3 | 1/(60+1) + 1/(60+3) = 0.0164 + 0.0159 =0.0323 |
| doc-003 | 3 | 1 | 1/(60+3) + 1/(60+1) =0.0323 |
| doc-002 | 2 | 4 | 1/(60+2) + 1/(60+4) = 0.0161 + 0.0156 =0.0317 |
RRF 的好处:无论两个检索器的分数范围差多少,都能公平地基于排名融合,不需要手动对齐分数。
实验设计
6 条测试查询,覆盖两种场景:
| 类型 | 查询 | 期望文档 | 测试点 |
|---|---|---|---|
| 关键词 | BAAI/bge-large-zh-v1.5 维度 | doc-003 | 精确模型名匹配 |
| 关键词 | RRF score sum 1/(k+rank) 公式 | doc-006 | 精确公式字符串 |
| 关键词 | chunk_size 256 1024 overlap 推荐 | doc-004 | 精确参数值 |
| 语义 | AI 助手总是给出过时的答案,有什么方法让它了解最新信息 | doc-001 | 没提 RAG |
| 语义 | 多个团队共用一套问答系统,怎么保证不同团队的资料互相看不到 | doc-008 | 没提多租户 |
| 语义 | 换一种问法,检索结果就完全不同,怎么解决这种不稳定性 | doc-007 | 没提 Multi-Query |
评估指标:MRR(Mean Reciprocal Rank)
RR = 1/rank(正确文档排在第几位) MRR = 所有查询的 RR 均值- 每次都排第一 → MRR = 1.0
- 平均排第二 → MRR = 0.5
- 全部未命中 → MRR = 0.0
三种检索器实现
BM25 检索器
中文要先做分词,用 jieba:
importjiebafromlangchain_community.retrieversimportBM25Retrieverdefchinese_tokenizer(text:str)->list[str]:returnlist(jieba.cut(text))bm25_retriever=BM25Retriever.from_documents(docs,k=3,preprocess_func=chinese_tokenizer,)向量检索器
fromlangchain_chromaimportChromafromlangchain_openaiimportOpenAIEmbeddings embeddings=OpenAIEmbeddings(model="BAAI/bge-large-zh-v1.5",api_key=os.getenv("EMBEDDING_API_KEY"),base_url="https://api.siliconflow.cn/v1",)vectorstore=Chroma.from_documents(docs,embedding=embeddings)vector_retriever=vectorstore.as_retriever(search_kwargs={"k":3})混合检索器(EnsembleRetriever + RRF)
fromlangchain_classic.retrieversimportEnsembleRetriever hybrid_retriever=EnsembleRetriever(retrievers=[bm25_retriever,vector_retriever],weights=[0.5,0.5],# 两者权重相同,内部用 RRF 融合排名)EnsembleRetriever的weights参数控制的是各检索器在 RRF 中的权重,不是直接加权分数。实际实现里它会对每个检索器的结果排名做加权 RRF 融合。
实验结果
====================================================================== 逐条查询结果 (RR = Reciprocal Rank;Hit@1 = 正确文档是否排第一) ====================================================================== [KEYWORD ] BAAI/bge-large-zh-v1.5 维度 期望文档: doc-003 BM25 [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-003', 'doc-006', 'doc-004'] Vector [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-003', 'doc-005', 'doc-002'] Hybrid [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-003', 'doc-006', 'doc-004'] [KEYWORD ] RRF score sum 1/(k+rank) 公式 期望文档: doc-006 BM25 [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-006', 'doc-002', 'doc-004'] Vector [H@1=✗] RR=0.50 | rank=2 | 召回: ['doc-004', 'doc-006', 'doc-003'] Hybrid [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-006', 'doc-004', 'doc-003'] [KEYWORD ] chunk_size 256 1024 overlap 推荐 期望文档: doc-004 BM25 [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-004', 'doc-003', 'doc-006'] Vector [H@1=✗] RR=0.50 | rank=2 | 召回: ['doc-006', 'doc-004', 'doc-003'] Hybrid [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-004', 'doc-006', 'doc-003'] [SEMANTIC] AI 助手总是给出过时的答案,有什么方法让它了解最新信息 期望文档: doc-001 BM25 [H@1=✗] RR=0.33 | rank=3 | 召回: ['doc-007', 'doc-005', 'doc-001'] Vector [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-001', 'doc-005', 'doc-007'] Hybrid [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-001', 'doc-007', 'doc-005'] [SEMANTIC] 多个团队共用一套问答系统,怎么保证不同团队的资料互相看不到 期望文档: doc-008 BM25 [H@1=✗] RR=0.33 | rank=3 | 召回: ['doc-002', 'doc-007', 'doc-008'] Vector [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-008', 'doc-001', 'doc-002'] Hybrid [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-008', 'doc-002', 'doc-007'] [SEMANTIC] 换一种问法,检索结果就完全不同,怎么解决这种不稳定性 期望文档: doc-007 BM25 [H@1=✗] RR=0.00 | rank=miss | 召回: ['doc-005', 'doc-001', 'doc-003'] Vector [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-007', 'doc-001', 'doc-005'] Hybrid [H@1=✓] RR=1.00 | rank=1 | 召回: ['doc-007', 'doc-001', 'doc-005']MRR 汇总:
====================================================================== MRR 汇总对比 MRR=1.0 → 每次都排第一;MRR=0.5 → 平均排第二;MRR=0.0 → 全未命中 ====================================================================== 查询类型 BM25 Vector Hybrid 最佳 ──────────────────────────────────────────────────────── 关键词查询 1.000 0.667 1.000 BM25 语义查询 0.222 1.000 1.000 Vector 总体 0.611 0.833 1.000 Hybrid ====================================================================== 结论: ✓ 关键词查询:BM25 MRR 更高(精确词匹配优势) ✓ 语义查询:Vector MRR 更高(语义理解优势) ✓ 混合检索:总体 MRR 最高,兼顾两类查询数字解读:
- BM25 在关键词查询上达到满分 1.000,但在语义查询上只有 0.222——第三条语义查询(“换一种问法”)完全 miss,排名都没有进前三。
- 向量检索在语义查询上完美(1.000),但在关键词查询上只有 0.667——有两条 RRF 公式和 chunk_size 的查询排到了第二名而非第一。
- 混合检索全类型满分 1.000,不仅继承了 BM25 的关键词优势,语义查询也不弱于纯向量。
关键认知:BM25 和向量检索的边界
| 维度 | BM25 | 向量检索 |
|---|---|---|
| 擅长 | 精确词匹配(型号、公式、参数) | 语义理解(同义词、换一种说法) |
| 失效场景 | 查询和文档用词不同 | 精确术语的向量表示不够区分性 |
| 典型查询 | “BERT-base-uncased 层数” | “为什么预训练模型需要微调” |
| 适合语言 | 英文效果更好(中文需分词) | 中英文均可 |
| 计算成本 | 低(无需 GPU,无 API 调用) | 较高(需要 Embedding 调用) |
什么时候一定要上混合检索:
- 知识库里包含产品型号、API 名、参数名、缩写等精确术语
- 用户查询行为多样(技术用户问精确术语,普通用户问概念)
- 要求高召回率,不能漏掉任何相关文档
什么时候可以只用向量:
- 知识库全是自然语言文本,没有精确术语
- 查询都是语义性的概念问题
- 资源有限,不想引入额外依赖
完整代码
代码已开源:
https://github.com/chendongqi/llm-in-action/tree/main/10-hybrid-search
核心文件:
hybrid_search.py— 三种检索策略的完整对比实验
运行方式:
gitclone https://github.com/chendongqi/llm-in-actioncd10-hybrid-searchcp.env.example .env# 填入 Embedding API Keypipinstall-rrequirements.txt python hybrid_search.py小结
本文通过代码实验对比了三种检索策略:
- 纯 BM25——关键词精确匹配的专家,精确术语场景无敌,但不懂语义
- 纯向量检索——语义理解的专家,概念性问法场景强,但精确术语不如 BM25
- 混合检索(RRF)——两者融合,MRR 全场景最高
RRF 算法的核心思路值得记住:不比分数,只比排名。这使它能够无缝融合任何两个评分体系完全不同的检索器。
生产环境中,混合检索已经是 RAG 系统的标配。Elasticsearch、Qdrant、Weaviate 都原生支持混合检索模式——向量检索+BM25 不再是可选项,而是默认推荐配置。
参考资料
- LangChain EnsembleRetriever 文档
- BM25 算法论文:Okapi BM25
- RRF 论文:Reciprocal Rank Fusion
- Qdrant 混合检索文档
