为了账号安全,请及时绑定邮箱和手机立即绑定

基于LLamaIndex的高级检索增强查询技术:打造下一代搜索引擎

使用开源大规模语言模型 Zephyr-7b-alpha 和 BGE 词嵌入 bge-large-en-v1.5
欢迎加入高级RAG 📚学习系列!

深入了解检索增强生成的精彩世界,本系列文章将带你探讨前沿的技术和策略,以提升你对RAG应用的理解和掌握。通过阅读以下文章来提升你的技能水平,并不要错过 🔔后续文章,我们将继续深入探讨高级RAG的世界,解锁其无限可能。

不要错过任何发现!把这篇文章加入书签🏷️,经常来看看这个精彩的学习系列的最新一期。

截至目前我们讨论过的话题有:

  1. 使用LlamaIndex通过额外的上下文和元数据优化检索
  2. 使用LlamaIndex通过重排器增强检索效率
  3. 使用LlamaIndex进行查询增强以实现更高水平的搜索(您现在的位置!)
  4. 使用LlamaIndex智能跟踪和调试文档的更改

在信息检索领域中,Retriever-Augmented Generation (RAG) 模型已经带来了范式转变,使大型语言模型 (LLMs) 更有能力生成丰富且准确的上下文响应。然而,解锁 RAG 全部潜力的道路通常超出了其默认查询-检索-生成框架的局限。

这篇文章深入探讨了高级查询技术的变革力量,这些技术缩小了用户初次输入与庞大数据库中最为相关的信息之间的差距。

检索对齐的难题:如何让检索结果更准确?

查询转换的核心在于一个基本挑战:用户生成的提示往往缺乏与相关文档措辞相匹配的精确语言或结构。这种不对齐可能阻碍检索过程,导致即使是最先进的大型语言模型(LLM)也会给出次优的回复。查询转换在检索阶段前通过战略性修改查询,从而提高查询的相关性,并引导LLM更好地提取信息。

零样本任务的问题

最近的研究表明,将复杂的查询分解成更小、更易管理的步骤有很多好处,这种方法对于需要知识增强的复杂查询特别有效。然而,完全零样本密集检索系统由于缺乏相关性标签,仍然面临重大挑战。然而,先进的查询转换技术成为解决这些挑战的有潜力方法,提供了创新策略以应对这些挑战。

查询转换的想法是,检索器可能不会仅根据用户的初始问题来检索相关的文档。然而,它会修改查询,以增加其与我们资源的相关性,并将结果提供给语言模型。

用于增强RAG的技术有很多,这带来了额外的挑战,即决定何时应用每种技术。本文将分析5种强大的查询转换技术,并将看到它们如何帮助缩小检索差距,提高检索效率,并进行更高级的搜索

  1. 假设性文档嵌入(HyDE)
  2. 子问题查询工具
  3. 路由查询工具
  4. 一步查询拆分
  5. 多步查询拆分

知识与行动,齐头并进

这趟旅程不会仅仅停留在理论层面。每个技术旁边都有一个指向专门GitHub仓库的链接,该GitHub仓库包含代码示例和实现细节。

咱们直接开始吧。

开源的大规模语言模型和词嵌入技术

大型语言模型 (LLM):在这次探索中,我们将利用[Zephyr-7b-alpha]的强大功能,它是一个顶尖的开源大型语言模型,以其出色的文字理解和生成能力而闻名。

Embeddings :我们将使用BGE嵌入模型(bge-large-en-v1.5),这是一款通用的嵌入模型,能有效帮助进行语义搜索和知识抽取。

该嵌入模型在MTEB Embedding Benchmark上排名第五。同时,也可以去看看他们的代码库https://github.com/FlagOpen/FlagEmbedding

咱们直接看代码吧。

    import logging  
    import sys  

    logging.basicConfig(stream=sys.stdout, level=logging.INFO)  
    logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))  

    import json  
    import torch  
    from pathlib import Path  
    import pandas as pd  
    pd.set_option("display.max_colwidth", -1)  

    from copy import deepcopy  

    # transformers  
    from transformers import BitsAndBytesConfig  

    # llama_index  
    from llama_index.prompts import PromptTemplate  
    from llama_index.llms import HuggingFaceLLM  
    from llama_index import download_loader, Document, VectorStoreIndex, ServiceContext  
    from llama_index.node_parser import SentenceSplitter  
    from langchain.embeddings import HuggingFaceEmbeddings  

    from llama_index.indices.query.query_transform import HyDEQueryTransform  
    from llama_index.query_engine.transform_query_engine import TransformQueryEngine  

    from IPython.display import Markdown, display  
    from llama_index.response.notebook_utils import display_source_node  

    from llama_index.query_engine import RetrieverQueryEngine  
    from IPython.display import Markdown, display, HTML  
    from llama_index.retrievers import VectorIndexRetriever  

    from sentence_transformers import SentenceTransformer  

    DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"  # 设备设置为GPU或CPU
#加载数据段

PDFReader = download_loader("PDFReader")  
loader = PDFReader()  
docs = loader.load_data(file=Path("QLoRa.pdf"))  

# 创建片段
node_parser = SentenceSplitter(chunk_size=256)  
nodes = node_parser.get_nodes_from_documents(docs)
    #加载开源LLM(zephyr-7b-alpha)

    from google.colab import userdata

    # huggingface api token
    hf_token = userdata.get('hf_token')

    quantization_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_compute_dtype=torch.float16,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_use_double_quant=True,
    )

    def messages_to_prompt(messages):
      prompt = ""
      for message in messages:
        if message.role == 'system':
          prompt += f"<|system|>\n{message.content}\n"
        elif message.role == 'user':
          prompt += f"<|user|>\n{message.content}\n"
        elif message.role == 'assistant':
          prompt += f"<|assistant|>\n{message.content}\n"

      # 确保以system提示开始
      if not prompt.startswith("<|system|>\n"):
        prompt = "<|system|>\n\n" + prompt

      # 在末尾添加assistant提示
      prompt = prompt + "<|assistant|>\n"

      return prompt

    llm = HuggingFaceLLM(
        model_name="HuggingFaceH4/zephyr-7b-alpha",
        tokenizer_name="HuggingFaceH4/zephyr-7b-alpha",
        query_wrapper_prompt=PromptTemplate("<|system|>\n\n<|user|>\n{query_str}\n<|assistant|>\n"),
        context_window=3900,
        max_new_tokens=256,
        model_kwargs={"quantization_config": quantization_config},
        # tokenizer_kwargs={},
        generate_kwargs={"temperature": 0.7, "top_k": 50, "top_p": 0.95, "do_sample":True},
        messages_to_prompt=messages_to_prompt,
        device_map="auto",
    )
    #加载 Open Embedding 模型

    embed_model = HuggingFaceEmbeddings(model_name="BAAI/bge-large-en-v1.5")

在应用任何查询变换之前,首先配置索引和检索模块。

    # 服务上下文
    service_context = ServiceContext.from_defaults(llm=llm,  
                                                   embed_model=embed_model  
                                                   )  

    # 向量索引
    vector_index = VectorStoreIndex(  
        nodes, service_context=service_context  
    )

我们一个个来讨论并应用前面提到的查询转换。

1. 假设的文档嵌入(HyDE)。

HyDE — 机内详情

HyDE,假设文档嵌入法是一种新的密集型检索方法,包括两个不同的阶段:

1. 生成一个假设的回答

  • 与直接根据原始查询搜索相关文档不同,HyDE 首先构建一个可能回答查询的虚拟文档。
  • 这通过一个遵循指令的语言模型生成可能的响应来实现。
  • 虽然这个虚拟文档可能在细节上不完全准确,但它仍作为相关文档的一个有价值的例子,捕捉了相关性的核心。

2. 编码与检索

  • 假设文档随后由一个无监督对比编码器处理,编码器将其关键特征提炼成一个紧凑的嵌入向量。
  • 重要的是,密集的瓶颈起到了有损压缩的作用,过滤掉了无关的细节。
  • 然后将该嵌入向量与代表实际文档的语料库嵌入数据库进行比较。
  • 检索过程利用了文档与文档之间的相似性,这种相似性通过对比训练期间的内积进行编码,使得能够识别出与假设答案高度匹配的文档。
  • 最相似的文档被检索并作为查询的潜在响应展示,从而提高检索准确性。

咱们直接看代码吧。

首先,我们做原始查询。然后,用相同的查询字符串来做嵌入查找和生成摘要。

    query_str = "描述使用BFloat16作为计算数据类型与其他可能选择之间的优缺点。会选择一个还是另一个?"  

    query_engine = vector_index.as_query_engine()  
    response = query_engine.query(query_str)  
    display(Markdown(f"{response}"))

请提供需要翻译的英文文本。

未启用HyDe时的响应

我们试试HyDe转换,看看结果如何。

    hyde = HyDE查询变换(include_original=True, llm=llm)  
    hyde_query_engine = 转换查询引擎(query_engine, hyde)  
    response = hyde_query_engine.query(query_str)  
    响应 = hyde_query_engine.query(查询字符串)  
    显示(Markdown(f"{响应}"))

解释:

    hyde = HyDEQueryTransform(include_original=True, llm=llm)  # 初始化HyDE查询变换,包含原始数据,使用大模型llm  
    hyde_query_engine = TransformQueryEngine(query_engine, hyde)  # 使用查询引擎和HyDE查询变换初始化转换查询引擎  
    response = hyde_query_engine.query(query_str)  # 使用查询字符串查询,得到响应  
    显示(Markdown(f"{响应}"))  # 显示响应的Markdown格式

无文本可翻译。

用HyDe来回应

我们来看一下这个假设的文档。我们使用 HyDEQueryTransform 生成一个假设文档,并用它来进行嵌入查询。

    query_bundle = hyde(query_str)  # 获取查询结果的捆绑包
    hyde文档 = query_bundle.嵌入字符串[0]  # 提取捆绑包中的第一个嵌入字符串
    hyde_doc  # 返回或使用提取的hyde文档,以便后续处理

hyde_doc

结论 — 你可以看到,HyDE 通过有效地准确想象,不仅显著提高了输出质量,还改善了语义嵌入质量,从而提升了最终输出。

访问 GitHub 代码库

2. 子查询引擎

了解传统的查询系统

普通的查询引擎是为了在海量数据中帮你找信息的。它们架起用户提问和内部数据库之间的桥梁。当你提出一个查询,引擎会仔细分析并找到最相关的信息,然后给你一个全面的答案。

传统查询系统的某些局限性

传统查询引擎在处理简单问题时表现出色,但在面对涉及多个文档的复杂问题时,常常会遇到挑战。

仅仅简单地合并文档并通过提取 top k 元素来分析,常常无法捕捉到真正有信息量的回答所需要的细微差别。

进入子问题查询引擎吧

分解策略:为了应对这种复杂性,子问题查询引擎采用了一种分而治之的策略。它们将复杂查询巧妙地分解成一系列子问题,每个子问题专注于原始查询的特定方面。

实现涉及为每个数据源定义一个子问题查询引擎。该引擎不会平等对待所有文档,而是针对每个数据源特有的子问题进行战略性处理。然后,使用顶层的子问题查询引擎来综合各个子问题的结果,以生成最终答案。

面对初始的复杂问题,我们利用大语言模型来生成子问题,并在选定的数据源上执行这些子问题。然后收集所有子问题的回答,最后合成最终的答案。

让我们直接看代码吧。

    从 llama_index.tools 导入 QueryEngineTool, ToolMetadata  
    从 llama_index.query_engine 导入 SubQuestionQueryEngine  
    从 llama_index.callbacks 导入 CallbackManager, LlamaDebugHandler  

    导入 nest_asyncio 并应用  
    nest_asyncio.apply()  

    # 使用 LlamaDebugHandler 捕获并打印由 SUB_QUESTION 事件类型触发的子问题跟踪  
    llama_debug = LlamaDebugHandler(print_trace_on_end=True)  
    callback_manager = CallbackManager([llama_debug])  

    # 服务上下文  
    service_context = ServiceContext.from_defaults 方法(llm=llm,  
                                                   embed_model=embed_model,  
                                                   callback_manager=callback_manager  
                                                   )  

    # 创建向量查询引擎  
    vector_query_engine = VectorStoreIndex.from_documents(  
        docs, service_context=service_context, use_async=True  
    ).as_query_engine()

咱们来构建一个子问题查询引擎,然后运行一些查询吧!

    # 设置基础查询引擎作为工具  
    query_engine_tools = [  
        QueryEngineTool(  
            query_engine=vector_query_engine,  
            metadata=ToolMetadata(  
                name="qlora_paper",  
                description="量化大语言模型的高效微调",  
            ),  
        ),  
    ]  

    query_engine = SubQuestionQueryEngine.from_defaults(  
        query_engine_tools=query_engine_tools,  
        service_context=service_context,  
        use_async=True,  
    )  

    response = query_engine.query("描述使用BFloat16作为计算数据类型与其他可能选择之间的权衡。在什么情况下会选择这种而不是那种?")

以下是生成的子问题如下:

回复:

子问题查询引擎回复

访问 GitHub 代码库

3. 路由查询引擎

现在,我们定义一下路由查询引擎,它从多个候选中选择一个来执行查询。

路由器查询工具

路由查询引擎是一个强大的决策模块,在根据用户查询和元数据定义的选项选择最合适的选择时起着关键作用。这些路由查询引擎是多功能模块,可以独立作为“选择模块”运行,也可以作为查询引擎或检索器,叠加在其他查询引擎或检索器之上。

路由器在多种应用场景中表现出色,包括从多种选项中选择合适的数据源,并根据用户的查询决定是执行摘要还是语义搜索。它们还能应对更复杂的任务,比如同时尝试多种选择并利用多路由功能整合结果。

我们也定义了一个“selector”。用户可以轻松地将路由器作为查询引擎或检索器使用,路由器负责选择合适的查询引擎或检索器来有效处理用户查询。

这里有几种选择器可用,每个都有它自己特定的属性。

  1. LLM 选择器 使用 LLM 输出一个 JSON,这个 JSON 被解析后,用来查询相应的索引。
  2. Pydantic 选择器(目前仅支持 gpt-4 和 gpt-3.5(默认))使用 OpenAI 函数调用 API 生成 pydantic 选择对象,而不是解析原始 JSON。
  3. 对于每种选择器类型,还可以选择路由到一个或多个索引。
  4. 然后,定义 RouterQueryEngine 时,可以选择所需的 selector 模块。在这里,我们使用 LLMSingleSelector,它选择一个底层查询引擎来路由查询。

让我们直接来看看代码吧。

我们将定义一个自定义的查询路由引擎,从多个候选查询引擎中挑选一个来执行查询任务。

    从 llama_index 导入 VectorStoreIndex, SummaryIndex, 和 SimpleKeywordTableIndex  

    service_context = ServiceContext.from_defaults(llm=llm,  
                                                   embed_model=embed_model  
    )  

    # 定义所有不同的索引,使用相同的数据源  

    # 向量存储索引  
    vector_index = VectorStoreIndex(  
        nodes, service_context=service_context  
    )  

    # 概要索引  
    summary_index = SummaryIndex(  
        nodes, service_context=service_context  
    )  

    # 关键词表索引  
    keyword_index = SimpleKeywordTableIndex(nodes, service_context=service_context)

接下来,我们给每个索引定义查询引擎。然后用QueryEngineTool把这些引擎包装起来。

    summary_query_engine = summary_index.as_query_engine(response_mode="树状总结", service_context=service_context)  
    vector_query_engine = vector_index.as_query_engine(service_context=service_context)  
    keyword_query_engine = keyword_index.as_query_engine(service_context=service_context)
    from llama_index.tools.query_engine import QueryEngineTool  

    summary_tool = QueryEngineTool.from_defaults(  
        query_engine=summary_query_engine,  
        description=(  
            "适用于Efficient Finetuning QLORA研究论文的总结相关问题"  
        ),  
    )  

    vector_tool = QueryEngineTool.from_defaults(  
        query_engine=vector_query_engine,  
        description=(  
            "用于从QLORA研究论文中检索与Efficient Finetuning相关的特定内容"  
        ),  
    )  

    keyword_tool = QueryEngineTool.from_defaults(  
        query_engine=keyword_query_engine,  
        description=(  
            "使用查询中提到的实体从QLORA研究论文中检索与Efficient Finetuning相关的特定信息更为适用"  
        ),  
    )

然后,我们将使用LLM selectors,它用于解析生成的JSON,以便为路由选择一个子索引。

LLMSingleSelector <!-- 用于选择单个项目的工具或函数 -->

     # 单一LLM选择器

    from llama_index.query_engine.router_query_engine import RouterQueryEngine  
    from llama_index.selectors.llm_selectors import LLMSingleSelector, LLMMultiSelector

    router_query_engine = RouterQueryEngine(  
        selector=LLMSingleSelector.from_defaults(service_context=service_context),  
        query_engine_tools=[  
            summary_tool,  
            vector_tool,  
            keyword_tool,  
        ],  
        service_context=service_context,  
    )  

    response = router_query_engine.query("什么是Double Quantization? # 双重量化")

无内容可翻译。

响应路由器查询引擎

LLMMultiSelector(多选器)

如果我们想将查询路由到多个索引,我们就可以使用多选器。多选器将查询发送给多个子索引库,然后汇总所有响应,以形成完整答案。

    router_query_engine  = RouterQueryEngine(  
        selector=LLMMultiSelector.from_defaults(service_context=service_context),  # 选择器从默认值中创建,service_context参数已指定
        query_engine_tools=[  
            summary_tool,  # 摘要工具
            vector_tool,   # 向量工具
            keyword_tool,  # 关键词工具
        ],  
        service_context=service_context,  # 服务上下文
    )  

    # 打印响应内容
    print(str(response))  # response是需要打印的内容

链接到 GitHub代码库

4. 一步查询分解

最近的研究显示,当大型语言模型将复杂问题拆分成更小、更易处理的步骤时,它们表现更佳。面对复杂查询时,知识库的不同部分可能与回答这些“子查询”有关,这些子查询属于整体问题的一部分。单步查询转换则承认这一点,并试图独立解决每个子查询。

单步查询分解功能旨在将复杂的问题转化为更简单的问题,专门用于从数据集中提取相关的信息。通过将原始问题拆解为更小且更集中的子查询,模型可以给出子答案,这些子答案共同解决了原始问题的复杂性,从而更好地回答了原始问题。

来自LlamaIndex文档的图片:

5. 多步骤查询分解.

这种方法基于语言模型在给出对原问题的答案前,向自己提问并回答自己的后续问题的概念。其目的是让模型能够无缝地整合它独立学到的信息。

这个模型将零散的事实联系在一起,综合见解,并揭示了在单一方法中可能被忽视的关系,使得这些关系不再被隐藏。

因此,多步查询变换克服了大语言模型的一个常见限制:难以将单独的事实结合起来形成新的结论。通过逐步探索知识,该模型揭示了可能被忽略的关联。

来自 LlamaIndex 文档的图片

让我们直接看代码吧。

    从 llama_index.indices.query.query_transform.base 导入 StepDecomposeQueryTransform 作为 StepDecomposeQueryTransform  
    从 llama_index.query_engine.multistep_query_engine 导入 MultiStepQueryEngine  

    # 注释: 设置日志为 DEBUG 以获取更详细的输出  

    step_decompose_transform = StepDecomposeQueryTransform(llm=llm, verbose=True)  
    query_engine = vector_index.as_query_engine(service_context=service_context)  # vector_index 和 service_context 的定义请参考上下文  

    query_engine = MultiStepQueryEngine(  
        query_engine=query_engine,  
        query_transform=step_decompose_transform  
    )

提示:当我运行MultiStepQueryEngine时,遇到了一个值错误,表示无法加载OpenAI模型。

看起来目前 MultiStepQueryEngine 只支持 OpenAI 的 GPT-4GPT-3.5。我会继续关注这个领域并根据需要更新代码。我也会将这个问题保持开放,欢迎大家在评论区分享你们的看法或想法。

哪个人适合我?

无论是子问题查询引擎还是RAG中的单步或多步查询分解,它们都处理复杂查询,但它们处理这些复杂查询的方式不同。

子查询引擎

它专注于分治法的方法。它将复杂的查询分解为一系列较小的、具体的子查询。每个子查询都被发送到专门的子查询引擎,该引擎从其特定的数据源检索相关信息,然后返回结果。

因此,它确保每个子问题都有合适的数据来源,从而得出更准确的结果。它汇总各个子问题的洞察,提供全面的回答,从而给出一个整体的答案。

单步查询/多步查询

它侧重于逐步细化查询。它将复杂的查询分解为中间步骤,通过逐步获取的信息丰富搜索过程。每一步都根据当前的查询状态来搜索相关文档,并用提取的知识更新当前查询。

因此,它通过在每一步优化查询来避免重复检索,从而避免冗余。

结论部分

这并不是终点,而是一个令人兴奋的起点。这项高级查询增强的探索虽然文中介绍的方法带来了变革性的力量,但信息检索的边界却在不断拓展。

未来的研究可能会深入探索混合方法,结合这些技术以带来更大的协同效应。我们可能会看到个性化转型的兴起,更好地满足个人用户的需求和偏好。最终,弥合检索缺口的旅程将持续进行,由创新推动,旨在将用户与最准确和有洞察力的信息紧密相连,无论信息存在于何地。

查看 Github 页面上的完整代码

[LLMs-playground/LlamaIndex-applications/Advanced-RAG/advanced_query_transformations/Advanced_Query_T…了解LLMs的基本原理、应用场景及实现方法。点击链接在GitHub上贡献代码:GitHub链接]

如需了解更多先进的RAG方法,请参阅此代码库。

LLMs-playground/LlamaIndex-applications/Advanced-RAG 项目中的 What, Why 和 How。通过在 GitHub 上创建一个账户,你也可以参与到 akashmathur-2212/LLMs-playground 的开发中。

感谢你读了我的这篇文章,希望它能让你的知识库更加丰富!在你离开之前,如果你喜欢这篇文章,不妨

👉 记得鼓掌,关注我哦,有任何反馈也告诉我。

👉 我使用大型语言模型(LLM)构建了多种功能的生成式AI应用,涉及了先进的RAG概念以及用于大数据处理的无服务器AWS架构。欢迎查看项目并在GitHub上给它点个星⭐

👉 可以关注我:LinkedIn |GitHub |Medium |作品

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
PHP开发工程师
手记
粉丝
10
获赞与收藏
54

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消