C_Meng PSNA

Never wait for the storm to pass, just dance in the rain.

0%

从 BM25 到稠密检索:BGE 系列与混合检索的工程实践

BM25 与稠密向量不是”新旧替代”,而是两种互补的匹配信号。RAG 系统里真正的分水岭,往往不在选哪个模型,而在你有没有把两种信号正确地融合,并且正确地评估。

导语

检索是 RAG 的地基,但工程上最常见的两种失败恰好发生在两个极端:

  • 只用 BM25:用户问”怎么退订”,文档里写的是”取消订阅”,一个词都对不上,召回为空;
  • 只用向量:用户搜订单号 SO-2024-8871、错误码 E1042、函数名 parse_args,语义模型把它当成无意义字符串,召回一堆”语义相近但完全无关”的内容。

这两种失败指向同一件事:词法匹配(lexical)和语义匹配(semantic)捕捉的是不同的相关性。前者对精确符号、稀有词、专有名词极强;后者对同义改写、跨语言、模糊描述极强。任何单独使用都会在真实业务里掉坑。

本文的路线:

  1. 先把 BM25 拆开(它为什么能活 30 年,以及它到底弱在哪);
  2. 讲稠密检索的范式转变(以及它丢掉了什么);
  3. 拆 BGE 系列的设计取舍(为什么它的 instruction 前缀不能省、M3 的三合一意味着什么);
  4. 讲混合检索的三种融合方式与它们的分数尺度问题;
  5. 给召回/重排的分工与 RAG 的标准配置;
  6. 最后是工程踩坑与选型。

面向已做过 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
2
3
离线:d → E(d) ∈ R^d      (文档编码,建索引)
在线:q → E(q) ∈ R^d (查询编码)
排序:score = cos(E(q), E(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)。它之所以在中文场景普及度极高,主要因为三件事:

  1. 开源 + 可商用(MIT/Apache 类许可,需以各模型卡为准);
  2. 完整的训练数据与配方公开(C-Pack 包含 C-MTEB 评测集、大规模弱监督数据、以及训练代码);
  3. 在中文检索基准 C-MTEB 上超过同期通用模型。

训练流程分三段,值得理解,因为它解释了”为什么 BGE 需要 instruction 前缀”:

1
2
3
4
5
6
① 预训练(RetroMAE 式自编码)
→ 让模型具备"从掩码中恢复语义"的能力
② 弱监督对比学习(大规模无标注 pair:标题-正文、问答对、相邻段落)
→ 学到通用的语义相似
③ 有监督微调(人工标注的相关性数据 + hard negatives)
→ 对齐"检索相关性"这个特定目标

第三步引入了一个关键设计:**区分”对称任务”和”非对称任务”**。

  • 对称任务(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
2
BM25   →  [(d1, 12.3), (d5, 9.8), (d2, 8.1), ...]
Dense → [(d2, 0.87), (d9, 0.85), (d1, 0.79), ...]

核心困难:分数尺度不可比。

信号 分数范围 分布形状
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 开始。

为什么有效:

  1. 完全绕开分数尺度问题——只用排名,天然可比;
  2. 对异常分数鲁棒——某一路给出一个荒谬的高分,只影响它自己的排名;
  3. 零参数标定——k=60 基本能直接用。

代价:

  1. 丢弃了分数信息——第 1 名和第 2 名的差距,在不同 query 下可能完全不同,但 RRF 一视同仁;
  2. **k 的选择影响”头部集中度”**:k 小 → 头部权重更极端(各路第 1 名主导);k 大 → 更平滑;
  3. 需要两路都返回足够多的结果——如果某一路只返回 5 条,这 5 条会拿到 5 个高 RRF 贡献,人为放大。

实践结论:先上 RRF,跑通基线,再考虑要不要调成加权融合。 大多数团队在 RRF 上的收益已经足够,继续调加权融合的边际收益很小,但调参成本很高。

4.4 方案三:级联(cascade)

不做分数融合,做流程串联:

1
2
3
BM25 top-100  ─┐
├→ 并集去重 → cross-encoder 重排 → top-5
Dense 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
2
3
4
5
6
7
8
9
10
11
12
13
全库 (10^5 – 10^8)
│ ① 召回(recall):高召回率、低成本、可并行多路
├─ BM25 top-50
├─ Dense top-50
└─ (可选) Sparse/ColBERT top-50
↓ 并集去重(~80–120 条)
│ ② 融合(fusion):RRF 或加权
↓ top-30
│ ③ 重排(rerank):cross-encoder,高精度、高成本
↓ top-3 – 10
│ ④ 后处理:去冗余(MMR)、按时间/权限过滤、上下文窗口填充
↓
LLM

每一层的目标不同:

阶段 优化目标 关键指标 常见错误
召回 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 精度换延迟

重排是延迟大头。三条优化路径:

  1. 减候选(100 → 20):最有效,但要看 nDCG 是否明显下降;
  2. 换小模型(v2-m3 → base):精度掉 1–3 个点,延迟降 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
2
3
4
5
1. 全量重算 embedding(不能增量)
2. 新建索引(不要原地覆盖,保留旧索引做 A/B)
3. 双写双跑 1–2 周,对比 recall/nDCG
4. 切流量
5. 观察 1 周后下线旧索引

坑二:高维的成本被低估

维度 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

这张表解释了两件事:

  1. 为什么”用 3072 维的大模型”在千万级文档上几乎不可行(内存墙);
  2. 为什么降维/量化(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
2
3
4
1. 先改 chunk 策略(成本最低,收益常最大)
2. 再加混合检索 + RRF(低成本,稳)
3. 再加 reranker(中等成本,稳)
4. 最后才微调 embedding(高成本,高收益但有风险)

为什么 chunk 排第一:检索的本质是”找对的最小单元”。如果 chunk 是硬切的 512 token,一句话被切成两半,那再好的模型也召回不到完整语义。按标题/段落/代码块边界切,保留 10–20% 重叠——这一个改动的收益经常超过换模型。

微调的具体做法(如果要走):

1
2
3
4
5
数据:500–2000 条 (query, positive),最好带 hard negatives
方法:对比学习 + in-batch negatives + 2–4 个 hard negatives
参数:LoRA 或全参微调,lr 1e-5 – 5e-6,1–3 epoch
评估:held-out 领域集 + 通用集(防退化)
注意:hard negative 要用当前模型挖(不是 BM25 挖的),每轮更新

一个必须做的检查:微调后同时测领域集和通用集(如 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
2
3
4
1. 从真实日志/工单里抽 200–500 条 query(覆盖主要意图)
2. 每条标注 1–5 条相关文档(人工,或用强模型初筛 + 人工确认)
3. 分三档:完全回答 / 部分相关 / 不相关
4. 固定一个 held-out 子集,只在最终决策时看

指标选择:

指标 含义 什么时候看
Recall@k k 条里有多少相关文档被召回 调召回阶段(k=50/100)
MRR@10 第一个相关文档排名的倒数 看”第一条对不对”
nDCG@10 排序质量(考虑位置折扣) 主指标,融合/重排都看这个
Hit Rate@k 至少有一条相关 RAG 场景最直观

一个陷阱:只用 LLM-as-judge 评估端到端答案,会掩盖检索问题——LLM 可以用不相关的上下文编出看起来合理的答案。必须单独评估检索层。


7. 选型建议

7.1 决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
你的查询里有多少"精确符号"(编号/代码/人名/型号)?
├── 很多 → 必须上 BM25(或 M3 sparse),且权重不低
└── 很少 → 稠密为主,BM25 作为兜底

语料是什么语言?
├── 纯中文 → bge-large-zh-v1.5(短 chunk)/ bge-m3(长文或需多语言)
├── 纯英文 → bge-large-en-v1.5
└── 多语言/跨语言 → bge-m3

文档多长?
├── < 512 token → bge-base/large v1.5(M3 的长上下文用不上)
├── 512–8192 → bge-m3(唯一选择,其他会截断)
└── > 8192 → 先分段,再考虑

延迟预算?
├── < 100ms 端到端 → bge-small + BM25 + RRF,不上重排
├── 100–300ms → bge-base + BM25 + RRF + bge-reranker-base
└── > 300ms → bge-large/m3 + BM25 + RRF + bge-reranker-v2-m3

有没有领域标注数据(500+ 对)?
├── 有 → 微调 bge-base(性价比最高),必然超过未微调的 large
└── 没有 → 先用通用模型 + 混合检索,同时开始造数据

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Week 1:BM25 基线 + 领域评测集(200 条)
→ 拿到 recall@50 / nDCG@10 的基线数字
→ 这一步的产出是"后续所有优化的参照系"

Week 2:加稠密检索(bge-base 或 bge-m3)+ RRF
→ 目标:nDCG@10 提升 5–10 个点
→ 如果没提升,先查 chunk 策略和归一化

Week 3:加 reranker(bge-reranker-base)
→ 目标:nDCG@5 提升 5–8 个点
→ 同时测延迟,确定候选数

Week 4+:领域微调 embedding(若有标注数据)
→ 用当前系统挖 hard negatives
→ 同时监控通用集,防退化

持续:监控线上 query 的失败案例,回流到评测集

这个顺序的关键是 Week 1:先有评测集和基线,后面的每一步才能判断”是不是真的变好了”。跳过 Week 1 直接调模型,是最常见的浪费。


8. 小结

  1. BM25 的核心不是”词频加权”,是概率相关性框架的产物。 IDF 来自”词在两个分布里的比例差”的推导,不是经验拍脑袋;词频饱和 f/(f+K) 和长度归一化 1−b+b·|D|/avgdl 是两个独立的正则化项。
  2. b 在等长 chunk 上会失效。 |D|/avgdl ≈ 1 时长度归一化恒等于 1——这是 RAG 里最常见的 BM25 参数误用。等长 chunk 应把 b 降到 0.3–0.5 或 0。
  3. BM15 是 BM25 的前身(Okapi 系列命名),不是”BM25 的一个参数配置”。 工程上有意义的是 BM25+(低频词下界)、BM25L(长文档归一化修正)、BM25F(多字段)。三者本质都是给词频项加 δ,位置不同。
  4. 中文 BM25 的效果,分词策略的影响大于公式选择。 字二元(bigram)在短文本/多未登录词场景经常打败 jieba 词级,因为它对分词错误天然鲁棒。
  5. 稠密检索丢掉了六样东西:精确匹配、稀有词权重、可解释性、零样本新词、低成本更新、稳定阈值。这六条就是混合检索存在的理由。
  6. BGE 的 instruction 前缀不是可选项。 它区分”对称任务(相似度)”和”非对称任务(检索)”,去掉会退回错误的表示模式。M3 是例外(不需要)。
  7. **v1.5 的关键改进是”相似度分布被拉开”**,这让”设相似度阈值做过滤”重新变得可行——v1 的分数全挤在 0.6–0.9,阈值毫无意义。
  8. BGE-M3 的三合一是”便利性”的胜利,不是”每一项最强”。 它的 sparse 在精确匹配上通常不如调好的 BM25(因为没有全局 IDF),multi-vector 的存储是稠密的 L 倍。它的价值在”一次编码三种信号”。
  9. 混合检索的困难不是”怎么融合”,是”分数尺度不可比”。 BM25 无界、cosine 有界、cross-encoder 是 logits。直接相加等于让 BM25 主导——这是最常见的实现错误。
  10. RRF 应该作为默认首选。 它绕开归一化、零参数标定、对异常值鲁棒。加权求和精度上限更高但调参成本高,只有在你已经有稳定评测集和精力扫参时才值得上。
  11. Min-Max 按 query 归一化是最危险的实现。 当某一路只返回 3 条时,这 3 条会被拉伸到满量程,0.4 的弱相关文档得到接近 1 的分,压过另一路的高分结果。
  12. 召回宁多勿少,重排宁精勿多。 召回阶段优化 precision 会砍掉正确答案(重排救不回来);重排阶段候选超过 30 条延迟会失控。拐点在每路 50、重排 20。
  13. ROI 排序:chunk 策略 > 混合检索 > reranker > 微调 embedding。 改 chunk 成本最低、收益常最大,但最容易被跳过(因为它不像”换个更强的模型”那么有成就感)。
  14. 领域微调 bge-base 经常打败未微调的 bge-large。 差距来自分布而非参数量。有 500–2000 条标注就该微调,并按轮次用当前模型挖 hard negatives。
  15. 先建评测集,再优化。 没有 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

工程实践

说明:文中参数量、维度、最大长度等数据以各模型卡为准(核对时间 2026-11-11),BGE 系列在 2025–2026 年有持续更新(reranker 与多模态分支),具体型号请查 HuggingFace 上的 BAAI 组织页。延迟数字为单机 GPU 量级的经验值,随硬件、batch、索引参数变化很大,必须在自己环境实测。