BM25算法

26 年 7 月 7 日 星期二
3581 字
18 分钟

为什么在 RAG 里会看到 BM25?

最近在看 RAG(Retrieval-Augmented Generation,检索增强生成)时,我经常看到一个名字:BM25

它不像大语言模型,也不是 Embedding 模型。BM25 是一个经典的文本相关性排序算法:输入一条查询和一批文档,它会给每篇文档计算一个分数,分数越高,代表文档和查询越相关。

BM25 在 RAG 中的检索流程
BM25 负责检索与排序,LLM 负责阅读资料并组织答案

这也解释了它为什么会出现在 RAG 中:

  1. 用户提出问题;
  2. 检索器从知识库中找出最相关的若干片段;
  3. 把片段和问题一起交给大模型;
  4. 大模型根据检索到的上下文生成答案。

BM25 负责的是第 2 步。它不理解世界,也不生成答案,但特别擅长查找关键词、产品名、函数名、错误码、缩写和专有名词

一句话理解:BM25 是经过“词频饱和”和“文档长度校正”的 TF-IDF。

先从最朴素的关键词计数说起

假设用户查询的是“BM25 RAG”,知识库里有两篇文档:

  • 文档 A:BM25 适合 RAG 关键词检索
  • 文档 B:向量检索可以理解语义

最简单的方法是数一数查询词出现了几次。A 同时包含 BM25RAG,明显应该排在 B 前面。

但只靠计数很快就会遇到三个问题:

  1. 所有词都同样重要吗? “的”“使用”出现在大量文档里,区分度远低于“BM25”。
  2. 出现 10 次就一定比出现 1 次相关 10 倍吗? 很可能不是,堆砌关键词不应该无限加分。
  3. 长文档是否天然占便宜? 一篇 1 万字的文章更容易偶然命中查询词,需要校正。

BM25 的设计,正是在同时处理这三个问题。

BM25 的三个核心信号
BM25 同时考虑词的稀有度、词频饱和与文档长度

BM25 的完整公式

常见的 BM25 公式如下:

score(D,Q)=qiQIDF(qi)f(qi,D)(k1+1)f(qi,D)+k1(1b+bDavgdl)\operatorname{score}(D,Q)=\sum_{q_i\in Q}\operatorname{IDF}(q_i)\cdot \frac{f(q_i,D)(k_1+1)} {f(q_i,D)+k_1\left(1-b+b\frac{|D|}{\operatorname{avgdl}}\right)}

符号看起来很多,拆开后其实只有三部分:

符号含义
QQ用户查询
DD当前参与打分的文档
qiq_i查询中的第 ii 个词
f(qi,D)f(q_i,D)查询词 qiq_i 在文档 DD 中出现的次数
$D
avgdl\operatorname{avgdl}语料库中文档的平均长度
k1k_1控制词频饱和速度,常见取值约为 1.22.01.2\sim2.0
bb控制文档长度归一化程度,范围为 010\sim1,常用值为 0.750.75

对查询里的每一个词,BM25 都计算一次“稀有度 × 词频贡献”,最后再把所有查询词的贡献相加。

第一部分:IDF,越少见的词越重要

IDF 是 Inverse Document Frequency,即逆文档频率。Lucene 使用的一个平滑版本是:

IDF(qi)=ln(1+Nn(qi)+0.5n(qi)+0.5)\operatorname{IDF}(q_i)=\ln\left(1+\frac{N-n(q_i)+0.5}{n(q_i)+0.5}\right)

其中:

  • NN 是语料库中的文档总数;
  • n(qi)n(q_i) 是包含词 qiq_i 的文档数。

如果一个词几乎每篇文档都有,那么它很难帮助我们区分文档,IDF 就低;如果一个词只出现在少数文档中,它的区分度高,IDF 就高。

例如知识库有 1000 篇文档:

  • “使用”出现在 900 篇里,提供的信息很少;
  • “BM25”只出现在 20 篇里,一旦命中就很有价值。

注意,IDF 关心的是“有多少篇文档包含这个词”,而不是这个词在全部文档中一共出现了多少次。

第二部分:TF 饱和,重复出现会加分,但不会无限加分

如果暂时忽略文档长度,词频部分可以近似看作:

f(qi,D)(k1+1)f(qi,D)+k1\frac{f(q_i,D)(k_1+1)}{f(q_i,D)+k_1}

当词第一次出现时,相关性明显增加;从 1 次增加到 2 次,仍然有帮助;但从 100 次增加到 101 次,几乎不会带来变化。这就是词频饱和

k1k_1 控制饱和的速度:

  • k1k_1 越小,曲线越快变平,词出现一次之后再重复的意义很小;
  • k1k_1 越大,词频的影响保留得越久;
  • k1=0k_1=0 时,只关心词是否出现,不再关心出现次数。

这比直接使用词频更合理,也能降低关键词堆砌对排序的影响。

第三部分:长度归一化,不能让长文档白占便宜

公式中的长度校正项是:

1b+bDavgdl1-b+b\frac{|D|}{\operatorname{avgdl}}

如果当前文档恰好等于平均长度,D/avgdl=1|D|/\operatorname{avgdl}=1,这个校正项也等于 1。

  • 文档比平均长度短,分母变小,同一次命中更“浓缩”,得分相对更高;
  • 文档比平均长度长,分母变大,同一次命中可能只是偶然,得分相对更低。

bb 决定校正有多强:

  • b=0b=0:完全忽略文档长度;
  • b=1b=1:完全按照文档长度比例进行归一化;
  • b=0.75b=0.75:常见默认值,也是 Lucene 的默认设置之一。

这里容易产生一个误解:BM25 并不是简单地“打压长文档”。如果长文档中某个词出现了很多次,词频仍然能够抵消一部分长度惩罚。它真正比较的是词频相对于文档长度是否足够突出

手算一次 BM25

假设分词后的知识库有 5 篇文档:

文档分词结果长度
D1BM25 / 适合 / RAG / 关键词 / 检索5
D2向量 / 检索 / 理解 / 语义4
D3BM25 / 通过 / 词频 / 和 / 文档长度 / 计算 / 相关性7
D4RAG / 结合 / BM25 / 和 / 向量 / 检索6
D5数据库 / 使用 / 倒排索引 / 加速 / 关键词 / 查询 / 请求7

平均文档长度为:

avgdl=5+4+7+6+75=5.8\operatorname{avgdl}=\frac{5+4+7+6+7}{5}=5.8

查询为 BM25 RAG,使用 k1=1.2k_1=1.2b=0.75b=0.75BM25 出现在 3 篇文档中,RAG 出现在 2 篇中,因此:

IDF(BM25)=ln(1+53+0.53+0.5)0.539\operatorname{IDF}(\text{BM25})=\ln\left(1+\frac{5-3+0.5}{3+0.5}\right)\approx0.539 IDF(RAG)=ln(1+52+0.52+0.5)0.875\operatorname{IDF}(\text{RAG})=\ln\left(1+\frac{5-2+0.5}{2+0.5}\right)\approx0.875

以 D1 为例,两个查询词都出现 1 次,文档长度为 5。单个词的词频与长度因子是:

1×(1.2+1)1+1.2(10.75+0.75×55.8)1.060\frac{1\times(1.2+1)}{1+1.2\left(1-0.75+0.75\times\frac{5}{5.8}\right)}\approx1.060

所以 D1 的总分约为:

(0.539+0.875)×1.0601.498(0.539+0.875)\times1.060\approx1.498

同理可得到:

文档命中的查询词BM25 分数(约)
D1BM25、RAG1.498
D4BM25、RAG1.394
D3BM250.497
D20
D50

D1 与 D4 命中了相同的查询词,每个词也都只出现一次,但 D1 更短,因此得分略高。D3 只命中 BM25,所以排在它们后面。

BM25 分数不是概率,也没有固定上限。分数只适合在同一次查询、同一套索引与打分配置下比较,不应把“分数 2”理解成“相关概率 200%”。

BM25 是怎样快速检索的?

如果每次查询都遍历知识库里的每一篇文档,再计算公式,数据一多就会很慢。工程实现通常使用倒排索引

普通的正向结构是“文档 → 词”:

text
D1 -> BM25, RAG, 关键词, 检索
D2 -> 向量, 检索, 语义

倒排索引则把方向反过来,记录“词 → 出现在哪些文档”:

text
BM25 -> D1(tf=1), D3(tf=1), D4(tf=1)
RAG  -> D1(tf=1), D4(tf=1)
检索 -> D1(tf=1), D2(tf=1), D4(tf=1)

查询 BM25 RAG 时,只需要访问这两个词的 posting list(倒排记录),而不用扫描所有文档。索引里还会保存文档频率、词频和文档长度,这些正好是 BM25 公式需要的数据。

用 Python 跑一个最小示例

可以使用 rank-bm25 快速实验:

shell
pip install rank-bm25 jieba
python
import jieba
from rank_bm25 import BM25Okapi

documents = [
    "BM25 适合 RAG 关键词检索",
    "向量检索可以理解语义",
    "BM25 通过词频和文档长度计算相关性",
    "RAG 可以结合 BM25 和向量检索",
]


def tokenize(text: str) -> list[str]:
    # 示例只做分词和去空白;生产环境还要统一大小写、同义词等规则
    return [word.strip() for word in jieba.cut(text) if word.strip()]


tokenized_corpus = [tokenize(document) for document in documents]
bm25 = BM25Okapi(tokenized_corpus, k1=1.2, b=0.75)

query = tokenize("BM25 RAG 检索")
scores = bm25.get_scores(query)

for index in scores.argsort()[::-1]:
    print(f"{scores[index]:.3f}\t{documents[index]}")

需要特别注意:rank-bm25 不会自动替你完成中文分词、停用词过滤或文本规范化。索引和查询必须使用完全一致的预处理流程,否则文档里是 BM25,查询被处理成 bm25,两者就可能无法命中。

另外,不同实现的 IDF 平滑、负 IDF 处理和默认参数可能不同,因此示例库的分数不一定与 Lucene/Elasticsearch 完全一致。比较结果前要先确认使用的是哪一种公式。

BM25 与向量检索有什么区别?

BM25 属于稀疏检索,依据离散词项是否重合来打分;向量检索属于稠密检索,通过 Embedding 在向量空间中寻找语义相近的内容。

对比项BM25向量检索
核心依据关键词重合、词频、文档频率向量空间中的语义相似度
优势精确词、编号、代码、专有名词;可解释;成本低近义词、改写、跨表达方式的语义匹配
弱点同义不同词时可能完全错过精确字符串和冷门实体有时不稳定
是否需要训练模型不需要需要 Embedding 模型
索引形态倒排索引向量索引,如 HNSW

例如查询“接口返回 504”,BM25 很容易精确命中包含 504 的故障文档;查询“请求一直没有响应”,向量检索可能找到标题为“连接超时处理”的文档,即使两边没有使用完全相同的词。

它们不是非此即彼。实际 RAG 系统常常同时执行两路检索,再融合结果,这就是混合检索(Hybrid Search)

BM25 与向量检索的混合检索流程
BM25 和向量检索分别召回,再通过 RRF 等方法融合排名

一种常见的融合方法是 RRF(Reciprocal Rank Fusion,倒数排名融合):

RRF(d)=rR1k+rankr(d)\operatorname{RRF}(d)=\sum_{r\in R}\frac{1}{k+\operatorname{rank}_r(d)}

它主要看文档在每一路结果中的名次,而不是直接相加原始分数。这样做很实用,因为 BM25 分数与向量余弦相似度通常不在同一量纲,直接相加往往没有明确意义。

一个典型的 RAG 检索链路可以是:

text
问题
 ├─ BM25 召回 Top 50
 └─ 向量召回 Top 50

       RRF 融合

    Reranker 重排 Top 20

   取 Top 5 交给 LLM

在 RAG 中使用 BM25 的实践建议

1. 分块大小会直接影响得分

BM25 会做长度归一化,因此 chunk 的长度分布很重要。一个 chunk 只有一句话,另一个 chunk 却有十页内容,两者很难公平比较。

分块时可以:

  • 让 chunk 长度大体稳定;
  • 尽量按段落、标题或语义边界切分;
  • 保留标题并拼接到正文中,因为标题往往包含高价值关键词;
  • 记录 document_id、章节和页码,方便召回后还原上下文。

2. 中文分词比调参数更值得先关注

英文通常可以按空格和词法规则切分,中文则必须决定如何分词。“大语言模型”可能被切成一个词,也可能被切成“大 / 语言 / 模型”。分词结果会同时影响 TF、DF、IDF 和文档长度。

在急着调整 k1k_1bb 之前,先检查:

  • 查询和文档是否使用同一个分词器;
  • 英文大小写、全角半角、繁简体是否统一;
  • 型号和标识符是否被错误拆分,如 GPT-5ERR_CONNECTION_RESET
  • 是否需要领域词典、同义词表和停用词表。

3. 不要迷信默认参数

k1=1.2k_1=1.2b=0.75b=0.75 是很好的起点,不是所有数据集的最优答案。

  • 文档长度差异小,可以尝试降低 bb
  • 长文档更容易因偶然命中而上榜,可以尝试提高 bb
  • 关键词重复次数很有业务意义,可以尝试提高 k1k_1
  • 短字段如标题,可以降低长度归一化,甚至单独设置字段权重。

调参时不要只看几个例子,应准备带有相关性标注的查询集合,通过 Recall@K、MRR、nDCG 等指标比较。

4. 多字段文档可以使用 BM25F 思路

标题、正文、标签的价值并不相同。标题里命中一次,通常比正文角落里命中一次更重要。工程上可以给字段设置不同权重,例如:

text
最终分数 = 3 × title_score + 1 × body_score + 2 × tag_score

严格来说,多字段统一建模对应 BM25F;搜索引擎也常通过字段 boost 实现相似效果。

5. 线上系统要保留可解释信息

BM25 的优势之一就是容易解释。调试时建议记录:

  • 查询经过分词后得到什么;
  • 每个查询词的 DF、IDF;
  • 文档中每个词的 TF;
  • 文档长度和平均长度;
  • BM25、向量检索、融合与重排各阶段的名次。

如果某篇奇怪的文档排到了第一,就能判断是分词问题、稀有词权重过高、chunk 过短,还是融合阶段出了问题。

常见误区

“BM25 就是关键词出现次数”

不准确。它同时考虑词的稀有程度、词频饱和和文档长度。关键词出现次数只是其中一个输入。

“BM25 分数越接近 1 越好”

错误。BM25 分数不是 010\sim1 的相似度或概率,数值大小依赖语料库、查询和具体实现。最重要的是同一查询下的相对排名。

“有了向量数据库,就不需要 BM25”

也不准确。向量检索善于理解语义,BM25 善于精确匹配。对于技术文档、代码、商品型号、药品名、法规编号等内容,两者结合往往更稳。

“换一个 k1k_1bb 就能解决召回问题”

很多时候真正的问题在分词、分块、字段设计或语料质量。参数只能调整已有信号的权重,无法召回一个在词项层面完全没有匹配的文档。

总结

BM25 的公式虽然长,但核心直觉可以压缩成三句话:

  1. 稀有词比常见词更有区分度,由 IDF 负责;
  2. 词重复出现有帮助,但收益逐渐饱和,由 TF 饱和负责;
  3. 长文档更容易偶然命中,需要长度归一化,由 bb 和文档长度负责。

在 RAG 中,BM25 是一个便宜、快速、可解释的关键词检索基线。它和向量检索不是替代关系:BM25 抓住“字面上必须对”的内容,向量检索补上“意思相近但说法不同”的内容,再通过 RRF 和 Reranker 组合,通常能得到更可靠的召回结果。

参考资料

文章标题:BM25算法

文章作者:梦幻の风

文章链接:https://www.hstudent.xyz/posts/ai/bm25%E7%AE%97%E6%B3%95[复制]

最后修改时间:


梦幻の风 梦幻の风

商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。