热门搜索:和平精英 原神 街篮2 

您的位置:首页 > > 教程攻略 > ai资讯 >使用RAG技术构建企业级文档问答系统:检索优化(4)BM25和混合检索

使用RAG技术构建企业级文档问答系统:检索优化(4)BM25和混合检索

来源:互联网 更新时间:2026-08-25 14:05

概述

在大语言模型火起来之前的很长一段时间里,信息检索这个领域其实主要靠TF-IDF和BM25这类方法撑着。别看它们资历老,效果经过了时间的检验,至今依然很能打。到了大模型时代,一个常见的做法是把BM25这种稀疏检索和向量检索结合起来,取长补短,往往能带来检索效果的明显提升。

使用RAG技术构建企业级文档问答系统:检索优化(4)BM25和混合检索

为什么叫“稀疏检索”?简单来说,是因为文档和查询是用稀疏向量来表示的。这种向量的维度有多大呢?通常等于所有文档中不重复词(也就是词表)的总数,其中绝大部分维度都是0,所以叫“稀疏”。与之对应的,是深度学习时代流行的Word Embedding、向量模型产出的“稠密向量”。稠密向量的维度通常只有几百或几千,而且几乎所有维度上都有非零值——跟TF-IDF、BM25那种动辄上万维的向量比起来,确实算得上低维了。

需要特别提醒一下。用Langchain默认的BM25检索器处理中文,效果会非常惨。一个常见的坑是:做项目时没单独检查一下稀疏检索的表现,直接上了混合检索,调整权重后觉得最终效果比纯向量检索好了一点点就收工了。实际上,语义检索的优势确实明显,混合检索能有小幅提升也算合理。但事后仔细一分析才发现,默认参数下的BM25在中文场景下根本就不可能有好效果。具体原因,在核心代码部分会详细解释。

原理

这部分公式比较多,如果不喜欢看公式,可以直接跳到代码部分,不影响后续使用。

提前说明一下:下面提到的“文档”,在我们的场景里指的就是知识片段,而“语料库”则对应所有文档片段的集合。

BM25

BM25公式

BM25是一个用来评估查询和文档相关性的重要函数,全称是Best Matching 25。它的评分公式如下:

(公式略)

其中:

  • Score(D, Q) 是文档D对查询Q的评分。
  • n 是查询中包含的词数。
  • q_i 是查询中的第i个词。
  • IDF(q_i) 是词q_i的逆文档频率,计算公式为:IDF(q_i) = log((N - n(q_i) + 0.5) / (n(q_i) + 0.5) + 1),其中N是文档总数,n(q_i)是包含词q_i的文档数量。
  • TF(q_i, D) 是词q_i在文档D中间出现的次数(注意是次数,不是频率占比)。
  • |D| 是文档D的长度(词数)。
  • a vgdl 是语料库中所有文档的平均长度。
  • k1 和 b 是调节参数,k1通常在1.2到2.0之间,b在0.5到0.75之间。

前辈TF-IDF

这里顺带提一下TF-IDF,因为BM25正是在它的基础上改进而来的。TF-IDF的公式为:

(公式略)

其中,TF(q_i, D)是词q_i在文档D中的占比,比如一篇文档共20个词,“数据”出现了3次,那么TF就是3/20。IDF的公式同样是:IDF(q_i) = log((N - n(q_i) + 0.5) / (n(q_i) + 0.5) + 1)(注:部分实现中略有差异,但核心思路一致)。

简单解释一下TF-IDF在做什么。它衡量的是某个词在某个文档中的重要性。其实你也能猜到,如果只看TF,那么得分最高的肯定是“的”、“了”这类停用词。引入IDF就是为了平衡这部分高频词的影响。两者一综合,像“的”、“了”这些词的得分自然就低了,而那些在特定文档里经常出现、又在其他文档中很少出现的词——也就是能代表这篇文档主题的词——反而会被突出出来。

两者对比

相比TF-IDF,BM25的改进主要体现在以下三个方面。

第一,词频(TF)调整。

BM25引入了词频的饱和效应:随着一个词在文档中间出现的次数越来越多,它带来的相关性增益会逐渐减小。这是通过一个非线性函数来实现的,关键部分如下:

(公式略)

直观理解一下:当词频比较小的时候,主导这个公式值的主要是分子中除词频之外的部分,所以随着词频增加,分数会增长。当词频大到一定程度,增长的边际收益就被分母稀释了。

用一组实验数据来说明:假设k1=2.0,b=0.75,a vgdl=20,文档初始有5个词,某个词出现了1次。之后每次往文档中增加一个同样的词,观察TF_adj的变化和增量(diff):

TFTF_adjdiff
11.600000
22.0338980.433898
32.2360250.202127
42.3529410.116916
52.4291500.076209
62.4827590.053609
72.5225230.039764
82.5531910.030669
92.5775660.024374
102.5974030.019837

可以清楚看到,随着TF越来越大,diff越来越小。这就完美解释了“饱和效应”——越到后面,词频带来的提升越有限。

第二,文档长度标准化。

TF-IDF没有考虑文档长度的影响。长文档包含的词本来就会更多,如果不处理,长文档天然容易得高分,但这并不合理。BM25通过一个长度标准化因子来解决这个问题,关键部分如下:

(公式略)

当文档比较长时,|D|较大,分母变大,从而降低了词频的影响。当文档较短时,|D|较小,分母变小,词频的影响被放大。这个标准化是通过参数b来调节的。

第三,引入额外的调节参数。

BM25中的k1和b这两个参数,给了使用者很大的灵活性,能根据不同的应用场景进行微调。相比之下,TF-IDF就没有这样的调节手段。

混合检索

混合检索的流程如下图所示。整个过程分为两路:一路使用稠密向量检索,另一路使用稀疏检索(BM25),分别检索知识片段。然后对两路的检索结果,采用RRF算法(具体可参考本系列关于RAG Fusion的文章)进行重排序,截取Top N的知识片段送入大语言模型,最后由大模型结合用户问题和知识片段生成答案。

混合检索流程

效果对比

从下面的表格可以看到,单独使用中文分词后的BM25检索,或者使用混合检索(Embedding微调 + BM25),检索命中率都超过了之前所有的方案。而混合检索的整个问答流程的准确率,也达到了目前的最好成绩。

(表格数据略)

核心代码

本文对应的代码已经开源,地址是:https://github.com/Steven-Luo/MasteringRAG/blob/main/retrieval/04_bm25_hybrid.ipynb

BM25检索

用Langchain实现BM25检索其实很简单,直接把切分好的知识库列表splitted_docs传进去就能构建检索器:

from langchain.retrievers import BM25Retriever

vanilla_bm25 = BM25Retriever.from_documents(splitted_docs)
chunks = vanilla_bm25.get_relevant_documents(question)

但前面也说过了,这样直接用,效果非常差。针对本系列构建的测试集,Top1到Top8的命中率表现如下:

retrievertop_khit_rate
vanilla_bm2510.000000
vanilla_bm2520.000000
vanilla_bm2530.032258
vanilla_bm2540.064516
vanilla_bm2550.064516
vanilla_bm2560.064516
vanilla_bm2570.064516
vanilla_bm2580.064516

原因在哪里?打开Langchain的源码(https://github.com/langchain-ai/langchain/blob/master/libs/community/langchain_community/retrievers/bm25.py)就能看到。在创建BM25检索器时,会用一个preprocess_func来处理文档列表,而如果不指定这个参数,默认使用的是default_preprocessing_func——它就是按空格来切分的。对中文来说,按空格切分,结果可想而知。

解决方法也很直接:加入中文分词器。

import jieba

chz_cut_bm25_retriever = BM25Retriever.from_documents(splitted_docs, preprocess_func=lambda text: list(jieba.cut(text)))
k = chz_cut_bm25_retriever.k = k
chz_cut_bm25_retriever.get_relevant_documents(question)

看看修改后的召回效果,正常多了:

retrievertop_khit_rate
jieba_cut_bm2510.666667
jieba_cut_bm2520.784946
jieba_cut_bm2530.838710
jieba_cut_bm2540.870968
jieba_cut_bm2550.870968
jieba_cut_bm2560.870968
jieba_cut_bm2570.870968
jieba_cut_bm2580.870968

混合检索

创建混合检索器的方式如下:

from langchain.retrievers import BM25Retriever, EnsembleRetriever

chz_cut_bm25_retriever = BM25Retriever.from_documents(splitted_docs, preprocess_func=lambda text: list(jieba.cut(text)))

def get_ensemble_retriever(k, weights=[0.5, 0.5]):
    chz_cut_bm25_retriever.k = k
    return EnsembleRetriever(
        retrievers=[vector_db.as_retriever(search_kwargs={"k": k}), chz_cut_bm25_retriever],
        weights=weights
    )

ensemble_retriever = get_ensemble_retriever()
ensemble_retriever.get_relevant_documents(question)

这里有一个需要调节的超参数:稀疏检索和向量检索各自的权重。针对本系列的数据,我们对不同的权重配比进行了实验,不同权重下的检索性能对比如下图所示:

(图略)

关于宇宙的好的网名有哪些
关于宇宙的好的网名有哪些

类型:角色扮演

大小:1

语言:简体中文

平台:互联网

游戏下载

热门手游

手机号码测吉凶
本站所有软件,都由网友上传,如有侵犯你的版权,请发邮件haolingcc@hotmail.com 联系删除。 版权所有 Copyright@2012-2013 haoling.cc