BM25 与稠密向量不是”新旧替代”,而是两种互补的匹配信号。RAG 系统里真正的分水岭,往往不在选哪个模型,而在你有没有把两种信号正确地融合,并且正确地评估。
导语
检索是 RAG 的地基,但工程上最常见的两种失败恰好发生在两个极端:
- 只用 BM25:用户问”怎么退订”,文档里写的是”取消订阅”,一个词都对不上,召回为空;
- 只用向量:用户搜订单号
SO-2024-8871、错误码E1042、函数名parse_args,语义模型把它当成无意义字符串,召回一堆”语义相近但完全无关”的内容。
这两种失败指向同一件事:词法匹配(lexical)和语义匹配(semantic)捕捉的是不同的相关性。前者对精确符号、稀有词、专有名词极强;后者对同义改写、跨语言、模糊描述极强。任何单独使用都会在真实业务里掉坑。
本文的路线:
- 先把 BM25 拆开(它为什么能活 30 年,以及它到底弱在哪);
- 讲稠密检索的范式转变(以及它丢掉了什么);
- 拆 BGE 系列的设计取舍(为什么它的 instruction 前缀不能省、M3 的三合一意味着什么);
- 讲混合检索的三种融合方式与它们的分数尺度问题;
- 给召回/重排的分工与 RAG 的标准配置;
- 最后是工程踩坑与选型。
面向已做过 RAG、被召回率折磨过的读者。默认你熟悉 embedding、ANN 索引和基本的检索评估指标。
1. BM25:一个用了 30 年还没被淘汰的基线
1.1 从 TF-IDF 到概率检索框架
BM25 的血统可以追溯到 1970 年代的 TF-IDF,但它的正式形式来自 Okapi 系统(Robertson、Walker、Jones 等人,City University London,1990 年代 TREC 系列)。理论上的完整表述见 Robertson & Zaragoza 的 The Probabilistic Relevance Framework: BM25 and Beyond(2009)。
它属于概率相关性框架(Probabilistic Relevance Framework):不从”词频加权”的经验公式出发,而是假设”文档与查询相关”是一个概率事件,通过检索概率模型(Binary Independence Model)推导出排序函数。
推导过程略过,但有一个关键结论值得记住:在词项独立假设下,最优排序函数只依赖每个词项在两个分布里的”出现比例差” —— 这就是 IDF 的来源。所以 IDF 不是”拍脑袋觉得稀有词重要”,它是概率模型的产物。
1.2 公式逐项拆解
1 | score(D, Q) = Σ_{i=1}^{n} IDF(q_i) · [ f(q_i, D) · (k1 + 1) ] / [ f(q_i, D) + k1 · (1 − b + b · |D| / avgdl) ] |
| 符号 | 含义 | 作用 |
|---|---|---|
f(q_i, D) |
词项 q_i 在文档 D 中的词频 |
词频信号 |
|D| |
文档长度(词数) | 长度归一化的分子 |
avgdl |
语料平均文档长度 | 长度归一化的基准 |
k1 |
词频饱和参数,通常 1.2(0.9–2.0) | 控制”词频涨到多少就饱和” |
b |
长度归一化强度,通常 0.75(0–1) | b=0 完全不惩罚长文档,b=1 完全按长度线性惩罚 |
IDF(q_i) |
逆文档频率 | 稀有词权重高 |
三个部件,三个直觉:
① IDF:稀有词更重要
Lucene 的平滑形式(保证非负):
1 | IDF(q) = ln( (N − n(q) + 0.5) / (n(q) + 0.5) + 1 ) |
其中 N 是文档总数,n(q) 是含该词的文档数。加 1 是为了避免 n(q) > N/2 时出现负值(原始 Robertson-Sparck Jones 形式会)。
这意味着:出现在超过一半文档里的词,IDF 接近 0,几乎不起作用。 这是自动的停用词过滤——不需要手工维护停用词表。
② 词频饱和:f/(f + K)
当 f 很小时,分数随 f 近似线性增长;当 f 远大于 K 时,分数趋于上限 k1+1。
直觉:一个词出现 3 次和出现 30 次,与相关性的关系不是线性的。出现 30 次可能是关键词堆砌,也可能是文档确实在深入讲这个主题——BM25 选择”保守地封顶”。
k1 控制饱和速度:k1 小 → 很快饱和(只奖励”出现过”);k1 大 → 更接近线性(奖励”多次出现”)。
③ 长度归一化:1 − b + b·|D|/avgdl
长文档天然包含更多词,也就更可能与查询有词重叠。不做归一化,长文档会系统性排在前面。
b 控制惩罚强度:
b = 0:不惩罚(适合长度差异本身携带信息的场景);b = 1:完全按长度比例惩罚;b = 0.75:经验折中。
工程上最值得注意的一点:如果你的文档是固定长度切分的 chunk(比如全部 512 token),|D|/avgdl ≈ 1,长度归一化项恒等于 1,b 完全失效。这时把 b 设成 0.3–0.5 甚至 0 更合理——默认 0.75 是为异构长度的网页/论文设计的,不是为等长 chunk 设计的。这是 RAG 场景里最常见的 BM25 参数误用。
1.3 变体:BM15、BM25+、BM25L、BM25F
先把术语说清楚,因为这块中文资料里经常混。
BM15 是”BM25 的前身”**,属于 Okapi 早期的 BM 系列命名(BM1、BM11、BM12、BM15、BM25)。系列的差别主要在词频归一化的形式:BM15 的词频项不做(或弱做)文档长度归一化,BM25 把长度归一化放进了分母的 K 里。工程上可以粗略理解为”b=0 附近的 BM25”**,但严格说两者的推导路径不同,不建议把 BM15 当成”BM25 的一个参数配置”。
真正在工程上有意义的是后三个变体:
| 变体 | 出处 | 修正的问题 | 公式改动 |
|---|---|---|---|
| BM25+ | Lv & Zhai, 2011 | 低频词被过度惩罚:tf=1 时长文档的分数可能低到接近 0,导致”只要出现一次”这个信号被抹掉 |
给词频项加下界 δ(通常 1.0):tf/(K+tf) → tf/(K+tf) + δ |
| BM25L | Lv & Zhai, 2011 | 长文档被过度惩罚:长度归一化对长文档的压制过强 | 先做归一化再加 δ(通常 0.5),再代入饱和函数:c = tf/(1−b+b·dl/avgdl),c' = c + δ |
| BM25F | Robertson & Zaragoza, 2004 | 多字段场景(标题/正文/锚文本权重不同) | 各字段词频先加权求和,再做饱和;长度归一化也按字段加权 |
三者的关系可以用一句话概括:
BM25+ 修的是”短文档里的低频词”,BM25L 修的是”长文档里的高频词”,两者都是给词频项加一个下界
δ,只是加的位置不同。
实际影响有多大? 在标准 TREC 数据集上,BM25+ 相对 BM25 的 MAP 提升通常在 1–3 个点,长文档集合(如 gov2、ClueWeb)上更明显。在 RAG 的固定长度 chunk 场景,收益通常很小——因为”长文档过度惩罚”这个前提不成立了。所以:
- 通用网页/论文检索:值得用 BM25+;
- 等长 chunk 的 RAG:BM25 原版足够,重点调
b和分词; - 有标题/正文结构:BM25F 的字段加权收益明显(标题权重 2–3 倍是常见配置)。
另外两个值得一提的变体:
- BM25-adpt(自适应
k1):让k1随词项在语料中的分布变化,稀有词用大k1(更看重词频),常见词用小k1。理论优雅,工程上很少见实现; - BM25PRF(伪相关反馈):用首次检索的 top-k 结果做查询扩展,再检索一次。在无监督场景能涨点,但会引入”查询漂移”。
1.4 四个结构性局限
局限一:词汇鸿沟(vocabulary mismatch)
这是最根本的一条。查询”如何退订”与文档”取消订阅”零重叠,BM25 打 0 分。同义词、上下位词、缩写、跨语言,全都无法处理。
局限二:无法处理语义组合
“苹果手机”和”手机苹果”对 BM25 是同一个词袋。否定(”不支持退款” vs “支持退款”)只差一个词,分数几乎一样。词袋假设丢弃了顺序和结构。
局限三:IDF 是全局统计,对领域漂移敏感
IDF 依赖整个语料库的统计。如果语料是混合的(技术文档 + 客服对话),同一个词在不同子集里的稀有度差异被平均掉了。分域建索引能缓解,但维护成本上升。
局限四:对分词器极度依赖(中文尤甚)
这一点在中文场景被严重低估。BM25 本身是”给定词序列算分”,词从哪来完全由分词器决定:
| 分词方案 | 效果 | 问题 |
|---|---|---|
| jieba 精确模式 | 通用好 | 领域词(”退订”、”免赔额”)会被切错 |
| jieba + 自定义词典 | 显著改善 | 需要维护词典 |
| character bigram(字二元) | 鲁棒性最好,无需词典 | 索引膨胀、噪声召回 |
| 模型分词(BPE/WordPiece) | 与 embedding 模型一致 | 中文语义单元被切碎,IDF 失真 |
一个反直觉的经验:在中文短文本检索上,字二元(bigram)经常打败词级分词。因为它对未登录词天然鲁棒(”退订”= “退”+”订”,两字都在就命中)。代价是索引变大、需要更严的 k1 与停用处理。
结论:在中文 RAG 里,分词策略对召回的影响,往往大于”BM25 还是 BM25+”的选择。
2. 稠密检索:把匹配交给学习
2.1 范式转变:从”词重叠”到”向量邻近”
稠密检索的做法很直接:
1 | 离线:d → E(d) ∈ R^d (文档编码,建索引) |
E(·) 是一个双塔(bi-encoder)模型:查询和文档分别编码,只在最后做向量相似度计算。这个”分别编码”是关键——它让文档可以离线预计算,在线只需要编码查询,一次 ANN 检索就是 O(log N)。
代价是:查询和文档在编码时互相看不见。这是双塔的表达力上限,也是它比 cross-encoder 快几个数量级的原因。
2.2 训练范式:对比学习
现代 embedding 模型几乎都是对比学习(contrastive learning)训出来的:
1 | L = −log [ exp(sim(q, d⁺)/τ) / Σ_{j} exp(sim(q, d_j)/τ) ] |
其中 d⁺ 是正例,d_j 包括正例和一批负例。三个关键设计:
| 设计 | 作用 | 工程影响 |
|---|---|---|
| in-batch negatives | 同 batch 内其他样本的文档当负例,零额外成本 | batch 越大效果越好(负例多),但受显存限制 |
| hard negatives | 用 BM25 或上一版模型召回”看起来像但不相关”的文档 | 效果提升最大的一招,也是领域微调的核心 |
| temperature τ | 控制分布的尖锐度 | 太小 → 训练不稳;太大 → 学不到区分度 |
Hard negative 是理解 embedding 质量的关键。随机负例太容易分辨(”如何退订” vs “天气预报”),模型学不到细粒度区分。真正让模型变强的是”如何退订” vs “如何订阅”这种高相似但不相关的样本对。
2.3 它解决了什么,又丢了什么
| BM25 的弱点 | 稠密检索是否解决 |
|---|---|
| 词汇鸿沟(同义词、改写) | ✅ 核心能力 |
| 跨语言检索 | ✅ 多语言模型可以(M3 支持 100+ 语言) |
| 语义组合/否定 | ⚠️ 部分(取决于训练数据,仍不可靠) |
| 上下文理解(”苹果”是公司还是水果) | ⚠️ 部分 |
| 稠密检索丢掉的 | 后果 |
|---|---|
| 精确字符串匹配 | 订单号、SKU、错误码、函数名、人名——被当成噪声 |
| 稀有词的高权重 | IDF 的”稀有即重要”机制不存在了;长尾词被模型压成”语义相近” |
| 可解释性 | BM25 能说”因为命中了这 3 个词”;向量给不出理由 |
| 零样本/冷启动 | 新领域的专有词汇,模型没见过就是没见过 |
| 索引更新的成本 | BM25 加文档是加倒排项;向量要重算 embedding |
| 精确的阈值 | 相似度分数分布因 query 而异,很难设全局阈值 |
这六条损失,就是混合检索存在的全部理由。
特别强调第四条:BM25 对新词是零成本自适应的。你加了一篇含”GLM-5.3-Flash”的文档,BM25 立刻能检索到(IDF 从语料里统计出来);稠密模型需要这个字符串在训练数据里出现过,否则它的 embedding 与”某个模型名”没区别。
2.4 一条被反复验证的经验
在领域数据上,一个微调过的 bge-base(110M)经常打败未微调的 bge-large(335M)。
原因不是参数量,是分布:通用 embedding 模型训练在网页/百科/问答上,你的业务语料(内部文档、产品手册、工单)的词汇分布和查询意图都不同。
所以在做任何模型选型之前,先问:我有没有 500–2000 条领域内的 (query, positive) 对? 有,就微调;没有,就先造。这个投入的 ROI 高于”换个更大的模型”。
3. BGE 系列:中文/多语言场景的事实标准
3.1 出身与训练配方
BGE(BAAI General Embedding)来自北京智源人工智能研究院,代表论文是 C-Pack: Packed Resources For General Chinese Embeddings(SIGIR 2024)。它之所以在中文场景普及度极高,主要因为三件事:
- 开源 + 可商用(MIT/Apache 类许可,需以各模型卡为准);
- 完整的训练数据与配方公开(C-Pack 包含 C-MTEB 评测集、大规模弱监督数据、以及训练代码);
- 在中文检索基准 C-MTEB 上超过同期通用模型。
训练流程分三段,值得理解,因为它解释了”为什么 BGE 需要 instruction 前缀”:
1 | ① 预训练(RetroMAE 式自编码) |
第三步引入了一个关键设计:**区分”对称任务”和”非对称任务”**。
- 对称任务(symmetric):句子 A 与句子 B 相似度(语义相似、去重、聚类);
- 非对称任务(asymmetric):短查询 → 长文档(检索)。
两者的最优表示空间不同:检索任务里,查询和文档不是”相似”,而是”相关”——一段文档可以完全不”像”查询文本,但回答了它。BGE 用 instruction 前缀来区分这两种模式。
3.2 instruction 前缀:不是可选项
BGE 中文 v1.5 系列在查询侧需要加:
1 | 为这个句子生成表示以用于检索相关文章: |
英文侧:
1 | Represent this sentence for searching relevant passages: |
文档侧不加。
为什么:模型在训练时见过”带指令的查询”,指令相当于告诉模型”现在做的是非对称检索,不是语义相似度”。去掉指令,模型会退回到”对称任务”的表示模式,检索性能掉几个点(BGE 官方文档提到过这个差异)。
三个常见的误用:
| 误用 | 后果 |
|---|---|
| 查询和文档都加 instruction | 分布与训练不一致,掉点 |
| 都不加 | 掉点(尤其中文) |
用了 sentence-transformers 的通用 pipeline 但没传 prompt |
默认不加,静默掉点 |
BGE-M3 是例外:它不需要 instruction(训练时没依赖这个机制),文档里明确说”无需添加指令”。
3.3 型号谱系
| 模型 | 维度 | 参数量 | 最大长度 | 语言 | 特点 |
|---|---|---|---|---|---|
bge-small-zh-v1.5 |
512 | 24M | 512 | 中 | 边缘/低延迟 |
bge-base-zh-v1.5 |
768 | 102M | 512 | 中 | 性价比最高 |
bge-large-zh-v1.5 |
1024 | 326M | 512 | 中 | 精度最好(中文 v1.5 系) |
bge-small-en-v1.5 |
384 | 33M | 512 | 英 | 英文轻量 |
bge-base-en-v1.5 |
768 | 109M | 512 | 英 | 英文主力 |
bge-large-en-v1.5 |
1024 | 335M | 512 | 英 | 英文精度 |
bge-m3 |
1024 | 568M | 8192 | 100+ | 稠密 + 稀疏 + 多向量三合一 |
bge-reranker-base |
— | 278M | 512 | 中英 | cross-encoder 重排 |
bge-reranker-large |
— | 560M | 512 | 中英 | 重排精度更高 |
bge-reranker-v2-m3 |
— | 568M | 8192 | 多语言 | 与 M3 配套 |
v1.5 相对 v1 的关键改进(常被忽略但很实用):相似度分布被”拉开”了。
v1 的问题是无相关文档对的相似度也普遍在 0.6–0.85,导致:
- 无法设阈值(”0.7 以上算相关”在 v1 上几乎没有区分度);
- rerank 阶段很难定 cutoff。
v1.5 通过调整训练(不用 1.0 作为正例目标,改用 0.9 左右)让分布更分散,这让”设定相似度阈值做过滤”重新变得可行。
3.4 BGE-M3:为什么”三合一”重要
M3 的三个 M:
| M | 含义 | 实现 |
|---|---|---|
| Multi-Linguality | 100+ 语言,支持跨语言检索 | XLM-RoBERTa 骨干 + 多语言训练 |
| Multi-Granularity | 输入最长 8192 token | 从短句到长文档统一处理 |
| Multi-Functionality | 稠密 + 稀疏 + 多向量 | 同一模型三种输出头 |
三种检索方式的输出:
① Dense(稠密):标准的 1024 维句向量,归一化后内积。
② Sparse(稀疏/词法权重):对每个 token 输出一个权重,形成 {token_id: weight} 的稀疏向量。
1 | w_t = ReLU(W_lex · h_t) |
注意:它不是 BM25。BM25 的权重来自全局 IDF 统计;M3 的权重来自上下文(同一个词在不同语境权重不同)。这是它的优势(能表达”这个词在这里重要”)也是它的劣势(见 6.4)。
③ Multi-Vector(多向量 / ColBERT 式):对每个 token 输出一个向量,检索时用 late interaction:
1 | score(q, d) = Σ_{i∈q} max_{j∈d} (E(q_i) · E(d_j)) |
即”查询的每个 token,在文档里找最相似的那个 token”,再求和。表达力接近 cross-encoder,但可以预计算文档侧向量。
**M3 的真正价值不在”一个模型三种输出”,而在”一次推理得到三种信号”**:
1 | 一次 forward → dense(1024) + sparse(~250 非零) + colbert(L×1024) |
这让混合检索的实现成本大幅下降——不需要维护 BM25 索引 + 稠密索引两套 pipeline,只需要一个模型 + 三种索引。
但要清醒:M3 的 sparse 检索在多数基准上不如调好的 BM25(尤其精确匹配场景),multi-vector 的存储成本是稠密的 L 倍(L = token 数)。三合一是”便利性”的胜利,不是”每一项都最强”。
3.5 重排模型(reranker)
BGE 的 reranker 是 cross-encoder:
1 | score = CrossEncoder(q, d) # q 和 d 拼在一起过一遍模型,输出一个标量 |
和双塔的区别:查询和文档在编码时就互相看见(cross-attention),所以能捕捉细粒度交互(”这个文档是否回答了这个问题”而不是”是否相似”)。
代价:不能预计算文档表示。每个 (q, d) 对都要完整前向,n 个候选就是 n 次推理。
| 双塔(bi-encoder) | 交叉(cross-encoder) | |
|---|---|---|
| 文档预计算 | ✅ | ❌ |
| 复杂度 | 检索 O(log N) |
重排 O(n) 次前向 |
| 精度 | 中 | 高 |
| 典型用途 | 召回(万级→百级) | 重排(百级→十级) |
| 代表 | bge-m3, bge-large | bge-reranker-v2-m3 |
这两个不是替代关系,是流水线的上下游。 用 cross-encoder 做全库检索是不可行的(几百万文档 × 每次查询),用 bi-encoder 做最终排序精度不够。
4. 混合检索:三种融合方式
4.1 问题定义
两路召回,各返回一个排序列表。怎么合并?
1 | BM25 → [(d1, 12.3), (d5, 9.8), (d2, 8.1), ...] |
核心困难:分数尺度不可比。
| 信号 | 分数范围 | 分布形状 |
|---|---|---|
| BM25 | [0, +∞),实测通常 0–30 |
长尾,随 query 长度变化 |
| Cosine | [-1, 1],实测常集中在 0.3–0.9 |
依赖模型和 query |
| M3 sparse | [0, +∞),量级与 BM25 不同 |
更平滑 |
| Cross-encoder | 无界 logits | 依赖模型 |
直接把分数相加,等于让”分数大的那一路”主导结果——这几乎总是 BM25(因为它的数值范围大)。这是混合检索最常见的实现错误。
4.2 方案一:加权求和(需要归一化)
1 | score(d) = α · norm(s_bm25(d)) + (1 − α) · norm(s_dense(d)) |
归一化方式的选择很关键:
| 归一化 | 公式 | 问题 |
|---|---|---|
| Min-Max(按 query) | (s − min)/(max − min) |
最常用也最危险:每路只有 top-k,min 是该 query 的 k 条里最小的,分布随 query 剧烈变化 |
| Z-score(按 query) | (s − μ)/σ |
比 min-max 稳,但仍受 k 条样本的分布影响 |
| 全局固定缩放 | s / C(C 是经验常数) |
稳定,但需要针对每个模型/数据集标定 |
Min-Max 的陷阱值得展开:假设稠密检索这一路只返回了 3 条(因为索引里就这么多相关文档),那么这 3 条会被拉伸到 [0,1] 满量程——一条相似度 0.4 的”勉强相关”文档会得到接近 1 的分数,压过 BM25 的高分结果。k 越小,这个问题越严重。
α 怎么定:不要拍脑袋。做法是在领域评测集上扫 α ∈ {0, 0.1, ..., 1.0},看 nDCG@10。经验值:
- 查询含大量专有名词/编号/代码 →
α偏大(0.5–0.7); - 查询是自然语言描述 →
α偏小(0.2–0.4); - 中英混合/多语言 → 稠密为主(0.2)。
更稳的做法是查询自适应:用规则(是否含数字/大写字母/引号)或一个小分类器决定 α。
4.3 方案二:RRF(倒数排名融合)
RRF(Reciprocal Rank Fusion, Cormack et al., SIGIR 2009)不看分数,只看排名:
1 | RRF(d) = Σ_{r ∈ R} 1 / (k + rank_r(d)) |
k 通常取 60(原论文的经验值),rank 从 1 开始。
为什么有效:
- 完全绕开分数尺度问题——只用排名,天然可比;
- 对异常分数鲁棒——某一路给出一个荒谬的高分,只影响它自己的排名;
- 零参数标定——
k=60基本能直接用。
代价:
- 丢弃了分数信息——第 1 名和第 2 名的差距,在不同 query 下可能完全不同,但 RRF 一视同仁;
- **
k的选择影响”头部集中度”**:k小 → 头部权重更极端(各路第 1 名主导);k大 → 更平滑; - 需要两路都返回足够多的结果——如果某一路只返回 5 条,这 5 条会拿到 5 个高 RRF 贡献,人为放大。
实践结论:先上 RRF,跑通基线,再考虑要不要调成加权融合。 大多数团队在 RRF 上的收益已经足够,继续调加权融合的边际收益很小,但调参成本很高。
4.4 方案三:级联(cascade)
不做分数融合,做流程串联:
1 | BM25 top-100 ─┐ |
优点:
- 不需要融合分数(重排模型自己会排序);
- 两路各自贡献候选,召回率是”并集”,接近上限;
- 实现最简单,工程上最稳。
缺点:
- 重排成本 = 并集大小(200 条要 200 次 forward);
- 如果某一路召回了大量噪声,会稀释重排的有效性(重排模型也是会错的)。
这是当前 RAG 系统的主流做法,因为它的假设最少、调参最少、可解释性最好。
4.5 三种方案对比
| 方案 | 需要归一化 | 调参成本 | 精度上限 | 计算成本 | 适用 |
|---|---|---|---|---|---|
| 加权求和 | 需要(关键难点) | 高(α + 归一化策略) | 高(若调好) | 低 | 分数分布稳定的场景 |
| RRF | 不需要 | 极低(k=60) | 中高 | 低 | 默认首选 |
| 级联 + 重排 | 不需要 | 低 | 最高 | 高(重排 O(n)) | 有重排预算、精度优先 |
4.6 统一模型路线:一个模型出多种信号
除了”拼两个系统”,还有”一个模型出多种表示”的路线:
| 方案 | 机制 | 优势 | 代价 |
|---|---|---|---|
| BGE-M3 | 一次 forward → dense + sparse + colbert | 部署简单、一次编码三种索引 | sparse 不如 BM25;colbert 存储大 |
| SPLADE | 用 BERT 做词项扩展,输出稀疏向量(含扩展词) | 稀疏检索也能做语义扩展;倒排索引可直接用 | 训练复杂;扩展词可能引入噪声 |
| ColBERT / ColBERTv2 | 纯 late interaction | 精度接近 cross-encoder | 存储 = token 数 × 维度,成本极高 |
| uniCOIL / DeepImpact | 学习词项权重 | 简单有效 | 表达力有限 |
SPLADE 值得单独说一句:它把”稀疏”和”语义”结合得最好——输出的稀疏向量里包含原始词 + 模型生成的扩展词(如查询”退订”会扩展出”取消”、”终止”),然后走标准倒排索引。这让它在保持倒排索引的速度优势的同时,部分解决了词汇鸿沟。
代价是:稀疏向量的非零项可能从 10 个涨到 100+ 个,索引膨胀,且扩展词不总是对的。
5. 召回与重排:漏斗与预算
5.1 标准漏斗
1 | 全库 (10^5 – 10^8) |
每一层的目标不同:
| 阶段 | 优化目标 | 关键指标 | 常见错误 |
|---|---|---|---|
| 召回 | recall@k 尽量高 | recall@50, recall@100 | 只看 precision,导致该有的没进来 |
| 融合 | 把好的排前面 | nDCG@30 | 用错的归一化,让一路主导 |
| 重排 | precision@k 高 | nDCG@5, MRR@10 | 候选太少(<20),重排无从发挥 |
| 生成 | 答案质量 | 端到端准确率 | 上下文塞太满,反而稀释 |
5.2 一个容易被忽略的原则
召回阶段不要追求 precision,重排阶段不要追求 recall。
召回阶段用 precision 优化(比如把 k 从 100 降到 20),会直接砍掉正确答案——而重排模型再强也救不回来。反之,重排阶段如果候选有 200 条,cross-encoder 的延迟会失控。
正确的做法:召回宁多勿少(100–200),重排宁精勿多(20–30),最终给 LLM 的宁少勿多(3–8 条,按需)。
5.3 RAG 里的标准配置(可直接抄)
| 环节 | 配置 | 说明 |
|---|---|---|
| Chunk | 256–512 token,重叠 10–20% | 按语义边界切(标题/段落),不要硬切 |
| BM25 | k1=1.2, b=0.3(等长 chunk 降低 b),中文用 jieba+自定义词典或 bigram |
索引与 chunk 同步更新 |
| Dense | bge-m3(多语言/长文档)或 bge-large-zh-v1.5(纯中文短 chunk) |
查询加 instruction(M3 除外) |
| 召回数 | 每路 top-50 | 太少融合无意义,太多重排慢 |
| 融合 | RRF,k=60 |
先用默认值,别急着调 |
| 重排 | bge-reranker-v2-m3 top-20 → top-5 |
阈值用相似度分布定(v1.5 后才可行) |
| 冗余控制 | MMR(λ=0.7)或按来源去重 | 避免 5 条都是同一段落的改写 |
| 上下文 | 每条带来源 ID + 位置信息 | 便于引用归因 |
5.4 延迟预算
以 50 万文档、单机 8 卡为例的粗略量级:
| 环节 | 延迟 | 备注 |
|---|---|---|
| 查询编码(bge-m3, 512 token) | 5–15 ms | batch=1,GPU |
| BM25 检索(top-50) | 2–10 ms | 内存倒排 |
| 向量检索(HNSW, top-50) | 3–15 ms | 取决于 ef_search 与索引规模 |
| RRF 融合 | < 1 ms | 纯计算 |
| 重排(20 条,bge-reranker-v2-m3) | 60–200 ms | 主要瓶颈 |
| 重排(20 条,bge-reranker-base) | 20–60 ms | 精度换延迟 |
重排是延迟大头。三条优化路径:
- 减候选(100 → 20):最有效,但要看 nDCG 是否明显下降;
- 换小模型(v2-m3 → base):精度掉 1–3 个点,延迟降 3 倍;
- 批处理 + FP16/INT8:工程优化,收益 1.5–2 倍。
如果端到端预算 < 200ms,重排通常只能留 100ms 以内,那就必须在上面的三条里选两条。
6. 工程踩坑
6.1 归一化:最基础也最容易错
坑一:忘记 L2 归一化,用内积当余弦
1 | cos(a, b) = a·b / (‖a‖‖b‖) |
如果向量没归一化,内积会被向量模长主导。BGE 系列输出的是已归一化的向量(norm=1),但:
- 自己做均值池化 / 加权池化后,norm 会变;
- 有的模型(早期 sentence-transformers)默认不归一化;
- 存进向量库后做「重排再算相似度」时,如果重新算向量忘了归一化,结果会不一致。
检查方法:随机取 100 条向量,算 np.linalg.norm(v),应该全部 ≈ 1.0(float32 下 0.999–1.001)。这一个检查能避免大量”分数看起来怪怪的”问题。
坑二:余弦距离 vs 内积索引不匹配
| 库 | 参数 | 等价于 |
|---|---|---|
FAISS IndexFlatIP + 归一化向量 |
inner product | cosine |
FAISS IndexFlatL2 + 归一化向量 |
L2 | 单调等价于 cosine(排序一致) |
Milvus COSINE |
自动归一化 | cosine |
Qdrant Cosine |
自动归一化 | cosine |
pgvector <=> |
cosine distance (1 - cos) |
注意是距离,越小越好 |
最容易错的是 pgvector:<=> 返回的是距离,ORDER BY 要升序;而 FAISS 的 IP 返回相似度,要降序。混用会导致”最不相关的排第一”。
坑三:不同模型的分数绝对不可比
bge-large 的 0.85 和 bge-m3 的 0.85 不是同一件事。换模型必须重跑评测、重新标定阈值。 迁移时复用旧阈值是”静默降级”的典型来源。
6.2 维度:不只是存储问题
坑一:维度不能混
384 / 512 / 768 / 1024 / 1536 / 3072 各不相同。混用会在索引层直接报错(好事),但更常见的是部分重建——只重算了新增文档,旧文档还是旧模型的向量 → 检索结果出现系统性偏差(新文档总是排前或排后)。
换模型的正确迁移流程:
1 | 1. 全量重算 embedding(不能增量) |
坑二:高维的成本被低估
| 维度 | 100 万条的存储(float32) | HNSW 内存(约 1.5×) |
|---|---|---|
| 384 | 1.5 GB | ~2.3 GB |
| 768 | 3.1 GB | ~4.6 GB |
| 1024 | 4.1 GB | ~6.1 GB |
| 3072 | 12.3 GB | ~18 GB |
这张表解释了两件事:
- 为什么”用 3072 维的大模型”在千万级文档上几乎不可行(内存墙);
- 为什么降维/量化(int8、binary)在工程上不是可选项而是必需项。
降维的三条路:
- Matryoshka 表示学习(MRL):训练时就让前 256 维可单独使用,截断几乎不掉点。部分新模型支持,BGE 老版本不支持;
- PCA 降维:通用但会掉 1–3 个点,且需要保留投影矩阵;
- 量化:int8 掉 ~0.5 个点,存储降 4 倍;binary 掉 3–8 个点,存储降 32 倍(适合粗筛 + 精排两阶段)。
6.3 召回数量:不是越多越好
三个真实的约束:
| 约束 | 机制 |
|---|---|
| 重排延迟 | cross-encoder 是 O(n) 次 forward,n 翻倍延迟翻倍 |
| 噪声稀释 | 候选里无关文档越多,重排模型的判断越容易被干扰(尤其是弱重排模型) |
| 上下文预算 | 最终给 LLM 的条数受 token 限制,多召回不能多用 |
一个经验曲线(多数 RAG 场景):
| 召回数 k | recall@k(相对上限) | 重排延迟(20 条基准) |
|---|---|---|
| 10 | 55–70% | 0.5× |
| 20 | 70–82% | 1× |
| 50 | 85–92% | 2.5× |
| 100 | 92–96% | 5× |
| 200 | 96–98% | 10× |
| 500 | 98%+ | 25× |
拐点在 50–100。从 100 加到 200 只涨 2–4 个点,但延迟翻倍。
所以标准配置是:每路 top-50,融合后 30–50,重排取 top-20 进 cross-encoder。 这个配置在绝大多数场景是 Pareto 最优的。
6.4 领域适配:三条路径的取舍
| 路径 | 成本 | 收益 | 风险 |
|---|---|---|---|
| 微调 embedding 模型 | 中(需 500–2000 对 + GPU) | 最高(5–20 个点 nDCG) | 过拟合到 query 分布;通用能力退化 |
| 换更大的通用模型 | 低 | 低(1–3 个点) | 成本上升,可能不如微调小模型 |
| 加 BM25 / 混合检索 | 低 | 中(3–8 个点) | 需要调融合 |
| 加 reranker | 低 | 中高(5–10 个点) | 延迟上升 |
| 改 chunk 策略 | 极低 | 中高(常被低估,5–15 个点) | 需要重建索引 |
排序建议(按 ROI):
1 | 1. 先改 chunk 策略(成本最低,收益常最大) |
为什么 chunk 排第一:检索的本质是”找对的最小单元”。如果 chunk 是硬切的 512 token,一句话被切成两半,那再好的模型也召回不到完整语义。按标题/段落/代码块边界切,保留 10–20% 重叠——这一个改动的收益经常超过换模型。
微调的具体做法(如果要走):
1 | 数据:500–2000 条 (query, positive),最好带 hard negatives |
一个必须做的检查:微调后同时测领域集和通用集(如 C-MTEB 子集)。如果领域涨 10 点但通用掉 8 点,说明过拟合——需要混入通用数据,或减小学习率。
6.5 BGE-M3 的 sparse:三个具体的坑
坑一:它的权重不是 IDF
BM25 的权重来自全局语料统计,M3 的 sparse 权重来自上下文。这意味着:
- 优势:能区分”这个词在这里重要”(同一个词在不同文档权重不同);
- 劣势:没有全局稀有度概念。一个在全语料里极罕见的词,在某个文档里权重可能不高(因为模型判断它在这个上下文不重要)。
所以在精确匹配(订单号、错误码)上,M3 sparse 经常打不过 BM25。
坑二:非零项多,存储成本不低
{token_id: weight} 的稀疏向量,非零项通常 100–300 个(BM25 通常 10–30 个)。存储和倒排索引的规模都更大。
坑三:更新与对齐
token_id 依赖 M3 的 tokenizer(XLM-R 的 sentencepiece)。这意味着:
- 不能用 jieba 分词后接 BM25 索引,两套东西无法共享;
- 换 tokenizer 就要全量重算。
结论:M3 的 sparse 适合”想用一套模型解决多种检索、且不想维护 BM25 索引”的场景;如果已经有成熟的 BM25 pipeline,直接用 BM25 更划算。
6.6 中文特有:分词与字级检索
前面提过,这里给可操作的建议:
| 方案 | 实现 | 适用 |
|---|---|---|
| jieba + 自定义词典 | 把业务词(产品名、术语)加进词典 | 通用场景,需要维护 |
| 字二元(bigram) | 连续两字作为一个 term | 鲁棒性最好,适合短查询、多未登录词 |
| 字一元(unigram) | 单字 | 召回极高但噪声大,需要强 IDF |
| 混合 | 词级 + 字级两路 BM25 | 精度最高,成本翻倍 |
一个实测经验:在中文客服/文档问答场景,bigram 检索的 recall@50 通常比 jieba 词级高 3–5 个点,因为它不会因为分词错误而丢词。代价是索引大 2–3 倍、需要更高的 k1 和更激进的停用处理(”的”、”了”这类字的 bigram 要过滤)。
6.7 评测:先建集,再优化
没有领域评测集的优化都是盲调。 建集的最小方案:
1 | 1. 从真实日志/工单里抽 200–500 条 query(覆盖主要意图) |
指标选择:
| 指标 | 含义 | 什么时候看 |
|---|---|---|
| Recall@k | k 条里有多少相关文档被召回 | 调召回阶段(k=50/100) |
| MRR@10 | 第一个相关文档排名的倒数 | 看”第一条对不对” |
| nDCG@10 | 排序质量(考虑位置折扣) | 主指标,融合/重排都看这个 |
| Hit Rate@k | 至少有一条相关 | RAG 场景最直观 |
一个陷阱:只用 LLM-as-judge 评估端到端答案,会掩盖检索问题——LLM 可以用不相关的上下文编出看起来合理的答案。必须单独评估检索层。
7. 选型建议
7.1 决策树
1 | 你的查询里有多少"精确符号"(编号/代码/人名/型号)? |
7.2 场景 → 推荐组合
| 场景 | 召回 | 融合 | 重排 | 理由 |
|---|---|---|---|---|
| 中文企业知识库 | BM25(jieba+词典) + bge-large-zh-v1.5 | RRF | bge-reranker-base | 中文场景成熟方案,延迟可控 |
| 多语言文档(含英文/日文) | BM25 + bge-m3(dense) | RRF | bge-reranker-v2-m3 | M3 唯一的跨语言选择 |
| 代码检索 | BM25(标识符分词) + bge-m3 | RRF,BM25 权重高 | 可选 | 代码强依赖精确标识符匹配 |
| 长文档 QA(合同/论文) | BM25 + bge-m3(8192) | RRF | bge-reranker-v2-m3 | 长上下文 + 多语言 |
| 电商商品搜索 | BM25(标题加权, BM25F) + bge-base | 加权(α=0.5) | 小 cross-encoder | 商品名精确匹配 + 语义改写 |
| 低延迟边缘/端侧 | BM25 + bge-small | RRF | 不上 | 重排延迟不可接受 |
| 高精度、低 QPS | 三路(BM25 + dense + sparse) | RRF | bge-reranker-large | 精度优先,成本可接受 |
| 冷启动(无标注) | BM25 + 通用 bge | RRF | 中等 reranker | 不要急着微调 |
7.3 一个务实的落地顺序
1 | Week 1:BM25 基线 + 领域评测集(200 条) |
这个顺序的关键是 Week 1:先有评测集和基线,后面的每一步才能判断”是不是真的变好了”。跳过 Week 1 直接调模型,是最常见的浪费。
8. 小结
- BM25 的核心不是”词频加权”,是概率相关性框架的产物。 IDF 来自”词在两个分布里的比例差”的推导,不是经验拍脑袋;词频饱和
f/(f+K)和长度归一化1−b+b·|D|/avgdl是两个独立的正则化项。 b在等长 chunk 上会失效。|D|/avgdl ≈ 1时长度归一化恒等于 1——这是 RAG 里最常见的 BM25 参数误用。等长 chunk 应把b降到 0.3–0.5 或 0。- BM15 是 BM25 的前身(Okapi 系列命名),不是”BM25 的一个参数配置”。 工程上有意义的是 BM25+(低频词下界)、BM25L(长文档归一化修正)、BM25F(多字段)。三者本质都是给词频项加
δ,位置不同。 - 中文 BM25 的效果,分词策略的影响大于公式选择。 字二元(bigram)在短文本/多未登录词场景经常打败 jieba 词级,因为它对分词错误天然鲁棒。
- 稠密检索丢掉了六样东西:精确匹配、稀有词权重、可解释性、零样本新词、低成本更新、稳定阈值。这六条就是混合检索存在的理由。
- BGE 的 instruction 前缀不是可选项。 它区分”对称任务(相似度)”和”非对称任务(检索)”,去掉会退回错误的表示模式。M3 是例外(不需要)。
- **v1.5 的关键改进是”相似度分布被拉开”**,这让”设相似度阈值做过滤”重新变得可行——v1 的分数全挤在 0.6–0.9,阈值毫无意义。
- BGE-M3 的三合一是”便利性”的胜利,不是”每一项最强”。 它的 sparse 在精确匹配上通常不如调好的 BM25(因为没有全局 IDF),multi-vector 的存储是稠密的 L 倍。它的价值在”一次编码三种信号”。
- 混合检索的困难不是”怎么融合”,是”分数尺度不可比”。 BM25 无界、cosine 有界、cross-encoder 是 logits。直接相加等于让 BM25 主导——这是最常见的实现错误。
- RRF 应该作为默认首选。 它绕开归一化、零参数标定、对异常值鲁棒。加权求和精度上限更高但调参成本高,只有在你已经有稳定评测集和精力扫参时才值得上。
- Min-Max 按 query 归一化是最危险的实现。 当某一路只返回 3 条时,这 3 条会被拉伸到满量程,0.4 的弱相关文档得到接近 1 的分,压过另一路的高分结果。
- 召回宁多勿少,重排宁精勿多。 召回阶段优化 precision 会砍掉正确答案(重排救不回来);重排阶段候选超过 30 条延迟会失控。拐点在每路 50、重排 20。
- ROI 排序:chunk 策略 > 混合检索 > reranker > 微调 embedding。 改 chunk 成本最低、收益常最大,但最容易被跳过(因为它不像”换个更强的模型”那么有成就感)。
- 领域微调 bge-base 经常打败未微调的 bge-large。 差距来自分布而非参数量。有 500–2000 条标注就该微调,并按轮次用当前模型挖 hard negatives。
- 先建评测集,再优化。 没有 held-out 的领域评测集,所有”提升”都可能是噪声。这是整个流程里唯一不能跳过的一步。
如果只留一句:BM25 和向量检索不是新旧之争,是”精确”与”泛化”两种匹配信号的互补——工程上的胜负手在融合方式、chunk 策略和评测集,而不在模型排行榜。
参考来源
BM25 与稀疏检索
- Robertson & Zaragoza, The Probabilistic Relevance Framework: BM25 and Beyond, Foundations and Trends in IR, 2009
- Robertson, Walker, Jones et al., Okapi at TREC-3, TREC 1994
- Lv & Zhai, Lower-Bounding Term Frequency Normalization(BM25+), CIKM 2011
- Lv & Zhai, *When Documents Are Very Long, BM25 Fails!*(BM25L), SIGIR 2011
- Robertson, Zaragoza, Taylor, Simple BM25 Extension to Multiple Weighted Fields(BM25F), CIKM 2004
- Trotman, Puurula, Burgess, Improvements to BM25 and Language Models Examined, ADCS 2014(BM25 变体的系统对比)
- Cormack, Clarke, Buettcher, Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods, SIGIR 2009(RRF, k=60)
稠密检索与 BGE
- Karpukhin et al., Dense Passage Retrieval for Open-Domain Question Answering, EMNLP 2020
- Xiao et al., C-Pack: Packed Resources For General Chinese Embeddings, SIGIR 2024(BGE 系列、C-MTEB)
- Chen et al., BGE M3-Embedding: Multi-Lingual, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation, 2024(arXiv:2402.03216)
- Muennighoff et al., MTEB: Massive Text Embedding Benchmark, EACL 2023
- Formal et al., SPLADE: Sparse Lexical and Expansion Model for First Stage Ranking, SIGIR 2021
- Khattab & Zaharia, ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT, SIGIR 2020;Santhanam et al., ColBERTv2, NAACL 2022
- Kusupati et al., Matryoshka Representation Learning, NeurIPS 2022
工程实践
- BGE 官方文档与模型卡(instruction 前缀、v1.5 相似度分布改进、M3 无需指令):https://github.com/FlagOpen/FlagEmbedding
- 关于 BGE-M3 稀疏编码局限性的中文讨论:掘金,2024-12-19
- SGLang / vLLM 的 embedding 服务与批量编码实践
说明:文中参数量、维度、最大长度等数据以各模型卡为准(核对时间 2026-11-11),BGE 系列在 2025–2026 年有持续更新(reranker 与多模态分支),具体型号请查 HuggingFace 上的 BAAI 组织页。延迟数字为单机 GPU 量级的经验值,随硬件、batch、索引参数变化很大,必须在自己环境实测。