【2024构建RAG问答系统】深度解析LlamaIndex自动合并检索框架,大语言模型必知

avatar
作者
筋斗云
阅读量:0

🔍 📱 嘿,Chatbot爱好者!今天我们来深入解析一个超强的文档检索和问答系统框架!🚀

🧠 你是否曾经面对海量文档感到头痛?需要快速精准回答问题却不知从何下手?别担心,这个基于LlamaIndex和TruLens的框架就是你的救星!

🔍 本文将为你揭秘:

  1. 如何构建多层次智能索引
  2. 自动合并检索的魔法
  3. 精准重排序的秘诀
  4. 性能评估的必杀技

🌟 无论你是工程师、数据科学家,还是对智能问答系统感兴趣的极客,这篇文章都能让你收获满满!

🏗️ 首先是框架整体设计

  1. 索引构建 - 文档的多重变身

    • 使用HierarchicalNodeParser进行文档分层
    • VectorStoreIndex创建向量存储
    • 索引持久化,方便随时调用
  2. 查询引擎 - 智能检索的核心

    • 基础检索 + 自动合并 = 超强理解力
    • SentenceTransformerRerank保证结果精准度
  3. 评估系统 - 性能监控利器

    • TruLens记录每一次查询
    • 可视化仪表板直观展示系统表现

💡 核心组件解析:

🌳 HierarchicalNodeParser - 文档分层的秘密武器

node_parser = HierarchicalNodeParser.from_defaults(chunk_sizes=chunk_sizes) nodes = node_parser.get_nodes_from_documents(documents) leaf_nodes = get_leaf_nodes(nodes) 

这个解析器就像一个超级智能的剪刀✂️,它能将文档切分成不同大小的片段。想象一下,你有一本厚厚的百科全书:

  • 2048字符可能对应一个章节
  • 512字符可能是一个小节
  • 128字符可能就是一两个段落

为什么要这样做?因为不同的问题需要不同粒度的信息!比如"解释相对论"可能需要整个章节,而"爱因斯坦出生年份"可能一个句子就够了。

🔄 AutoMergingRetriever - 自动合并的魔法师

retriever = AutoMergingRetriever(     base_retriever, automerging_index.storage_context, verbose=True ) 

这个检索器简直就是一个魔法师🧙‍♂️!它不仅能找到相关的文本片段,还能自动将它们合并。想象一下,你问"苹果公司的发展历程",它能自动将"乔布斯创立苹果"、"iPhone发布"、"Tim Cook接任CEO"这些片段智能地组合在一起!

📊 VectorStoreIndex - 高效检索的幕后英雄

automerging_index = VectorStoreIndex(     leaf_nodes, storage_context=storage_context, service_context=merging_context ) 

这个索引就像一个超级图书管理员👨‍💼,它将所有文本片段转化为向量,并用一种特殊的方式存储。当你问问题时,它能以光速找到最相关的片段!

🔎 SentenceTransformerRerank - 结果精炼大师

rerank = SentenceTransformerRerank(     top_n=rerank_top_n, model="BAAI/bge-reranker-base" ) 

这个组件就像一个严格的评委,它会对初步检索的结果进行二次评判,只保留最相关的部分。这就保证了回答的精准性!

🌟 为什么这个框架如此强大?

  • 多层级索引:通过创建不同层次的索引(比如两层和三层),我们可以比较哪种结构更适合我们的数据和问题类型。
auto_merging_index_0 = build_automerging_index(     documents,     llm=OpenAI(model="gpt-3.5-turbo", temperature=0.1),     embed_model="local:BAAI/bge-small-en-v1.5",     save_dir="merging_index_0",     chunk_sizes=[2048,512], )  auto_merging_index_1 = build_automerging_index(     documents,     llm=OpenAI(model="gpt-3.5-turbo", temperature=0.1),     embed_model="local:BAAI/bge-small-en-v1.5",     save_dir="merging_index_1",     chunk_sizes=[2048,512,128], ) 
  • 自动合并检索:不用担心文本片段太小,系统会自动合并相关片段!

  • 重排序机制:通过similarity_top_krerank_top_n参数,我们可以微调检索的精度和效率。

auto_merging_engine_0 = get_automerging_query_engine(     auto_merging_index_0,     similarity_top_k=12,     rerank_top_n=6, ) 
  • 集成评估:使用TruLens进行系统评估,让你的系统性能一目了然!

tru_recorder = get_prebuilt_trulens_recorder(     auto_merging_engine_0,     app_id ='app_0' )  run_evals(eval_questions, tru_recorder, auto_merging_engine_0) 

🚀 实战小贴士:

  1. 根据你的文档类型调整chunk_sizes。长文档可能需要更多层级。
  2. 实验不同的similarity_top_krerank_top_n值,找到最佳平衡点。
  3. 使用TruLens仪表板比较不同配置的性能,持续优化你的系统。

🎉 总结:
这个框架不仅强大,而且高度可定制。无论你是处理科技文档、法律合同还是文学作品,它都能帮你构建一个智能、高效的问答系统。

为什么要创建不同层次索引?

  1. 创建不同层次索引的原因:

     

    a) 性能优化:不同的文档类型和查询需求可能需要不同的索引结构。
    b) 平衡精度和效率:层次越多,索引越精细,但计算开销也越大。
    c) 适应性研究:了解哪种索引结构最适合特定的文档集和查询类型。

  2. 原理:

     

    a) 粒度控制:不同层次提供不同粒度的文本块。
    b) 上下文保留:多层结构有助于在检索时保留更多上下文信息。
    c) 检索灵活性:可以根据查询复杂度选择合适层次的文本块。

  3. 举例说明:

假设我们有一本关于世界历史的教科书。

两层索引 [2048, 512]:

  • 第一层(2048字符)可能对应一个小节。
  • 第二层(512字符)可能对应一个段落。

三层索引 [2048, 512, 128]:

  • 第一层(2048字符)对应一个小节。
  • 第二层(512字符)对应一个段落。
  • 第三层(128字符)可能对应1-2个句子。

查询示例:

  1. "简述第二次世界大战的主要原因"

    • 两层索引可能足够,因为这是一个宽泛的问题,可能需要整个段落的信息。
  2. "希特勒在哪一年入侵波兰?"

    • 三层索引可能更有效,因为这个具体的事实可能只需要1-2个句子就能回答。

通过比较不同层次索引的性能,我们可以找到最适合这本历史教科书的索引结构。

现在,解释这行代码:

auto_merging_engine_0 = get_automerging_query_engine(     auto_merging_index_0,      similarity_top_k=12,      rerank_top_n=6 ) 

这行代码创建了一个查询引擎,具体解释如下:

  1. auto_merging_index_0:这是之前创建的自动合并索引。

  2. similarity_top_k=12:这个参数指定初始检索时要返回的最相似文本块的数量。在这里,系统会首先检索12个最相似的文本块。

  3. rerank_top_n=6:这个参数指定在重新排序后要保留的top相关文本块的数量。系统会对初始检索的12个文本块进行重新排序,然后只保留最相关的6个。

  4. get_automerging_query_engine:这个函数创建了一个查询引擎,它使用自动合并检索器和重排序后处理器。

工作流程:

  1. 当收到查询时,引擎首先检索12个最相似的文本块。
  2. 然后,它可能会合并一些相关的文本块(这是AutoMergingRetriever的功能)。
  3. 接着,它会对这些文本块进行重新排序。
  4. 最后,它只保留排序后的前6个文本块用于生成回答。

这种方法的优点是它在广泛搜索(12个初始结果)和精确答案(6个最终结果)之间取得了平衡,有助于提高回答的相关性和准确性。

节点解析器Nodeparser详解

节点解析器(Node Parser)的概念、作用和工作原理:

  1. 节点解析器的概念:
    节点解析器是一个工具,用于将大型文档分割成更小、更易管理的部分,这些部分被称为"节点"。在LlamaIndex中,节点是文档的基本单位,包含文本内容和元数据。

  2. 节点解析器的作用:

    • 文档分割: 将大文档切分成小块,便于后续处理和检索。
    • 层次化结构: 创建文档的层次结构,保留文档的原始结构和上下文关系。
    • 元数据添加: 为每个节点添加相关的元数据,如位置信息、标题等。
  3. HierarchicalNodeParser的工作原理:

    • 多层次分割: 按照指定的chunk_sizes列表,从大到小依次分割文档。
    • 创建层次结构: 较大的块成为父节点,较小的块成为子节点,形成树状结构。
    • 保留上下文: 每个子节点都知道其父节点,有助于在检索时提供更多上下文
  4. node_parser = HierarchicalNodeParser.from_defaults(     chunk_sizes=[2048, 512, 128] ) nodes = node_parser.get_nodes_from_documents([document]) 
    • 创建了一个三层的解析器,chunk_sizes分别是2048, 512, 和128个字符。
    • 首先,文档被分割成2048字符的大块。
    • 然后,这些大块被进一步分割成512字符的中等块。
    • 最后,中等块被分割成128字符的小块。
    • 这样形成了一个三层的树状结构。
  5. 为什么这样做很有用:

    • 灵活检索: 可以根据查询的具体需求,选择合适大小的文本块进行检索。
    • 上下文保留: 即使检索到很小的文本块,也可以通过其父节点快速获取更多上下文。
    • 提高准确性: 多层次的结构有助于在不同粒度上匹配查询,提高检索的准确性。

通过使用HierarchicalNodeParser,我们可以更有效地组织和检索大型文档中的信息,既保留了文档的整体结构,又提供了灵活的检索粒度。


完整代码

 import os  from llama_index import (     ServiceContext,     StorageContext,     VectorStoreIndex,     load_index_from_storage, ) from llama_index.node_parser import HierarchicalNodeParser from llama_index.node_parser import get_leaf_nodes from llama_index import StorageContext, load_index_from_storage from llama_index.retrievers import AutoMergingRetriever from llama_index.indices.postprocessor import SentenceTransformerRerank from llama_index.query_engine import RetrieverQueryEngine   def build_automerging_index(     documents,     llm,     embed_model="local:BAAI/bge-small-en-v1.5",     save_dir="merging_index",     chunk_sizes=None, ):     chunk_sizes = chunk_sizes or [2048, 512, 128]     node_parser = HierarchicalNodeParser.from_defaults(chunk_sizes=chunk_sizes)     nodes = node_parser.get_nodes_from_documents(documents)     leaf_nodes = get_leaf_nodes(nodes)     merging_context = ServiceContext.from_defaults(         llm=llm,         embed_model=embed_model,     )     storage_context = StorageContext.from_defaults()     storage_context.docstore.add_documents(nodes)      if not os.path.exists(save_dir):         automerging_index = VectorStoreIndex(             leaf_nodes, storage_context=storage_context, service_context=merging_context         )         automerging_index.storage_context.persist(persist_dir=save_dir)     else:         automerging_index = load_index_from_storage(             StorageContext.from_defaults(persist_dir=save_dir),             service_context=merging_context,         )     return automerging_index   def get_automerging_query_engine(     automerging_index,     similarity_top_k=12,     rerank_top_n=6, ):     base_retriever = automerging_index.as_retriever(similarity_top_k=similarity_top_k)     retriever = AutoMergingRetriever(         base_retriever, automerging_index.storage_context, verbose=True     )     rerank = SentenceTransformerRerank(         top_n=rerank_top_n, model="BAAI/bge-reranker-base"     )     auto_merging_engine = RetrieverQueryEngine.from_args(         retriever, node_postprocessors=[rerank]     )     return auto_merging_engine import os ​ from llama_index import (     ServiceContext,     StorageContext,     VectorStoreIndex,     load_index_from_storage, ) from llama_index.node_parser import HierarchicalNodeParser from llama_index.node_parser import get_leaf_nodes from llama_index import StorageContext, load_index_from_storage from llama_index.retrievers import AutoMergingRetriever from llama_index.indices.postprocessor import SentenceTransformerRerank from llama_index.query_engine import RetrieverQueryEngine ​ ​ def build_automerging_index(     documents,     llm,     embed_model="local:BAAI/bge-small-en-v1.5",     save_dir="merging_index",     chunk_sizes=None, ):     chunk_sizes = chunk_sizes or [2048, 512, 128]     node_parser = HierarchicalNodeParser.from_defaults(chunk_sizes=chunk_sizes)     nodes = node_parser.get_nodes_from_documents(documents)     leaf_nodes = get_leaf_nodes(nodes)     merging_context = ServiceContext.from_defaults(         llm=llm,         embed_model=embed_model,     )     storage_context = StorageContext.from_defaults()     storage_context.docstore.add_documents(nodes) ​     if not os.path.exists(save_dir):         automerging_index = VectorStoreIndex(             leaf_nodes, storage_context=storage_context, service_context=merging_context         )         automerging_index.storage_context.persist(persist_dir=save_dir)     else:         automerging_index = load_index_from_storage(             StorageContext.from_defaults(persist_dir=save_dir),             service_context=merging_context,         )     return automerging_index ​ ​ def get_automerging_query_engine(     automerging_index,     similarity_top_k=12,     rerank_top_n=6, ):     base_retriever = automerging_index.as_retriever(similarity_top_k=similarity_top_k)     retriever = AutoMergingRetriever(         base_retriever, automerging_index.storage_context, verbose=True     )     rerank = SentenceTransformerRerank(         top_n=rerank_top_n, model="BAAI/bge-reranker-base"     )     auto_merging_engine = RetrieverQueryEngine.from_args(         retriever, node_postprocessors=[rerank]     )     return auto_merging_engine from llama_index.llms import OpenAI ​ index = build_automerging_index(     [document],     llm=OpenAI(model="gpt-3.5-turbo", temperature=0.1),     save_dir="./merging_index", ) ​ query_engine = get_automerging_query_engine(index, similarity_top_k=6) TruLens Evaluation from trulens_eval import Tru ​ Tru().reset_database() Two layers auto_merging_index_0 = build_automerging_index(     documents,     llm=OpenAI(model="gpt-3.5-turbo", temperature=0.1),     embed_model="local:BAAI/bge-small-en-v1.5",     save_dir="merging_index_0",     chunk_sizes=[2048,512], ) auto_merging_engine_0 = get_automerging_query_engine(     auto_merging_index_0,     similarity_top_k=12,     rerank_top_n=6, ) from utils import get_prebuilt_trulens_recorder ​ tru_recorder = get_prebuilt_trulens_recorder(     auto_merging_engine_0,     app_id ='app_0' ) eval_questions = [] with open('generated_questions.text', 'r') as file:     for line in file:         # Remove newline character and convert to integer         item = line.strip()         eval_questions.append(item) def run_evals(eval_questions, tru_recorder, query_engine):     for question in eval_questions:         with tru_recorder as recording:             response = query_engine.query(question) run_evals(eval_questions, tru_recorder, auto_merging_engine_0) from trulens_eval import Tru ​ Tru().get_leaderboard(app_ids=[]) Tru().run_dashboard() Three layers auto_merging_index_1 = build_automerging_index(     documents,     llm=OpenAI(model="gpt-3.5-turbo", temperature=0.1),     embed_model="local:BAAI/bge-small-en-v1.5",     save_dir="merging_index_1",     chunk_sizes=[2048,512,128], ) auto_merging_engine_1 = get_automerging_query_engine(     auto_merging_index_1,     similarity_top_k=12,     rerank_top_n=6, ) ​ tru_recorder = get_prebuilt_trulens_recorder(     auto_merging_engine_1,     app_id ='app_1' ) run_evals(eval_questions, tru_recorder, auto_merging_engine_1) from trulens_eval import Tru ​ Tru().get_leaderboard(app_ids=[]) Tru().run_dashboard() 

🔬 核心原理解析

  1. 多层次索引:
    想象你在整理一本百科全书。你可能会先分章节(2048字符),再分小节(512字符),最后到段落(128字符)。这就是多层次索引的原理!它让系统能够根据问题的具体情况,选择最合适的"阅读范围"。

  2. 自动合并检索:
    这就像是一个超级阅读理解高手。它不仅能找到相关的段落,还能自动将多个段落组合起来,形成一个完整的答案。再也不用担心答非所问啦!

  3. 重排序机制:
    这相当于给检索结果做最后一道筛选。通过精确的语义理解,它能将最相关的信息排在最前面,确保回答的质量。

💡 使用小贴士

  1. 根据你的文档类型调整chunk_sizes。科技文档可能需要更细的划分,而文学作品可能需要保留更多上下文。

  2. 多尝试不同的similarity_top_k和rerank_top_n值。这就像调整显微镜的焦距,找到最清晰的图像!

  3. 充分利用TruLens仪表板。它就像你的AI助手的"体检报告",告诉你哪里表现出色,哪里需要改进。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!