70.高级RAG-混合检索器(BM25和向量检索,RRF、归一化分数、DBSF公式详解,融合排序)

内容参考于:图灵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)

相关推荐
Devin~Y3 小时前
从本地生活电商到 AI RAG:互联网大厂 Java 面试场景完整实战
java·spring boot·redis·elasticsearch·spring cloud·kafka·rag
孙启超13 小时前
【AI应用开发】 RAG篇(四):Prompt 工程与进阶技术
人工智能·llm·embedding·rag·向量化·chunking·文档切分
粗体鱼20 小时前
RAG/Agent 记忆混合检索多路召回:RRF 算法与Chunk RRF、Document RRF如何决策TopK
postgresql·milvus·es·rag·rff·mermory
叫我Paul就好1 天前
RAG 入门到精通 - Rerank & Hybrid Search
人工智能·rag
only-qi2 天前
大模型Agent面试攻略:落地工程痛点、评估体系与Agentic RAG核心精讲
人工智能·算法·面试·职场和发展·langchain·rag
NPE~2 天前
RAG 实战教程:从零构建企业智能问答 Agent
python·ai·实战·rag·agent搭建
sugar__salt3 天前
向量数据库从零到实战:用 Milvus + Zilliz 构建 AI 日记语义检索系统
数据库·人工智能·embedding·milvus·rag·zilliz
孙启超4 天前
【AI应用开发】 RAG篇(一):概述与核心架构
llm·embedding·向量数据库·rag·向量化·ai应用开发·chunking
爱昏羔4 天前
上篇:从PDF到向量库 — 物流行业RAG系统的知识库构建全解析
python·langchain·pdf·agent·rag