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 都在用它。

核心公式:

scss 复制代码
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) 的做法更优雅:只看排名,不看分数

公式:

scss 复制代码
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)

ini 复制代码
RR = 1/rank(正确文档排在第几位)
MRR = 所有查询的 RR 均值
  • 每次都排第一 → MRR = 1.0
  • 平均排第二 → MRR = 0.5
  • 全部未命中 → MRR = 0.0

三种检索器实现

BM25 检索器

中文要先做分词,用 jieba:

python 复制代码
import jieba
from langchain_community.retrievers import BM25Retriever

def chinese_tokenizer(text: str) -> list[str]:
    return list(jieba.cut(text))

bm25_retriever = BM25Retriever.from_documents(
    docs,
    k=3,
    preprocess_func=chinese_tokenizer,
)

向量检索器

python 复制代码
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

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)

python 复制代码
from langchain_classic.retrievers import EnsembleRetriever

hybrid_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5],   # 两者权重相同,内部用 RRF 融合排名
)

EnsembleRetrieverweights 参数控制的是各检索器在 RRF 中的权重,不是直接加权分数。实际实现里它会对每个检索器的结果排名做加权 RRF 融合。


实验结果

ini 复制代码
======================================================================
  逐条查询结果  (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 汇总:

markdown 复制代码
======================================================================
  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 名、参数名、缩写等精确术语
  • 用户查询行为多样(技术用户问精确术语,普通用户问概念)
  • 要求高召回率,不能漏掉任何相关文档

什么时候可以只用向量:

  • 知识库全是自然语言文本,没有精确术语
  • 查询都是语义性的概念问题
  • 资源有限,不想引入额外依赖

完整代码

代码已开源:

github.com/chendongqi/...

核心文件:

  • hybrid_search.py --- 三种检索策略的完整对比实验

运行方式:

bash 复制代码
git clone https://github.com/chendongqi/llm-in-action
cd 10-hybrid-search
cp .env.example .env   # 填入 Embedding API Key
pip install -r requirements.txt
python hybrid_search.py

小结

本文通过代码实验对比了三种检索策略:

  1. 纯 BM25------关键词精确匹配的专家,精确术语场景无敌,但不懂语义
  2. 纯向量检索------语义理解的专家,概念性问法场景强,但精确术语不如 BM25
  3. 混合检索(RRF)------两者融合,MRR 全场景最高

RRF 算法的核心思路值得记住:不比分数,只比排名。这使它能够无缝融合任何两个评分体系完全不同的检索器。

生产环境中,混合检索已经是 RAG 系统的标配。Elasticsearch、Qdrant、Weaviate 都原生支持混合检索模式------向量检索+BM25 不再是可选项,而是默认推荐配置。


参考资料

相关推荐
冬奇Lab1 小时前
一天一个开源项目(第95篇):Claude for Financial Services - Anthropic 官方金融行业 AI 代理套件
人工智能·开源·资讯
bbsh20991 小时前
AI辅助编程时代,企业级网站系统建设为什么还需要专业平台?
人工智能
05候补工程师1 小时前
[实战复盘] 拒绝 AI 屎山!我从设计模式中学到的“调教”AI 新范式
人工智能·python·设计模式·ai·ai编程
飞Link2 小时前
垂直领域 AI 的曙光:GPT-Rosalind 如何重塑生命科学与药物研发?
人工智能·gpt
一只数据集2 小时前
全尺寸人形机器人灵巧手力觉触觉数据集-2908条ROSbag数据覆盖14大应用场景深度解析
大数据·人工智能·算法·机器人
火山引擎开发者社区2 小时前
火山引擎全面支持 Milvus 2.6 版本:更快、更省、更稳
人工智能
cczixun2 小时前
OpenAI连发GPT-5.5系列:免费版幻觉大降,安全版能力飙升,千亿融资估值直冲8520亿美元
人工智能·gpt·安全
飞Link2 小时前
商汤 SenseNova 6.7 Flash-Lite 深度评测:原生多模态 Agent 的“降本增效”终极方案?
人工智能
飞Link2 小时前
OpenAI 与微软“非排他性”协议解读:AI 云计算市场将迎来百家争鸣?
人工智能·microsoft·云计算