ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RAG检索核心:BM25算法原理与工程实践详解

RAG检索核心:BM25算法原理与工程实践详解 做RAG的人迟早都会碰到BM25这个词。不管你是用LangChain、LlamaIndex还是自己手撸一个知识库问答系统检索环节几乎绕不开它。BM25这个算法其实很老了1970年代就有雏形到现在还能在大模型时代稳坐检索模块的主力位置这件事本身就值得琢磨。这篇内容我打算从原理到实践把RAG场景下BM25的来龙去脉讲透。包括它为什么在RAG里这么重要、公式里每个参数到底在干什么、怎么在项目里真正落地、以及我踩过的一些坑。无论你是刚接触RAG的新手还是已经上过线想优化检索效果的同学这篇应该都能给你一些参考。1. 为什么RAG里总能看到BM25这个老算法1.1 RAG的基础流程检索环节到底在做什么RAGRetrieval-Augmented Generation检索增强生成的逻辑其实很直白大模型不知道私有知识那就先从外部知识库里把相关资料捞出来拼到提示词里一起喂给模型。整个流程可以简化成三个动作召回、排序、生成。召回就是检索环节。你有一个问题知识库里有几万甚至几百万篇文档检索模块要快速挑出最可能相关的几篇交给模型作答。这个快速和相关之间的平衡就是检索算法的核心命题。如果召回内容质量太差后面大模型再强也很难给出正确回答因为输入信息就是错的。检索算法从大类上分一种是基于词频统计的稀疏检索一种是基于向量语义的稠密检索。BM25就属于前者的代表也是目前RAG框架里默认集成的检索算法之一。LangChain的Retriever里默认带一个BM25RetrieverLlamaIndex里也有对应的检索节点这些都是现成的证据。1.2 稀疏检索与稠密检索的路线之争很多人一听到RAG第一反应就是embedding加向量数据库好像只有向量检索才是RAG的标配。这个想法不能说错但至少是不完整的。向量检索解决的是语义相似问题它把句子映射成几百上千维的向量然后算余弦相似度。好处是意思相近能被捞出来坏处是训练成本高、对长尾实体和专有名词很头疼解释性也偏弱。BM25这类稀疏检索走的是完全不同的路子。它不关心语义只看词项命中情况。查询里的每个词在文档里出现几次、文档里包含多少信息量这些统计特征叠加起来就是得分。它没有任何训练成本任何语料拿来就能用对精确术语匹配非常拿手。实际做RAG项目时这两类算法经常是互为补充的。比如你问苹果公司的CEO是谁向量检索可能因为CEO苹果这些词在语义空间中靠近某些通用概念而召回一堆泛泛的内容BM25则能精确定位同时包含苹果CEO这两个词组的文档片段。反过来如果你描述一个概念但没用文档里的原词BM25往往就抓瞎了这时向量检索就派上了用场。1.3 BM25能解决什么问题有哪些肉眼可见的优势放在RAG语境下BM25的独特价值可以用四个词概括零训练、强可解释、高性能、可组合。零训练意味着你不需要准备标注数据来教它任何一段文本丢给它就能建立倒排索引。RAG场景里知识库经常要换、要新增文档BM25做增量更新也比微调一个embedding模型方便太多。强可解释是指每个匹配词都能看到命中次数和IDF权重召回结果不理想时你能直接定位问题出在哪个词上而不是像向量检索那样模型觉得像但说不清哪里像。高性能方面倒排索引配合BM25打分在千万级文档规模下也能做到毫秒级响应。很多团队做RAG初期根本不需要上千维向量做全量暴力比对一个BM25就能扛住流量。可组合是最近比较火的Hybrid RAG思路把BM25和向量检索做混合召回再进行统一排序。单纯用向量检索可能在精确匹配上漏掉信息单纯用BM25在语义泛化上不够两者组合通常能在RecallK上提升一大截。2. BM25算法核心公式逐项拆解2.1 从TF-IDF到BM25它到底改进了什么BM25的完整名称是Best Matching 25它起源于概率检索模型。在它之前大家最熟悉的相关性打分方法就是TF-IDF词频乘以逆文档频率。TF-IDF的思路很简单一个词在文档里出现得越多文档越相关但这个词如果在整个语料库里都稀罕那它的区分能力就越强权重也就越高。TF-IDF的问题在于它没有处理两个影响实际效果的因素。第一文档长度。一篇5000字的文章里出现5次算法和一篇100字短文里出现5次算法置信度显然不一样。TF-IDF对这种长度差异没有惩罚机制。第二词频增长带来的收益是线性的甚至在某些场景下应该趋于饱和出现10次并不一定比出现5次好两倍。BM25就是在TF-IDF基础上做了这两个改进。它引入文档长度归一化参数和词频饱和曲线让打分结果更贴近人类的直观判断。这也是为什么在绝大多数检索基准测试上BM25都比原始TF-IDF要稳。2.2 公式里每个符号都代表什么TF、IDF、k1、bBM25的核心公式可以写成这样score(D, Q) Σ IDF(qi) * f(qi, D) * (k1 1) / (f(qi, D) k1 * (1 - b b * |D| / avgdl))其中求和符号表示对查询Q里的每个词qi分别计算最后累加。这个公式看着复杂其实拆开就三块。第一块IDF(qi)这是逆文档频率衡量这个词在整个语料库中的稀缺程度。词越少见IDF越大词在几乎所有文档里都出现IDF就趋近于零。BM25使用的IDF公式通常写为IDF(qi) ln((N - df(qi) 0.5) / (df(qi) 0.5) 1)N是文档总数df(qi)是包含词qi的文档数量。第二块是f(qi, D)也就是词qi在文档D中的出现次数即词频。第三块是一个调节因子里面有k1和b两个超参数。k1控制词频饱和的程度默认值一般在1.2到2.0之间越大说明词频增长对分数的影响越强b控制文档长度归一化的强度默认通常是0.75表示在多大程度上惩罚比平均长度更长的文档。当b等于0时长度归一化完全不生效公式就退化成纯词频版本。参数默认值作用调参方向k11.5词频饱和程度值越大词频越重要值越小数更稳b0.75文档长度惩罚强弱值越大长文档惩罚越狠0表示不惩罚avgdl自动计算语料平均文档长度一般不需要手动设2.3 拿真实数字算一遍BM25打分光看公式容易晕我手算一个简单例子。假设有一个知识库一共50篇文档我搜苹果 发布 新品这个查询。其中苹果出现在5篇文档里发布出现在20篇文档里新品出现在1篇文档里。现在有一篇候选文档D长度是20个词包含3次苹果、1次发布、0次新品。整个语料库的平均文档长度avgdl是15个词。k1取1.5b取0.75。先算苹果的IDFIDF ln((50 - 5 0.5) / (5 0.5) 1) ln(45.5 / 5.5 1) ln(9.2727) ≈ 2.227再算苹果的词频部分文档D的长度是20avgdl是15那么长度比dl/avgdl就是20/15约等于1.333。代入词频调节公式tf_component 3 * (1.5 1) / (3 1.5 * (1 - 0.75 0.75 * 1.333)) 3 * 2.5 / (3 1.5 * (0.25 1.0)) 7.5 / (3 1.875) 7.5 / 4.875 ≈ 1.538所以词项苹果对这一篇文档贡献的分数就是2.227乘以1.538约等于3.426。发布在20篇文档里出现过IDF是IDF ln((50 - 20 0.5) / (20 0.5) 1) ln(30.5 / 20.5 1) ln(2.4878) ≈ 0.911它的词频是1代入公式tf_component 1 * 2.5 / (1 1.5 * (0.25 1.0)) 2.5 / (1 1.875) ≈ 0.870这词贡献约0.793分。新品在这篇文档里没出现贡献直接是0。这篇文档最终BM25分数是3.426加0.793约等于4.219。整个过程能清楚看到稀缺词苹果在打分里占了绝对主导高频词发布只是辅助完全没出现的词不产生任何影响。3. 在RAG项目里落地BM25的完整实操3.1 最简实现rank_bm25库五分钟跑通Python生态里最省事的BM25库是rank_bm25封装了BM25Okapi、BM25L、BM25Plus等几个变体。安装一行命令pip install rank_bm25基本用法也特别简单。先准备一个分好词后的文档列表每个文档是一个list of tokens。然后建索引调用get_scores拿查询得分。from rank_bm25 import BM25Okapi # 这里已经对文档做了分词英文用split中文用jieba后面细说 corpus [ 深度学习模型在自然语言处理中表现出色.split(), BM25是一种经典的稀疏检索算法.split(), RAG通过检索增强大模型的事实性.split(), ] bm25 BM25Okapi(corpus) query 稀疏检索.split() scores bm25.get_scores(query) print(scores)get_scores会返回一个数组长度和文档数一致每个值就是对应文档的BM25分数。如果只想拿分数最高的前几条可以对分数做argsort排序。如果文档量很大rank_bm25没有内置倒排索引加速每次查询都遍历全部文档打分性能会有点吃紧后面我再说优化方案。3.2 中文RAG必须处理的分词与预处理细节英文场景用空格分词就能跑中文不行。自然语言处理切错一个词效果直接下跌。中文分词我建议优先用jieba简单直接产线可用。更精细的项目可以考虑HanLP或LAC但对大多数RAG场景来说jieba已经够了。import jieba doc BM25是一种经典的稀疏检索算法 tokens list(jieba.cut_for_search(doc)) # 结果大概是 [BM25, 是, 一种, 经典, 的, 稀疏, 检索, 算法]这里有个重要技巧不要用默认的cut模式做索引建议用cut_for_search。搜索引擎模式会输出更细粒度的词比如自然语言处理这种长词会再切出自然语言处理等子词这样查询时不管是搜自然语言处理还是自然语言都能命中更多候选文档提高召回率。预处理层面还有几个值得注意的点。标点符号和特殊字符建议统一过滤掉否则深度学习RNN和深度学习 RNN会因为逗号差异导致分词ID完全不同。英文和数字建议统一小写大小写不一致在RAG检索里是经典翻车原因。停用词要不要去掉需要看情况很多RAG场景反而建议保留停用词因为查询里的语气词可能也有区分作用。我的经验是先保留实际测试有噪音再加工。3.3 与向量检索组合成Hybrid RAG的融合策略只跑BM25效果可能不够理想尤其是用户问法跟文档原文表达差异很大的时候苹果公司创始人和乔布斯创立的企业之间没有半毛钱词项重叠。这种语义匹配必须交给向量检索让embedding模型计算相似度。所以Hybrid RAG成了主流解法核心就是怎么把两路分数合到一起去排序。最简单的融合方式是Score Fusion对BM25分数和向量余弦相似度做加权求和。但这里有个坑两个分数不在一个量纲上。BM25分数跟文档长度、词频强相关数值范围可能是0到十几余弦相似度通常在-1到1之间。直接相加的话BM25会完全主导排序结果向量检索等于白做。必须先做归一化。我用得比较多的是Min-Max归一化def normalize(scores): min_s min(scores) max_s max(scores) if max_s min_s: return [1.0] * len(scores) return [(s - min_s) / (max_s - min_s) for s in scores]归一化后BM25分数和向量分数都在0到1之间再按权重w1和w2融合final_score w1 * norm_bm25[i] w2 * norm_vector[i]权重怎么定我的经验是先用0.5对0.5跑一批测试集看bad case偏向哪边再往弱的那边加权重。比如发现语义泛化不够就适当调高向量权重发现专有名词匹配不准就调高BM25权重。除了Score Fusion还有一类叫Rank Fusion的方法比较有代表性的是RRFReciprocal Rank Fusion。它不看具体分数只看排名的倒数按位次融合score(d) Σ 1 / (k rank(d))k一般取60。RRF对分数尺度不敏感抗异常值能力很强两个检索器得分差异再大排名稳定就稳定。3.4 单机大规模文档场景下的索引优化思路rank_bm25虽然好用但有个现实问题它把全部文档的token保存在内存列表里查询时要遍历所有文档计算分数。文档数量到十万级别时单次查询耗时可能到几百毫秒甚至秒级RAG在线服务根本扛不住。方案一用Whoosh或者Elasticsearch这类自带倒排索引和BM25实现的检索引擎。ES的BM25参数可以自定义能直接替代rank_bm25的索引结构查询性能在百万级文档下都能保持毫秒级。方案二如果不想引入重型中间件可以用Faiss做稀疏向量检索。把BM25的每个文档表示成稀疏向量词项ID作为维度值词项权重作为特征值用Faiss的IndexIVFSparseQR构建索引。这个方案可以把BM25检索加速几个数量级同时保持内存可控。还有一个工程上很常见但容易忽略的点RAG的检索粒度不一定是整篇文档。正确做法是先对文档做chunk切分再对每个chunk分别建BM25索引检索时返回的是块级别的结果。这样能避免长文档中只有一小段相关内容时整体BM25分数被无关词项稀释掉。分块策略对RAG召回效果的影响有时候比换检索算法还大。4. 常见问题与排查技巧实录4.1 检索结果不准先检查这四处我接手过不少RAG项目遇到召回结果乱七八糟的情况第一个排查点永远是分词。文档和查询的分词一致性是BM25生效的前提两边用的分词器不一致比如索引用默认cut查询用全模式就会导致该命中的词意外落空。解决办法是索引和查询共用同一套分词函数用一个配置写死。第二个排查点是大小写和标点。英文文档里Deep Learning和deep learning如果没统一小写分词后是完全不同的token。标点符号同理文本清洗不够干净会在切分时混入空字符串或杂质词这些词既干扰索引结构又污染打分过程。项目初期就统一做文本规范化能省后面非常多的调试时间。第三个排查点是停用词。BM25对查询里的每个词都单独打分像我们进行一个这类高频但没有区分能力的词如果保留在查询里有可能和某些文档里的同样高频词产生高匹配把真正相关的内容挤下去。遇到这种情况把停用词表加进去重新测一下效果通常会改善。第四个排查点是查询词里包含拼写变体或中英文混排。比如用户输入BM25算法原理文档里写的是BM25 的数学原理分词后可能一个是算法一个是数学解决方式一般要靠同义词扩展或者在查询预处理里做概念归一化把相近表达映射到同一个词。4.2 BM25和向量检索的分数能直接相加吗这个问题我几乎每次分享都会被问到答案很明确不能至少不建议。同样的规则BM25分数和余弦相似度的取值范围完全不同甚至同一个向量检索模型换一个模型版本后相似度分布也会变直接相加等于让高数值那一路彻底压制另一路。正确做法是先观察两路分数的分布。跑一批查询把BM25分数和向量分数分别画出来看各自的均值和标准差。然后再做归一化。除了我前面提到的Min-Max还可以用Z-Score归一化它不依赖最大值最小值对离群值的抗性更好z (x - mean) / std融合时还可以加一层后处理规则。比如某个查询如果只被一路检索器命中就降低另一路的置信度这是RRF天然具备的能力。我常用的方案是把Score Fusion和RRF都实现一遍离线用标注好的测试集跑对比选效果更稳的那个上线。另外有一个容易忽略的细节向量检索的topK和BM25的topK不一定非要设成一样。实际场景里BM25精确匹配的召回往往更准我会给BM25设较小的K比如5给向量检索设较大的K比如20最后融合时再统一排序。这样既保证了精确信息不被漏掉又兼顾了语义召回。4.3 长文档场景经常误伤怎么调整BM25公式里有文档长度归一化按理说已经惩罚了长文档但实践中的坑在于如果你把整本书或几十页的PDF当一条文档建索引长度归一化远远不够。查询里一个低频词出现在几千词的长文档里词频的绝对值可能很大导致这篇长文档频繁霸榜而真正高质量的一小段内容反而排不上去。解决思路是按RAG标准做法把长文档拆成适当大小的chunk再索引。chunk大小推荐控制在200到500个token之间既保留足够的上下文信息又让每个chunk的搜索粒度和用户查询更加匹配。拆错了效果也会变差比如把一个完整的章节拆得七零八落使某个问题需要的答案分散在相邻chunk里无论BM25还是向量检索都难以一并召回。通常推荐用带重叠的分块方式相邻块之间留约10%到20%的重叠区域能明显缓解信息截断问题。如果一定要对整篇文档做检索也可以通过调低b值来减少长度惩罚。b值越小文档长度对打分的影响越弱长文档被单独惩罚的幅度就会减小。但这会牺牲短文档的匹配精度属于一种权衡最好还是结合实验效果来调整。5. 我对BM25在RAG中定位的几点实践经验5.1 选型判断什么时候纯BM25就够用不是所有RAG项目都需要上一套向量检索。做技术选型时我喜欢先问自己三个问题知识库规模有多大查询和文档的风格是否一致团队的工程成本预算有多少。如果知识库就是几百篇技术文档或公司内部FAQ而且用户的提问方式和文档标题高度一致比如文档标题是如何配置Nginx反向代理用户问题也差不多这个说法那纯BM25的效果完全够用。没必要为了高级去额外维护embedding模型的推理服务、向量索引和参数调优这会增加整个链路的复杂度和排查难度。反过来如果用户的提问口语化严重比如我的网页怎么打不开了是不是代理哪里没弄对而文档里全是标准术语那纯BM25基本救不回来必须上向量检索。还有一种情况是知识库文档规模到百万级以上BM25的倒排索引在内存占用和查询时延上的指标都比遍历式的稠密检索更可控在没有专门工程投入时优先选BM25更务实。5.2 值得继续深挖的扩展方向BM25在RAG里还有很多扩展玩法我觉得比较有价值的有三个方向。第一个是BM25与重排模型一起用。BM25作为粗排阶段先召回100条候选再用Cross-Encoder这类精排模型对候选做精细打分取前10条。这样既控制了精排的计算成本又能让最终效果比单独使用任何一种检索器都好。实际项目中这个组合基本上是我最常上的架构。第二个是查询改写。BM25对查询词敏感查询里用词和文档里用词不一致时效果就会打折扣。可以用一个大模型把用户问题改写成多个候选查询比如补全缩写、扩展同义表达再把候选查询分别走BM25检索后合并结果。这个思路对提升长尾查询的召回率特别明显。第三个是词权重学习。传统BM25认为查询里每个词的IDF是固定的可实际操作中查询词的重要性差异很大。比如用户搜苹果公司最新手机发布会上提到的芯片性能参数芯片和性能明显比提到最新更重要。现在有方法用小样本学习或点击日志来调整每个查询词的权重把BM25从无监督算法升级成轻量监督算法我试过一轮效果提升比调k1和b更明显但对标注数据有一定要求。最后分享一点个人体会BM25能在RAG时代继续成为标配不是因为它有多复杂而是因为它足够简单、稳定、可解释。我见过不少团队一上来就堆向量数据库和重排序模型结果检索效果反而不如先用BM25加倒排索引跑通的基线。算法选型这件事永远是从最朴素的方法开始用数据判断瓶颈在哪里再逐步加复杂度。如果你正在做RAG项目先花一晚上把BM25基线跑通把数据看明白后面每一步优化都会更有底气。
返回列表