大模型RAG实战|混合检索:BM25检索+向量检索的LlamaIndex实现
ThinkRAG大模型RAG实战系列文章,带你深入探索使用LlamaIndex框架,构建本地大模型知识库问答系统。本系列涵盖知识库管理、检索优化、模型本地部署等主题,通过代码与实例,讲解如何打造生产级系统,实现本地知识库的快速检索与智能问答。

此前,一篇文章详细介绍了使用Elasticsearch实现混合检索。而今天要说的是另一种效果更好的方案——在实际问答场景中,它比向量数据库自带的混合检索功能更胜一筹。
1 什么是混合检索
目前,大模型RAG系统中普遍采用混合检索来提升检索准确性。
核心思路很简单:针对待回答的问题,同时跑两路检索——一路是
向量语义相似度检索
基于关键词的全文检索
这就是混合检索的基本原理。
借助向量数据库(比如Chroma、LanceDB,或者之前提到的Elasticsearch),实现向量检索非常容易。所以实现混合检索的关键问题就落在了:如何做好全文检索?
好在LlamaIndex提供了
BM25检索
2 构建BM25检索器
BM25是一种文本相关度检索方法,核心是判断一个文档和查询语句的匹配程度——搜索引擎里对搜索结果排序,用的就是类似的思路。
这种相关度判断,主要基于词语权重和文档权重的融合。
- (Term Frequency):一个词在文档中间出现的频率越高,文档相关性越高。
词频 TF
- (Inverse Document Frequency):每个词在整个索引中间出现的频率越高,其相关性越低。IDF主要是为了压低像“的”这样高频词的影响,让包含低频专业术语的文档获得更高权重。
逆向文档频率 IDF
TF和IDF结合起来就形成了经典的
TF-IDF算法
BM25Retriever
不过,该检索器默认使用的tokenizer不支持中文,所以需要引入
Jieba
SimpleBM25Retriever
代码如下:
import jieba
from typing import List
def chinese_tokenizer(text: str) -> List[str]:
return list(jieba.cut(text))
class SimpleBM25Retriever(BM25Retriever):
@classmethod
def from_defaults(cls, index, similarity_top_k, **kwargs) -> "BM25Retriever":
docstore = index.docstore
return BM25Retriever.from_defaults(
docstore=docstore, similarity_top_k=similarity_top_k, verbose=True,
tokenizer=chinese_tokenizer, **kwargs
)
用向量检索时,我们通常基于index构建检索器。同样,定制的BM25检索器也把index作为参数传入,构建时传递index.docstore。BM25检索的内容基于LlamaIndex框架中的文档存储(docstore)。
之前的文章中提到过,可以采用MongoDB作为文档存储和索引存储。但如果改用
Redis
3 实现混合检索
接下来要把向量检索和BM25检索到的结果融合起来。LlamaIndex提供了一个很优雅的方法:
QueryFusionRetriever
基于此,可以定制一个新的类
SimpleFusionRetriever
代码如下:
class SimpleFusionRetriever(QueryFusionRetriever):
def __init__(self, vector_index, top_k=2, mode=FUSION_MODES.DIST_BASED_SCORE):
self.top_k = top_k
self.mode = mode
# Build vector retriever from vector index
self.vector_retriever = VectorIndexRetriever(
index=vector_index, similarity_top_k=top_k, verbose=True,
)
# Build BM25 retriever from document storage
self.bm25_retriever = SimpleBM25Retriever.from_defaults(
index=vector_index, similarity_top_k=top_k,
)
super().__init__(
[self.vector_retriever, self.bm25_retriever],
retriever_weights=[0.6, 0.4],
similarity_top_k=top_k,
num_queries=1, # set this to 1 to disable query generation
mode=mode,
use_async=True,
verbose=True,
)
融合处理的模式设置为
dist_based_score
class FUSION_MODES(str, Enum):
RECIPROCAL_RANK = "reciprocal_rerank" # 应用倒数排序融合
RELATIVE_SCORE = "relative_score" # 应用相对得分融合
DIST_BASED_SCORE = "dist_based_score" # 应用基于距离的得分融合
SIMPLE = "simple" # 基于原始得分简单重排序
先说说
relative_score
- :将每个检索器的得分归一化到0~1之间,通过减去最小值再除以极差实现。
最小-最大归一化
- :归一化后,用加权求和的方式得出最终得分,权重反映各检索器得分在最终排名中的相对重要性。
加权求和
而我们采用的
dist_based_score
QueryFusionRetriever还有一个很有趣的特性:它可以将原始查询通过LLM改写生成最多4个新查询,然后对每个检索器都逐一检索。如果有2个检索器,一共就是4×2=8次检索。最终对这些结果用上述算法进行融合,去重并重新排序。
我们这次只是实验混合检索,不希望因为过多的LLM调用而增加延迟,所以将参数num_queries设为1,禁用查询生成。
4 下一步做什么
本文介绍了使用LlamaIndex实现“稠密向量检索 + BM25全文检索”的混合检索方法,属于典型的2路召回。
值得关注的是,IBM最近的研究文章对比了各种检索方式的组合,提出采用
Blended RAG
后续将继续探索使用LlamaIndex实现3路召回混合检索,届时会与大家分享结果。
本文中的代码,都可以在
ThinkRAG
https://github.com/wzda vid/ThinkRAG
ThinkRAG是基于LlamaIndex框架、前端使用Streamlit开发的大模型知识库RAG系统,可本地部署和离线运行。
-
- 关于宇宙的好的网名有哪些
- 角色扮演 | 1
- 网名