内容参考于:图灵AI大模型全栈
检索器是用来从知识库或向量数据库中查找相关文本的东西,然后再交给大语言模型生成答案
接口地址:https://developers.llamaindex.ai/python/framework-api-reference/retrievers/
BM25检索器(传统检索),在没有向量之前我们的搜索都是使用的它
BM25全称Best Matching 25,它一个经典的词频(关键词)检索算法,现代搜索引擎中广泛使用
这里有一个词频的概念,它的意思是把文本拆成一个一个的词,然后去文档中搜索的时候查看这个词出现的频率,它的使用场景,比如游戏中的平a,它的意思是普通攻击,如果用向量检索的话通过语意是没办法知道平a是普通攻击的意思,使用词频的话就可以了,还比如通过xx去代码中找xx方法,这个通过向量也是没办法找到的,通过磁盘会比较好
它会把文本拆成一个一个的内容,在知识库中去找,BM25检索器是向量检索和BM25检索混合使用,它存在一个问题,词频和向量一起检索,那么它检索出来的东西会很多,它里面可能存在相同,也可能存在不同,所以要有一个过滤的操作,还有通过词频检索检索出来的内容排在前面还是通过向量检索出来的内容排在前面
安装
pip install llama-index-retrievers-bm25
QueryFusionRetriever融合检索器
它首先会拿到我们的问题,然后使用大模型把问题扩展成多个问题,然后分别去进行检索,检索时每个问题都会经过多个检索器,然后得到最终相关的文档后进行融合排序,最终得到Top k 个相关文档
它有三个排序方式reciprocal_rerank、relative_score、dist_based_score
reciprocal_rerank:它使用RRF排序,它的排序公式是 权重值 乘以 1 除以 (排名 + 固定值),这里面的固定值一般是60,通过这个公式排名越高分数就越高,权重是代码中retriever_weights参数设定的,由于它只依赖于排序,所以它可能会丢失相似度高的文档,下方是计算过程,它效率是最快的,如果不追求非常精准就使用它
文档 Vector 排名 BM25 排名 Vector 贡献 (权重=0.4) BM25 贡献 (权重=0.6) 加权 RRF 总分 Doc A 1 3 0.4 × 1/ (1+60) ≈ 0.00656 0.6 × 1/ (3+60) ≈ 0.00952 0.01608 Doc B 2 1 0.4 × 1/ (2+60) ≈ 0.00645 0.6 × 1/( 1+60) ≈ 0.00984 0.01629 Doc C 3 5 0.4 × 1/ (3+60) ≈ 0.00635 0.6 × 1/ (5+60) ≈ 0.00923 0.01558 Doc D - 2 0 0.6 × 1/ (2+60) ≈ 0.00968 0.00968 Doc E 4 - 0.4 × 1/ (4+60) ≈ 0.00625 0 0.00625 relative_score:分数归一化(Min-Max Scaling)算法,它的排序就加入了相似度,它的公式首先拿着 (当前的分数 减去 最小的分数) 除以 (最大的分数 减去 最小的分数),比如文档A的分数是0.85,文档B的分数是0.72,文档C的分数是0.91,带入公式(0.85-0.72) 除以 (0.91-0.72),它的排序是以分数为主
文档 Vector 原始分 Vector 归一化计算过程 Vector 归一化结果 BM25 原始分 BM25 归一化计算过程 BM25 归一化结果 最终得分计算过程 最终得分 排序 Doc A 0.85 (0.85 - 0.72) / (0.91 - 0.72) 0.684 12.5 (12.5 - 5.0) / (18.0 - 5.0) 0.577 0.6×0.684 + 0.4×0.577 0.641 1 Doc B 0.72 (0.72 - 0.72) / (0.91 - 0.72) 0.000 18.0 (18.0 - 5.0) / (18.0 - 5.0) 1.000 0.6×0.000 + 0.4×1.000 0.400 3 Doc C 0.91 (0.91 - 0.72) / (0.91 - 0.72) 1.000 5.0 (5.0 - 5.0) / (18.0 - 5.0) 0.000 0.6×1.000 + 0.4×0.000 0.600 2 dist_based_score:DBSF公式,它的公式 (当前分数 减去 平均值) 除以 标准差,它的计算逻辑是最合理的,但是效率比较慢
平均值的计算:(文档A的分数 加上 文档B的分数 加上 文档C的分数) 除以 文档的数量,如下图公示图
标准差的计算:(文档A的分数 减去 平均数) 加上 (文档B的分数 减去 平均数) 加上 (文档C的分数 减去 平均数) 除以3 ,然后开根号
开根号就是:比如给 9 这个数字开根号,3 乘以 3等于9,9开完根号的结果就是3,如果 x 乘以 x死活没办法等于n,那么就找一个最接近n的x作为结果,如下图公式图
文档 Vector 原始分 Vector 均值 Vector 标准差 Vector 标准化过程 Vector 标准化结果 BM25 原始分 BM25 均值 BM25 标准差 BM25 标准化过程 BM25 标准化结果 最终得分计算过程 最终得分 排序 Doc A 0.85 0.8267 0.0807 (0.85 - 0.8267) / 0.0807 +0.289 12.5 11.8333 5.333 (12.5 - 11.8333) / 5.333 +0.125 0.6×0.289 + 0.4×0.125 0.223 1 Doc B 0.72 0.8267 0.0807 (0.72 - 0.8267) / 0.0807 -1.322 18.0 11.8333 5.333 (18.0 - 11.8333) / 5.333 +1.156 0.6×(-1.322) + 0.4×1.156 -0.331 3 Doc C 0.91 0.8267 0.0807 (0.91 - 0.8267) / 0.0807 +1.033 5.0 11.8333 5.333 (5.0 - 11.8333) / 5.333 -1.281 0.6×1.033 + 0.4×(-1.281) 0.107 2
它的步骤
1.创建文档存储器
2.创建索引存储器
3.创建向量检索器
4.创建BM25检索器
5.创建QueryFusionRetriever(融合检索器)
6.使用混合检索器问大模型问题
效果图:下图红框是我们的问题,通过BM25检索到的相关文档如下图蓝框,它排在第一个位置,注意下图中的相似度存在0,这是因为我们非让它返回7个,但是文本中并没有我们问题中的关键词

而使用向量检索它就排到了第三位

混合检索它又排到了第一位

代码:
python
import os
import chromadb
from llama_index.core import (
SimpleDirectoryReader,
StorageContext,
VectorStoreIndex,
load_index_from_storage,
)
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.storage.docstore import SimpleDocumentStore
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.vector_stores.chroma import ChromaVectorStore
from base_llm import embed_model, llm
# 索引数据的存储路径
PERSIST_DIR = "./storage"
# 存储管理数据的存储路径
DOCSTORE_PATH = "./storage/docstore.json"
# 使用本地 Chroma 作为向量库
# 创建Chroma向量数据库
db = chromadb.PersistentClient(path="./chroma_db")
# 从Chroma向量数据库中得到一个数据库
collection = db.get_or_create_collection("hybrid_demo")
# 创建Chroma向量数据库连接
vector_store = ChromaVectorStore(chroma_collection=collection)
# 判断本地有没有索引,如果没有就创建
if not os.path.exists(PERSIST_DIR):
print("首次构建索引...")
documents = SimpleDirectoryReader(
input_files=["./data_file/deepseek介绍.txt"]
).load_data()
# 创建文档分片对象
splitter = SentenceSplitter()
# 对文档进行分片,并返回分片后的节点
nodes = splitter.get_nodes_from_documents(documents)
# 文档存储器 保存节点原文,BM25 检索依赖这里的数据
docstore = SimpleDocumentStore()
# 想文档存储器中添加文档分片后的内容
docstore.add_documents(nodes)
# 设置文档存储位置并进行存储
docstore.persist(persist_path=DOCSTORE_PATH)
# 存储管理器 统一管理向量库和文档存储
storage_context = StorageContext.from_defaults(
vector_store=vector_store,
docstore=docstore,
)
# 创建索引存储器
index = VectorStoreIndex(
nodes=nodes,
storage_context=storage_context,
embed_model=embed_model,
)
# 设置索引存储位置并进行存储
index.storage_context.persist(persist_dir=PERSIST_DIR)
print("索引构建完成")
else:
# 本地已有索引时,直接加载持久化数据
print("检测到本地索引,直接加载...")
# 文档存储器 加载本地的文档数据
docstore = SimpleDocumentStore.from_persist_path(DOCSTORE_PATH)
# 创建存储管理器
storage_context = StorageContext.from_defaults(
persist_dir=PERSIST_DIR,
vector_store=vector_store,
docstore=docstore,
)
# 加载本地索引数据
index = load_index_from_storage(
storage_context=storage_context,
embed_model=embed_model,
)
# 创建向量检索器
vector_retriever = index.as_retriever(similarity_top_k=7)
# 创建BM25检索器
bm25_retriever = BM25Retriever.from_defaults(
# 从docstore中进行检索
docstore=docstore,
similarity_top_k=7,
)
# 创建融合检索器
retriever = QueryFusionRetriever(
retrievers=[
vector_retriever,
bm25_retriever
],
# 指定大模型
llm=llm,
# 最终返回5个相关文档
similarity_top_k=5,
# 设置排序模式,这里使用RRF排序
mode="reciprocal_rerank",
use_async=True,
# 生成num_queries个问题
num_queries=1,
# 设置占比权重,0.4位置对应retrievers里的vector_retriever,0.6位置对应retrievers里的bm25_retriever
# 一般向量为主,要占大头,这里是测试可以随便写
retriever_weights=[0.4, 0.6]
)
# 测试问题文本
query = "Alexander Wang 对deepseek的评价"
print("\nbm25检索" + "=" * 50)
vector_res = bm25_retriever.retrieve(query)
for vector_node in vector_res:
print(f"bm25检索相似度: {vector_node.score}")
print(vector_node.text[:500])
print("\n向量检索" + "=" * 50)
vector_res = vector_retriever.retrieve(query)
for vector_node in vector_res:
print(f"向量检索相似度: {vector_node.score}")
print(vector_node.text[:500])
print("\n混合检索" + "=" * 50)
fusion_results = retriever.retrieve(query)
for node in fusion_results:
print(f"混合检索相似度: {node.score}")
print(node.text[:500])
# 使用融合检索结果作为上下文,让大模型生成最终回答
print("\n模型回复" + "=" * 50)
query_engine = RetrieverQueryEngine.from_args(
retriever=retriever,
llm=llm,
)
res = query_engine.query(query)
print(res)



