在检索增强生成模型(RAG模型)工作流中,分块在优化数据摄入中起着关键作用,通过将大型文档拆分成可管理的片段来实现。结合Qdrant的混合向量检索和高级重排序方法,可以确保查询匹配时更相关的检索内容。本文展示了分块的重要性以及如何通过混合搜索和重排序等策略性后处理步骤来提高RAG管道的有效性。通过由LlamaIndex
、Qdrant
、Ollama
和OpenAI驱动的架构,我们展示了多种数据摄入技术如何带来更好的数据评估和可视化效果。
这本作品由M K Pavan Kumar创作的。
什么是分块?检索增强生成不仅仅是方法论或流程,实际上它是一个深入的研究领域,涉及大量的研究和开发。
在检索增强生成(RAG)系统中,分块处理是必不可少的预处理步骤,涉及将大型文档或文本拆分为更小、更易处理的信息块。这一过程对于提升RAG系统中信息检索和生成任务的效率和效果至关重要。
分块的主要目的是创建更小的文本块,这些文本块可以更轻松地被索引、搜索和检索。通过将长文档分成更小的部分,RAG系统可以更准确地在响应查询或生成内容时找到相关信息。这种颗粒化的方法允许与最相关的原文部分进行更精确的匹配。
分块的方法有很多种,具体取决于内容的性质以及RAG系统的特定需求。常见的方法包括按固定数量的标记、句子或段落来分割文本。更高级的技术可能会考虑语义边界,确保每个分块都能保持连贯的意义和上下文。
块大小是RAG系统中一个重要的考虑因素。块过大可能包含无关信息,从而降低检索内容的相关度。相反,如果块太小,可能缺少必要的上下文,使得系统难以有效利用信息。找到合适的平衡点对于系统达到最佳性能至关重要。
用了哪些分块策略?- 语义分块:这种策略是基于语义意义而不是任意长度来划分文本。它的目标是创建包含连贯思想或主题的语义块。语义分块经常使用自然语言处理(NLP)技术来识别文本中的概念边界。
- 主题节点解析器:这种方法在文档中创建主题的分层结构。它识别主要主题和次级主题,然后根据这些主题来划分文本。这种方法对于保持复杂文档的整体结构和上下文特别有效。
- 语义双重合并分块:这种方法包括两个步骤。首先,文本被分割成小的固定大小的块。然后,根据语义相似性合并这些块。这种方法旨在创建更大且语义连贯的块,同时保持块大小的一致性。这种方法使用Spacy进行语义相似性计算。
该实验设计描述了实验的框架,并突出了多种分块策略的探索及其在整合了混合搜索和重排序等高级技术时的有效性。总体来说,我们的目标是在不同的分块条件下评估RAG流程中的检索质量和相关度。
在步骤1和步骤2中,数据通过各种分块策略进行处理或输入,比如“语义相似性”、“主题节点解析”和“语义双重合并”。每个分块策略都会单独评估,并且与Qdrant的混合搜索
(使用稀疏和稠密向量)及重排序机制如LLMReranking
、SentenceTransformerReranking
和LongContextReorder
一起评估。这创建了多个迭代或实验,提供了一个比较框架,看看每个分块策略单独使用或与高级机制结合时对检索过程的影响。
移动到步骤3和步骤4,准备了一个金数据集,包含核心元素如“问题”、“正确答案”和“实际上下文”。这个数据集构成了实验的基础。对于每个查询,还会生成额外的数据点:来自大型语言模型,例如OpenAI或Ollama的回答以及从Qdrant向量存储中检索到的上下文。这确保了模型生成的答案和向量存储提供的上下文都能被捕获以便后续评估。
在步骤 5中,金标准数据集中的每个问题都会发送给大规模语言模型以获取回复。随后,评估框架——本实验中使用的是 RAGAS——会根据所获取的结果计算指标。重点关注以下几个指标:忠实度,答案的相关度,和答案的正确性。这些指标对于评估 RAG 管道在各种情况下的整体表现和可靠性至关重要。
最后,在步骤 6中,评估结果将被发送到一个可视化框架,在这里分析性能指标。这一步帮助我们可视化不同指标及其在各种实验设置中的表现,从而更好地了解哪些片段处理、检索和重排序的组合最适合您的 RAG 管道。
指标(来自RAGAs):忠实度: 这衡量生成的答案与给定上下文之间的事实一致性。答案得分范围在 0 到 1 之间,得分越高越好。如果生成的答案中的所有陈述都能从给定上下文中推断出来,就认为该答案是忠实的。要计算这项指标,首先从生成的答案中识别一组陈述。然后将每个陈述与给定上下文比对,看是否可以从上下文中推断出来。忠实度得分由此过程得出。
源于 RAGA 的官方文件。
答案相关性: 评估标准“答案相关性”侧重于评估生成的答案与给定提示的相关程度。对于不完整或包含多余信息的答案,得分较低,而分数越高表示相关性越好。这个指标是通过 question
、context
和 answer
来计算的。即计算原始问题与根据回答生成的人工问题之间的平均余弦相似度。
来自 RAGA 官方文档如下。
答案正确性: 答案正确性的评估涉及评估生成的答案与实际答案之间的准确性。这一评估基于ground truth
和答案
进行,评分范围在0到1之间。较高的分数表示生成的答案更接近实际答案,表明正确性更高。答案正确性包括两个关键方面:语义上的相似以及事实上的相似。这两个方面通过加权方式结合起来,从而得出答案正确性的评分。用户还可以选择设定一个‘阈值’来将评分结果转换为二进制。
后面的內容請確保使用更口语化的中文表达。
为了实验用途,我选择了mlops
研究论文,你可以在这里查看:here。
我们已经准备了一个包含20个questions
以及它的ground_truth
和context
的黄金数据集,数据集的一个样本如下。
[
{
"question": "MLOps的目标是什么呢?",
"ground_truth": "MLOps的目标是通过缩短开发(Dev)和运维(Ops)之间的距离来促进机器学习产品的创建。",
"context": "MLOps(机器学习运维)是一种范式,涵盖最佳实践、概念集合以及从概念化、实现、监控、部署到扩展的端到端过程。"
},
{
"question": "MLOps的关键原则是什么?",
"ground_truth": "MLOps的关键原则包括CI/CD自动化、工作流编排、可重复性、数据、模型和代码的版本管理、协作、持续的机器学习训练和评估、机器学习元数据跟踪与记录、持续监控与反馈循环。",
"context": "MLOps旨在通过这些原则来促进机器学习产品的创建:CI/CD自动化、工作流编排、可重复性;数据、模型和代码的版本管理;协作;持续的机器学习训练和评估;机器学习元数据跟踪与记录;持续监控;以及反馈循环。"
},
{
"question": "MLOps结合了哪些学科?",
"ground_truth": "MLOps结合了机器学习、软件工程(特别是DevOps)和数据工程这三个领域。",
"context": "最重要的是,它是一种工程实践,利用了机器学习、软件工程(特别是DevOps)和数据工程这三个领域。"
}
]
我们在该项目中有三个数据索引文件,第一个使用了普通索引器和所有三种分块策略,第二个则是结合了Qdrant混合搜索功能(稀疏向量和稠密向量)的分块策略,最后一个则是结合了分块策略和重排序机制,如“LLM重排序”、“Sentence-Transformer重排序”和“长上下文重排”。
具有所有三种分块方式的数据索引器:
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext, Settings
from llama_index.core.indices.base import IndexType
from llama_index.node_parser.topic import TopicNodeParser
from llama_index.core.node_parser import (
SentenceSplitter,
SemanticSplitterNodeParser,
SemanticDoubleMergingSplitterNodeParser,
LanguageConfig
)
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
from dotenv import load_dotenv, find_dotenv
import qdrant_client
import os
class LlamaIndexDataHandler:
def __init__(self, chunk_size: int, chunk_overlap: int, top_k: int):
load_dotenv(find_dotenv())
input_dir = os.environ.get('input_dir')
collection_name = os.environ.get('collection_name')
qdrant_url = os.environ.get('qdrant_url')
qdrant_api_key = os.environ.get('qdrant_api_key')
llm_url = os.environ.get('llm_url')
llm_model = os.environ.get('llm_model')
embed_model_name = os.environ.get('embed_model_name')
# 初始化设置
self.top_k = top_k
self.collection_name = collection_name
# 设置LLM和嵌入模型
Settings.llm = Ollama(base_url=llm_url, model=llm_model, request_timeout=300)
Settings.embed_model = OllamaEmbedding(base_url=llm_url, model_name=embed_model_name)
Settings.chunk_size = chunk_size
Settings.chunk_overlap = chunk_overlap
# 加载指定目录中的文档
self.documents = self.load_documents(input_dir)
# 初始化Qdrant客户端
self.qdrant_client = qdrant_client.QdrantClient(url=qdrant_url, api_key=qdrant_api_key)
# 设置Qdrant向量存储库
self.qdrant_vector_store = QdrantVectorStore(collection_name=self.collection_name,
client=self.qdrant_client,
# sparse_config= None,
# dense_config= = None
)
# 设置存储上下文
self.storage_ctx = StorageContext.from_defaults(vector_store=self.qdrant_vector_store)
self.vector_store_index: IndexType = None
self.sentence_splitter = SentenceSplitter.from_defaults(chunk_size=Settings.chunk_size,
chunk_overlap=Settings.chunk_overlap)
def load_documents(self, input_dir):
return SimpleDirectoryReader(input_dir=input_dir, required_exts=['.pdf']).load_data(show_progress=True)
def index_data_based_on_method(self, method: str):
if method == 'semantic_chunking':
# 初始化分段器
splitter = SemanticSplitterNodeParser(
buffer_size=1, breakpoint_percentile_threshold=95, embed_model=Settings.embed_model
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建向量存储索引
self.vector_store_index = VectorStoreIndex(
nodes=splitter.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter]
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store)
elif method == 'semantic_double_merge_chunking':
config = LanguageConfig(language="english", spacy_model="en_core_web_md")
splitter = SemanticDoubleMergingSplitterNodeParser(
language_config=config,
initial_threshold=0.4,
appending_threshold=0.5,
merging_threshold=0.5,
max_chunk_size=5000,
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建向量存储索引
self.vector_store_index = VectorStoreIndex(
nodes=splitter.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter]
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store)
elif method == 'topic_node_parser':
node_parser = TopicNodeParser.from_defaults(
llm=Settings.llm,
max_chunk_size=Settings.chunk_size,
similarity_method="llm",
similarity_threshold=0.8,
window_size=3 # 文献建议窗口大小为5
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建向量存储索引
self.vector_store_index = VectorStoreIndex(
nodes=node_parser.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter]
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store)
def create_query_engine(self):
return self.vector_store_index.as_query_engine(top_k=self.top_k)
def query(self, query_text):
query_engine = self.create_query_engine()
return query_engine.query(str_or_query_bundle=query_text)
# 示例用例
def main():
llama_index_handler = LlamaIndexDataHandler(chunk_size=128, chunk_overlap=20, top_k=3)
llama_index_handler.index_data_based_on_method(method='semantic_double_merge_chunking')
# response = llama_index_handler.query("operational challenges of mlops")
# print(response)
if __name__ == "__main__":
main()
数据索引工具具有分块策略 + Qdrant 混合搜索功能(稀疏向量 + 密集向量)
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext, Settings
from llama_index.core.indices.base import IndexType
from llama_index.node_parser.topic import TopicNodeParser
from llama_index.core.node_parser import (
SentenceSplitter,
SemanticSplitterNodeParser,
SemanticDoubleMergingSplitterNodeParser,
LanguageConfig
)
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
from dotenv import load_dotenv, find_dotenv
from fastembed import TextEmbedding
import qdrant_client
import os
class LlamaIndexDataHandler:
def __init__(self, chunk_size: int, chunk_overlap: int, top_k: int):
load_dotenv(find_dotenv())
input_dir = os.environ.get('input_dir')
collection_name = os.environ.get('collection_name')
qdrant_url = os.environ.get('qdrant_url')
qdrant_api_key = os.environ.get('qdrant_api_key')
llm_url = os.environ.get('llm_url')
llm_model = os.environ.get('llm_model')
embed_model_name = os.environ.get('embed_model_name')
# 初始化设置
self.top_k = top_k
self.collection_name = collection_name
# 设置LLM和嵌入模型
Settings.llm = Ollama(base_url=llm_url, model=llm_model, request_timeout=300)
Settings.embed_model = OllamaEmbedding(base_url=llm_url, model_name=embed_model_name)
Settings.chunk_size = chunk_size
Settings.chunk_overlap = chunk_overlap
# 加载指定目录的文档
self.documents = self.load_documents(input_dir)
# 初始化Qdrant客户端
self.qdrant_client = qdrant_client.QdrantClient(url=qdrant_url, api_key=qdrant_api_key)
# 设置Qdrant向量存储库
self.qdrant_vector_store = QdrantVectorStore(
collection_name=self.collection_name,
client=self.qdrant_client,
fastembed_sparse_model=os.environ.get("sparse_model"),
enable_hybrid=True
)
# 设置存储上下文环境
self.storage_ctx = StorageContext.from_defaults(vector_store=self.qdrant_vector_store)
self.vector_store_index: IndexType = None
self.sentence_splitter = SentenceSplitter.from_defaults(chunk_size=Settings.chunk_size,
chunk_overlap=Settings.chunk_overlap)
def load_documents(self, input_dir):
return SimpleDirectoryReader(input_dir=input_dir, required_exts=['.pdf']).load_data(show_progress=True)
def index_data_based_on_method(self, method: str):
if method == 'semantic_chunking':
# 初始化拆分器
splitter = SemanticSplitterNodeParser(
buffer_size=1, breakpoint_percentile_threshold=95, embed_model=Settings.embed_model
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建向量存储索引
self.vector_store_index = VectorStoreIndex(
nodes=splitter.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter],
embed_model=OllamaEmbedding(model_name=os.environ.get("embed_model_name"))
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store,
embed_model=OllamaEmbedding(model_name=os.environ.get("embed_model_name")))
elif method == 'semantic_double_merge_chunking':
config = LanguageConfig(language="english", spacy_model="en_core_web_md")
splitter = SemanticDoubleMergingSplitterNodeParser(
language_config=config,
initial_threshold=0.4,
appending_threshold=0.5,
merging_threshold=0.5,
max_chunk_size=5000,
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建向量存储索引
self.vector_store_index = VectorStoreIndex(
nodes=splitter.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter],
embed_model=OllamaEmbedding(model_name=os.environ.get("embed_model_name"))
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store,
embed_model=OllamaEmbedding(model_name=os.environ.get("embed_model_name")))
elif method == 'topic_node_parser':
node_parser = TopicNodeParser.from_defaults(
llm=Settings.llm,
max_chunk_size=Settings.chunk_size,
similarity_method="llm",
similarity_threshold=0.8,
window_size=3 # 建议窗口大小为5
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建向量存储索引
self.vector_store_index = VectorStoreIndex(
nodes=node_parser.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter],
embed_model=OllamaEmbedding(model_name=os.environ.get("embed_model_name"))
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store,
embed_model=OllamaEmbedding(model_name=os.environ.get("embed_model_name")))
def create_query_engine(self):
return self.vector_store_index.as_query_engine(top_k=self.top_k)
def query(self, query_text):
query_engine = self.create_query_engine()
return query_engine.query(str_or_query_bundle=query_text) # 查询引擎查询(字符串或查询包=query_text)
# 使用示例
def main():
llama_index_handler = LlamaIndexDataHandler(chunk_size=128, chunk_overlap=20, top_k=3)
llama_index_handler.index_data_based_on_method(method='semantic_double_merge_chunking')
# response = llama_index_handler.query("operational challenges of mlops")
# print(response)
if __name__ == "__main__":
main()
数据索引器,带有分块策略和重排序机制,如“LLM 重排序器”、“Sentence Transformer 重排序”和“长上下文重组”。
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext, Settings
from llama_index.core.indices.base import IndexType
from llama_index.node_parser.topic import TopicNodeParser
from llama_index.core.node_parser import *
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.core.postprocessor import LLMRerank, SentenceTransformerRerank, LongContextReorder
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
from dotenv import load_dotenv, find_dotenv
import qdrant_client
import os
class LlamaIndexDataHandler:
def __init__(self, chunk_size: int, chunk_overlap: int, top_k: int):
load_dotenv(find_dotenv())
input_dir = os.environ.get('input_dir')
collection_name = os.environ.get('collection_name')
qdrant_url = os.environ.get('qdrant_url')
qdrant_api_key = os.environ.get('qdrant_api_key')
llm_url = os.environ.get('llm_url')
llm_model = os.environ.get('llm_model')
embed_model_name = os.environ.get('embed_model_name')
# 初始化设置
self.top_k = top_k
self.collection_name = collection_name
# 设置LLM和嵌入模型
Settings.llm = Ollama(base_url=llm_url, model=llm_model, request_timeout=300)
Settings.embed_model = OllamaEmbedding(base_url=llm_url, model_name=embed_model_name)
Settings.chunk_size = chunk_size
Settings.chunk_overlap = chunk_overlap
# 从指定目录加载文档
self.documents = self.load_documents(input_dir)
# 初始化Qdrant客户端
self.qdrant_client = qdrant_client.QdrantClient(url=qdrant_url, api_key=qdrant_api_key)
# 设置Qdrant向量存储
self.qdrant_vector_store = QdrantVectorStore(collection_name=self.collection_name,
client=self.qdrant_client,
# sparse_config=None, # dense_config=None
)
# 设置StorageContext
self.storage_ctx = StorageContext.from_defaults(vector_store=self.qdrant_vector_store)
self.vector_store_index: IndexType = None # 类型注释
self.sentence_splitter = SentenceSplitter.from_defaults(chunk_size=Settings.chunk_size,
chunk_overlap=Settings.chunk_overlap)
def load_documents(self, input_dir):
return SimpleDirectoryReader(input_dir=input_dir, required_exts=['.pdf']).load_data(show_progress=True)
def 基于方法索引数据(self, method: str):
if method == 'semantic_chunking':
# 初始化分割器
splitter = SemanticSplitterNodeParser(
buffer_size=1, breakpoint_percentile_threshold=95, embed_model=Settings.embed_model
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建VectorStoreIndex
self.vector_store_index = VectorStoreIndex(
nodes=splitter.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter]
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store)
elif method == 'semantic_double_merge_chunking':
config = LanguageConfig(language="english", spacy_model="en_core_web_md")
splitter = SemanticDoubleMergingSplitterNodeParser(
language_config=config,
initial_threshold=0.4,
appending_threshold=0.5,
merging_threshold=0.5,
max_chunk_size=5000,
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建VectorStoreIndex
self.vector_store_index = VectorStoreIndex(
nodes=splitter.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter]
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store)
elif method == 'topic_node_parser':
node_parser = TopicNodeParser.from_defaults(
llm=Settings.llm,
max_chunk_size=Settings.chunk_size,
similarity_method="llm",
similarity_threshold=0.8,
window_size=3 # 文献建议为5(原文为3)
)
if not self.qdrant_client.collection_exists(collection_name=self.collection_name):
# 创建VectorStoreIndex
self.vector_store_index = VectorStoreIndex(
nodes=node_parser.get_nodes_from_documents(documents=self.documents),
storage_context=self.storage_ctx, show_progress=True, transformations=[self.sentence_splitter]
)
else:
self.vector_store_index = VectorStoreIndex.from_vector_store(vector_store=self.qdrant_vector_store)
def 创建查询引擎(self, postprocessing_method: str):
if postprocessing_method == 'llm_reranker':
reranker = LLMRerank(llm=Settings.llm, choice_batch_size=self.top_k)
return self.vector_store_index.as_query_engine(top_k=self.top_k, node_postprocessors=[reranker])
elif postprocessing_method == 'sentence_transformer_rerank':
reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-2-v2", top_n=self.top_k
)
return self.vector_store_index.as_query_engine(top_k=self.top_k, node_postprocessors=[reranker])
elif postprocessing_method == 'long_context_reorder':
reorder = LongContextReorder()
return self.vector_store_index.as_query_engine(top_k=self.top_k, node_postprocessors=[reorder])
return self.vector_store_index.as_query_engine(top_k=self.top_k)
def 查询(self, query_text, postprocessing_method: str):
query_engine = self.create_query_engine(postprocessing_method=postprocessing_method)
return query_engine.query(str_or_query_bundle=query_text)
# 示例用法
def main():
llama_index_handler = LlamaIndexDataHandler(chunk_size=128, chunk_overlap=20, top_k=3)
llama_index_handler.基于方法索引数据(method='semantic_double_merge_chunking')
# response = llama_index_handler.query("operational challenges of mlops")
# print(response)
if __name__ == "__main__":
main()
一旦索引器已经准备好,我们现在需要把重点放在评估上。我创建了一个名为 evaluation_playground.py
的文件,在其中我们连接到现有数据集的向量存储,这些数据集是在数据导入阶段创建的,然后请求大型语言模型根据我们的 gold_dataset(黄金数据集)提供回复。
import os
import logging
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.core.postprocessor import LLMRerank, SentenceTransformerRerank, LongContextReorder
from llama_index.llms.openai import OpenAI
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
from datasets import Dataset
import qdrant_client
import pandas as pd
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
answer_correctness,
)
from dotenv import load_dotenv, find_dotenv
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class LlamaIndexEvaluator:
def __init__(self, input_dir="data", model_name="llama3.2:latest", base_url="http://localhost:11434"):
logger.info(f"初始化 LlamaIndexEvaluator,输入目录是 {input_dir},模型名为 {model_name},基础URL为 {base_url}")
load_dotenv(find_dotenv())
self.input_dir = input_dir
self.model_name = model_name
self.base_url = base_url
self.documents = self.load_documents()
self.top_k = 5
Settings.llm = Ollama(model=self.model_name, base_url=self.base_url)
logger.info("从 gold_data.json 文件加载数据集")
self.dataset = pd.read_json(os.path.join(input_dir, "gold_data.json"))
self.query_engine = self.build_query_engine(postprocessing_method='llm_reranker')
def load_documents(self):
logger.info(f"从目录 {self.input_dir} 加载文档")
documents = SimpleDirectoryReader(input_dir=self.input_dir, required_exts=['.pdf'],
num_files_limit=30).load_data(show_progress=True)
logger.info(f"加载 {len(documents)} 个文档")
return documents
def build_query_engine(self, postprocessing_method: str):
logger.info("初始化 Qdrant 客户端")
# 初始化 Qdrant 客户端
qdrant_cli = qdrant_client.QdrantClient(url=os.environ.get("qdrant_url"),
api_key=os.environ.get("qdrant_api_key"))
logger.info("设置 Qdrant 的向量存储")
# 根据实验需要设置 Qdrant 的向量存储(混合或正常模式)
qdrant_vector_store = QdrantVectorStore(collection_name=os.environ.get("collection_name"), client=qdrant_cli)
# qdrant_vector_store = QdrantVectorStore(collection_name=os.environ.get("collection_name"),
# client=qdrant_cli,
# fastembed_sparse_model=os.environ.get("sparse_model"),
# enable_hybrid=True
# )
logger.info("从现有集合构建 VectorStoreIndex")
vector_index = VectorStoreIndex.from_vector_store(
vector_store=qdrant_vector_store,
embed_model=OllamaEmbedding(model_name=os.environ.get("embed_model_name"), base_url=self.base_url)
)
query_engine = None
if postprocessing_method == 'llm_reranker':
reranker = LLMRerank(llm=Settings.llm, choice_batch_size=self.top_k)
query_engine = vector_index.as_query_engine(top_k=self.top_k, node_postprocessors=[reranker])
elif postprocessing_method == 'sentence_transformer_rerank':
reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-2-v2", top_n=self.top_k
)
query_engine = vector_index.as_query_engine(top_k=self.top_k, node_postprocessors=[reranker])
elif postprocessing_method == 'long_context_reorder':
reorder = LongContextReorder()
query_engine = vector_index.as_query_engine(top_k=self.top_k, node_postprocessors=[reorder])
query_engine = vector_index.as_query_engine(similarity_top_k=5, llm=Settings.llm)
logger.info(f"查询引擎构建成功:{query_engine},准备好进行查询了")
return query_engine
def generate_responses(self, test_questions, test_answers=None):
logger.info("生成测试问题的响应")
responses = [self.query_engine.query(q) for q in test_questions]
answers = []
contexts = []
for r in responses:
answers.append(r.response)
contexts.append([c.node.get_content() for c in r.source_nodes])
dataset_dict = {
"question": test_questions,
"answer": answers,
"contexts": contexts,
}
if test_answers is not None:
dataset_dict["ground_truth"] = test_answers
logger.info("响应生成成功")
return Dataset.from_dict(dataset_dict)
def evaluate(self):
logger.info("开始评估")
evaluation_ds = self.generate_responses(
test_questions=self.dataset["question"].tolist(),
test_answers=self.dataset["ground_truth"].tolist()
)
metrics = [
faithfulness,
answer_relevancy,
answer_correctness,
]
logger.info("评估数据集的性能")
evaluation_result = evaluate(
dataset=evaluation_ds,
metrics=metrics
)
logger.info("评估完成")
return evaluation_result
if __name__ == "__main__":
evaluator = LlamaIndexEvaluator()
_evaluation_result = evaluator.evaluate()
logger.info("评估结果如下:")
logger.info(_evaluation_result)
这里几乎不需要任何人工努力,因为每个实验结束后,评估结果会被提取并保存在一个名为evaluation_result.json
的文件里。正如前面所说,这一步也可以自动化。
[
{
"method": "语义切分",
"result": {
"忠实度": 0.7084,
"答案相关性": 0.9566,
"答案正确性": 0.6119
}
},
{
"method": "带混合搜索的语义切分",
"result": {
"忠实度": 0.8684,
"答案相关性": 0.9182,
"答案正确性": 0.6077
}
},
{
"method": "语义切分大语言模型再排序",
"result": {
"忠实度": 0.8575,
"答案相关性": 0.8180,
"答案正确性": 0.6219
}
},
{
"method": "主题节点解析",
"result": {
"忠实度": 0.6298,
"答案相关性": 0.9043,
"答案正确性": 0.6211
}
},
{
"method": "主题节点解析混合",
"result": {
"忠实度": 0.4632,
"答案相关性": 0.8973,
"答案正确性": 0.5576
}
},
{
"method": "主题节点解析大语言模型再排序",
"result": {
"忠实度": 0.6804,
"答案相关性": 0.9028,
"答案正确性": 0.5935
}
},
{
"method": "语义双重合并切分",
"result": {
"忠实度": 0.7210,
"答案相关性": 0.8049,
"答案正确性": 0.5340
}
},
{
"method": "带混合的语义双重合并切分",
"result": {
"忠实度": 0.7634,
"答案相关性": 0.9346,
"答案正确性": 0.5266
}
},
{
"method": "语义双重合并切分大语言模型再排序",
"result": {
"忠实度": 0.7450,
"答案相关性": 0.9424,
"答案正确性": 0.5527
}
}
]
现在是时候将我们收集的评估结果可视化,以便确定在我们的RAG pipeline中使用哪些参数,从而为客户带来最高的准确性。
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# 提供的数据集
data = [
{"method": "semantic_chunking", "result": {"faithfulness": 0.7084, "answer_relevancy": 0.9566, "answer_correctness": 0.6119}},
{"method": "semantic_chunking_with_hybrid_search", "result": {"faithfulness": 0.8684, "answer_relevancy": 0.9182, "answer_correctness": 0.6077}},
{"method": "semantic_chunking_with_llm_reranking", "result": {"faithfulness": 0.8575, "answer_relevancy": 0.8180, "answer_correctness": 0.6219}},
{"method": "topic_node_parser", "result": {"faithfulness": 0.6298, "answer_relevancy": 0.9043, "answer_correctness": 0.6211}},
{"method": "topic_node_parser_hybrid", "result": {"faithfulness": 0.4632, "answer_relevancy": 0.8973, "answer_correctness": 0.5576}},
{"method": "topic_node_parser_with_llm_reranking", "result": {"faithfulness": 0.6804, "answer_relevancy": 0.9028, "answer_correctness": 0.5935}},
{"method": "semantic_double_merge_chunking", "result": {"faithfulness": 0.7210, "answer_relevancy": 0.8049, "answer_correctness": 0.5340}},
{"method": "semantic_double_merge_chunking_hybrid", "result": {"faithfulness": 0.7634, "answer_relevancy": 0.9346, "answer_correctness": 0.5266}},
{"method": "semantic_double_merge_chunking_with_llm_reranking", "result": {"faithfulness": 0.7450, "answer_relevancy": 0.9424, "answer_correctness": 0.5527}},
]
# 展平数据以创建DataFrame
flattened_data = []
for d in data:
row = {"method": d["method"]}
row.update(d["result"])
flattened_data.append(row)
# 创建DataFrame
df = pd.DataFrame(flattened_data)
# 创建每个图的数据子集
df1 = df.iloc[:3]
df2 = df.iloc[3:6]
df3 = df.iloc[6:]
# 设置seaborn样式
sns.set(style="whitegrid")
# 绘制每个方法的指标,分别绘制为分组条形图,类似于提供的示例图像
fig, axes = plt.subplots(3, 1, figsize=(10, 18))
# 绘制每个方法集,每个指标为条形图
for i, (df_subset, ax, title) in enumerate(zip([df1, df2, df3], axes,
["语义分块法的表现",
"主题节点解析法的表现",
"语义双重合并分块法的表现"])):
df_melted = df_subset.melt(id_vars='method', var_name='metric', value_name='score')
sns.barplot(x='method', y='score', hue='metric', data=df_melted, ax=ax, palette='Blues')
ax.set_title(title)
ax.set_ylabel("得分")
ax.tick_params(axis='x', rotation=45)
ax.legend(title='各项指标')
# 调整布局以增加间距
plt.tight_layout()
plt.show()
结果与假设如下:
上面我们做的实验结果。
对于这个实验,我们保持了许多因素不变,例如将 chunk_size 设为“128”,chunk_overlap 设为“20”,top_k 设为 5,dense_embedding_model 设为“nomic-embed-text:latest”,sparse_embedding_model 设为“Qdrant/bm42-all-minilm-l6-v2-attentions”。虽然这听起来更复杂,但我们可以通过上述说明的程序进行组合,肯定能比简单的 RAG 管道带来更好的效果。
�综上所述:在评估各种分块策略和混合方法时,带有混合搜索的语义分块始终表现出更佳的忠实度和答案的相关性。然而,带有LLM重排序的语义分块在正确性方面略胜一筹。主题节点解析方法显示稳定的性能,但在忠实度方面稍显不足,而语义双重合并技术在所有指标上保持了平衡。这些洞察为改进RAG管道提供了实用的指导,其中混合搜索策略尤为关键,混合搜索策略脱颖而出,成为提高相关性和正确性的关键增强手段。
虽然在这个实验中保持了一些参数不变,但我们仍然有很大的实验潜力。通过探索这些参数的不同组合方式以及之前讨论的方法,我们可以开发一个更复杂且高度优化的RAG管道,其性能有望超过当前方法。
共同学习,写下你的评论
评论加载中...
作者其他优质文章