向量检索的一个盲区
假设你的知识库里有一篇文档,内容包含这样一句话:
"中文场景推荐使用 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 融合排名
)
EnsembleRetriever 的 weights 参数控制的是各检索器在 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 名、参数名、缩写等精确术语
- 用户查询行为多样(技术用户问精确术语,普通用户问概念)
- 要求高召回率,不能漏掉任何相关文档
什么时候可以只用向量:
- 知识库全是自然语言文本,没有精确术语
- 查询都是语义性的概念问题
- 资源有限,不想引入额外依赖
完整代码
代码已开源:
核心文件:
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
小结
本文通过代码实验对比了三种检索策略:
- 纯 BM25------关键词精确匹配的专家,精确术语场景无敌,但不懂语义
- 纯向量检索------语义理解的专家,概念性问法场景强,但精确术语不如 BM25
- 混合检索(RRF)------两者融合,MRR 全场景最高
RRF 算法的核心思路值得记住:不比分数,只比排名。这使它能够无缝融合任何两个评分体系完全不同的检索器。
生产环境中,混合检索已经是 RAG 系统的标配。Elasticsearch、Qdrant、Weaviate 都原生支持混合检索模式------向量检索+BM25 不再是可选项,而是默认推荐配置。