提升结果的高级 RAG 相关技术

177 阅读51分钟

在本章的最后,我们将探讨几种高级技术,以提升检索增强生成(RAG)应用程序的效果。这些技术超越了基础的 RAG 方法,旨在解决更复杂的挑战并实现更好的结果。我们的出发点是我们在之前章节中使用过的技术。我们将基于这些技术,了解它们的局限性,然后引入新的技术来弥补这些不足,从而将您的 RAG 实践提升到新的水平。

在本章中,您将通过一系列代码实验,亲自体验如何实施这些高级技术。我们的主题将包括以下内容:

  • 基础 RAG 及其局限性
  • 混合 RAG/多向量 RAG 提升检索效果
  • 混合 RAG 中的重排序
  • 代码实验 14.1 – 查询扩展
  • 代码实验 14.2 – 查询分解
  • 代码实验 14.3 – 多模态 RAG (MM-RAG)
  • 其他值得探索的高级 RAG 技术

这些技术通过增强查询、将问题拆解为子问题以及结合多种数据模态来增强检索和生成能力。我们还将讨论一系列其他的高级 RAG 技术,涉及索引、检索、生成及整个 RAG 流水线的优化。我们将从基础 RAG 讨论开始,这是我们在第二章中回顾过的主要 RAG 方法,您现在应该已经非常熟悉了。

技术要求

本章的代码可以在以下 GitHub 仓库中找到:
github.com/PacktPublis…

基础 RAG 及其局限性

到目前为止,我们已经使用了三种类型的 RAG 方法:基础 RAG、混合 RAG 和重排序。最初,我们使用的是所谓的基础 RAG。这是我们在第二章的启动代码中使用的基本 RAG 方法,也是之后多个代码实验中使用的基础。基础 RAG 模型是 RAG 技术的初始版本,提供了将检索机制与生成模型结合的基础框架,尽管它在灵活性和可扩展性方面存在一些局限。

基础 RAG 检索了许多碎片化的上下文块,这些是我们将文本向量化后放入 LLM 上下文窗口的块。如果你使用的文本块不够大,所得到的上下文就会更碎片化。这种碎片化会导致上下文和语义的理解和捕捉下降,从而降低 RAG 应用中检索机制的有效性。在典型的基础 RAG 应用中,你通常使用某种类型的语义搜索,因此仅依赖这种搜索类型会暴露这些局限性。因此,我们引入了更先进的检索方法:混合检索。

混合 RAG/多向量 RAG 提升检索效果

混合 RAG 扩展了基础 RAG 的概念,通过利用多个向量进行检索,而不是仅仅依赖于查询和文档的单一向量表示。在第 8 章中,我们深入探讨了混合 RAG,并通过代码实现了这一方法,不仅使用了 LangChain 中推荐的机制,还自己重新创建了这个机制,以便了解其内部工作原理。混合 RAG,也叫做多向量 RAG,可以不仅仅涉及语义和关键词搜索(如我们在代码实验中所见),而是可以结合多种不同的向量检索技术,这些技术在你的 RAG 应用中都有意义。

我们的混合 RAG 代码实验引入了关键词搜索,扩展了我们的搜索能力,尤其是在处理上下文较弱的内容时(如姓名、代码、内部缩写等类似文本)。这种多向量方法让我们可以考虑查询和数据库内容的更广泛方面。这反过来可以提高检索结果的相关性和准确性,从而支持生成过程。这使得生成的内容不仅更相关和信息丰富,而且更符合输入查询的细节。多向量 RAG 在需要高精度和细致度的生成内容应用中尤其有用,比如技术写作、学术研究辅助、包含大量内部代码和实体引用的公司内部文档,以及复杂的问答系统。

但多向量 RAG 并不是我们在第 8 章中探讨的唯一高级技术;我们还应用了重排序。

混合 RAG 中的重排序

在第 8 章中,除了混合 RAG 方法,我们还引入了一种重排序方法,这也是一种常见的高级 RAG 技术。在语义搜索和关键词搜索完成检索后,我们根据两者的排名情况对结果进行重排序,取决于它们是否出现在两者中,以及它们最初的排名。

现在,您已经了解了三种 RAG 技术,其中包括两种高级技术!但是本章的重点是带给你三种新的高级方法:查询扩展、查询分解和 MM-RAG。我们还将提供许多其他方法供你探索,但我们筛选并挑选了这三种高级 RAG 技术,因为它们在各种 RAG 应用中具有广泛的应用场景。

在本章的第一个代码实验中,我们将讨论查询扩展。

代码实验 14.1 – 查询扩展

本实验的代码可以在 GitHub 仓库的 CHAPTER14 目录中的 CHAPTER14-1_QUERY_EXPANSION.ipynb 文件中找到。

许多用于增强 RAG 的技术集中在提升某一方面,比如检索或生成,但查询扩展有潜力同时改进这两个方面。我们在第 13 章已经讨论过扩展的概念,但当时我们关注的是 LLM 输出。在这里,我们将这个概念聚焦于模型的输入,通过添加额外的关键词或短语来扩展原始提示。这种方法可以提升检索模型的理解能力,因为它为用户查询提供了更多上下文信息,从而提高了获取相关文档的可能性。通过改进检索,你已经在帮助改进生成过程,为生成提供了更好的上下文,但这种方法也有可能产生更有效的查询,从而帮助 LLM 提供更好的回应。

通常,查询扩展和答案的工作原理是:你将用户查询传递给 LLM,并通过一个提示来获取问题的初步答案,尽管此时你并未展示 LLM 通常在 RAG 应用中看到的上下文。从 LLM 的角度来看,这种类型的变化有助于扩大搜索范围,同时保持对原始意图的关注。

在创建 rag_chain_from_docs 链之前,我们在新的一行开始,接下来我们将引入多个提示模板来实现这一目标:

from langchain.prompts.chat import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate

让我们回顾一下这些提示模板及其用途:

  • ChatPromptTemplate 类:为创建基于聊天的提示提供模板,我们可以利用它将其他提示模板组合成一个更符合聊天形式的方法。
  • HumanMessagePromptTemplate 类:为在聊天提示中创建人类消息提供提示模板。HumanMessage 对象表示人类用户在与语言模型的对话中发送的消息。在本场景中,我们将用 user_query 字符串填充此提示,该字符串来自用户!
  • SystemMessagePromptTemplate 类:系统也会获得一个提示,针对基于聊天的 LLM,这个提示与人类生成的提示相比具有不同的意义。这个模板提供了我们在聊天提示中创建这些系统消息的方式。

接下来,我们希望创建一个函数来处理查询扩展,利用刚才讨论的不同提示模板。这是我们将使用的系统消息提示,你需要根据你的 RAG 系统所针对的领域进行自定义——在这个例子中是环境报告:

"You are a helpful expert environmental research assistant. Provide an example answer to the given question, that might be found in a document like an annual environmental report."

这是我们创建的函数的第一步:

def augment_query_generated(user_query):
    system_message_prompt = SystemMessagePromptTemplate.from_template(
        "You are a helpful expert environmental research assistant. Provide an example answer to the given question, that might be found in a document like an annual environmental report."
    )
    human_message_prompt = HumanMessagePromptTemplate.from_template("{query}")
    chat_prompt = ChatPromptTemplate.from_messages([
        system_message_prompt, human_message_prompt])
    response = chat_prompt.format_prompt(
        query=user_query).to_messages()
    result = llm(response)
    content = result.content
    return content

在这里,你可以看到我们利用了所有三种提示模板来构建我们发送给 LLM 的消息集合。最终,这将导致 LLM 给出一个回应,尽力回答我们的查询。

接下来,提供一些代码来调用这个函数,帮助我们理解输出,代表查询扩展的效果:

original_query = "What are Google's environmental initiatives?"
hypothetical_answer = augment_query_generated(
    original_query)
joint_query = f"{original_query} {hypothetical_answer}"
print(joint_query)

在这里,我们将用户查询 original_query 定义为源查询,表示它即将经过扩展。hypothetical_answer 是我们从 LLM 返回的响应字符串。然后,我们将原始用户查询和假设答案连接成一个新的查询 joint_query,并将其作为新查询使用。输出将类似于这样(为了简洁,省略了一些内容):

What are Google's environmental initiatives?
In 2022, Google continued to advance its environmental initiatives, focusing on sustainability and reducing its carbon footprint. Key initiatives included:
1. **Carbon Neutrality and Renewable Energy**: Google has maintained its carbon-neutral status since 2007 and aims to operate on 24/7 carbon-free energy by 2030. In 2022, Google procured over 7 gigawatts of renewable energy, making significant strides towards this goal.
2. **Data Center Efficiency**: Google's data centers are among the most energy-efficient in the world. In 2022, the company achieved an average power usage effectiveness (PUE) of 1.10, significantly lower than the industry average. This was accomplished through advanced cooling technologies and AI-driven energy management systems.
3. **Sustainable Products and Services**…[TRUNCATED]

这个回答要长得多,我们的 LLM 确实尽力做出了详细回答!这个初步答案将是你给原始用户查询的假设答案。通常我们会避免使用假设答案,但在这里,我们借助它们来利用 LLM 的内部工作机制,从中提取出与用户查询相关的概念。

此时,我们将通过原始代码,但不再像之前那样传递 original_query 字符串,而是将原始查询和假设答案的连接字符串传入我们的 RAG 流程:

result_alt = rag_chain_with_source.invoke(joint_query)
retrieved_docs_alt = result_alt['context']
print(f"Original Question: {joint_query}\n")
print(f"Relevance Score:
    {result_alt['answer']['relevance_score']}\n")
print(f"Final Answer:\n{
    result_alt['answer']['final_answer']}\n\n")
print("Retrieved Documents:")
for i, doc in enumerate(retrieved_docs_alt, start=1):
    print(f"Document {i}: Document ID:
        {doc.metadata['id']} source:
        {doc.metadata['search_source']}")
    print(f"Content:\n{doc.page_content}\n")

你会看到传入我们原始 RAG 流程的是更长的 joint_query 字符串,然后我们看到一个扩展的结果集,其中混合了我们提供的数据和 LLM 帮助添加的扩展结构。

由于 LLM 返回的文本是 Markdown 格式,我们可以使用 IPython 以更美观的方式打印出来。以下代码可以打印输出:

from IPython.display import Markdown, display
markdown_text_alt = result_alt['answer']['final_answer']
display(Markdown(markdown_text_alt))

以下是输出示例:

Google has implemented a comprehensive set of environmental initiatives aimed at sustainability and reducing its carbon footprint. Here are the key initiatives:
1. Carbon Neutrality and Renewable Energy: Google has been carbon-neutral since 2007 and aims to operate on 24/7 carbon-free energy by 2030. In 2022, Google procured over 7 gigawatts of renewable energy.
2. Data Center Efficiency: Google's data centers are among the most energy-efficient globally, achieving an average power usage effectiveness (PUE) of 1.10 in 2022. This was achieved through advanced cooling technologies and AI-driven energy management systems.
…[TRUNCATED FOR BREVITY]
3. Supplier Engagement: Google works with its suppliers to build an energy-efficient, low-carbon, circular supply chain, focusing on improving environmental performance and integrating sustainability principles.
4. Technological Innovations: Google is investing in breakthrough technologies, such as next-generation geothermal power and battery-based backup power systems, to optimize the carbon footprint of its operations.
These initiatives reflect Google's commitment to sustainability and its role in addressing global environmental challenges. The company continues to innovate and collaborate to create a more sustainable future.
---- END OF OUTPUT ----

比较原始查询的结果,看看你是否认为这个回答有所改善!如你所见,每次得到的回答都不同,你可以根据哪些方法最适合你的 RAG 应用来判断。

需要注意的是,使用这种方法时,你将 LLM 引入了检索阶段,而过去我们仅在生成阶段使用 LLM。然而,这也意味着现在在检索阶段,提示工程变得更加重要,而之前我们只关注生成阶段。这个方法类似于我们在第 13 章讨论的提示工程,当时我们谈到通过迭代来获得更好的 LLM 结果。

有关查询扩展的更多信息,可以阅读原始论文:arxiv.org/abs/2305.03…

查询扩展只是许多增强原始查询以帮助提升 RAG 输出的方法之一。在本章的最后,我们列出了更多方法,而在下一个代码实验中,我们将讨论一种叫做查询分解的方法,它在 RAG 场景中尤为有用,特别是因为它强调了问答的能力。

代码实验室 14.2 – 查询分解

该实验的代码可以在 GitHub 仓库的 CHAPTER14 目录中的 CHAPTER14-2_DECOMPOSITION.ipynb 文件中找到。

查询分解是一种旨在改进 GenAI 领域问答系统的策略。它属于查询翻译类别,这是一系列旨在改进 RAG 管道初始阶段(检索阶段)的方法。通过查询分解,我们将一个问题分解为多个较小的问题。这些较小的问题可以根据需要顺序处理或独立处理,从而为不同的场景提供更多灵活性,帮助解决在使用 RAG 时可能遇到的各种问题。在每个子问题得到解答后,会有一个整合步骤,最终给出一个比原始 RAG 解决方案更广泛的回答。

除了查询分解,还有其他一些查询翻译方法,如 RAG-Fusion 和多查询,它们侧重于处理子问题,但本节专注于问题的分解。我们将在本章末尾进一步讨论这些其他技术。

在提出这种方法的论文中,谷歌的研究人员将其称为 Least-to-Most 或分解方法。LangChain 在其官网上也有相关文档,称其为查询分解。因此,在讨论这一方法时,我们可以说自己并不孤单!

为了帮助我们理解如何实现查询分解,我们将介绍几个新概念:

  1. 思维链(Chain-of-Thought,CoT) :一种提示工程策略,通过结构化输入提示来模拟人类推理,目的是提高语言模型在需要逻辑、计算和决策的任务中的表现。
  2. 交替检索(Interleaving Retrieval) :在 CoT 驱动的提示和检索之间来回切换,以便在后续推理步骤中检索到更相关的信息,而不是仅仅将用户查询直接传递给检索。这个组合方法被称为交替检索与思维链(IR-CoT)。

将这些概念结合起来,我们得到了一种方法,它将问题分解为子问题,并通过动态检索过程逐步解决这些子问题。执行顺序如下:首先,将原始用户查询分解为子问题,然后从第一问题开始进行检索,回答该问题后,再进行第二问题的检索,将第一个问题的答案添加到结果中,接着使用这些数据回答第二个问题。这一过程会一直持续,直到所有子问题都得到了回答,最终得到一个综合的答案。

在这一切解释之后,您可能会想直接进入代码并查看它是如何工作的,因此让我们开始吧!

我们将导入一些新的包:

from langchain.load import dumps, loads

dumpsloads 函数是从 langchain.load 导入的,用于分别将 Python 对象序列化为字符串表示并从字符串反序列化。在我们的代码中,我们将使用它们将每个文档对象转换为字符串表示,然后再进行去重操作。

接着,我们跳过检索器定义,添加一个新的代码块来设置我们的查询分解提示、链和代码。首先,创建一个新的提示模板:

prompt_decompose = PromptTemplate.from_template(
    """You are an AI language model assistant.
    Your task is to generate five different versions of
    the given user query to retrieve relevant documents from
    a vector search. By generating multiple perspectives on
    the user question, your goal is to help the user
    overcome some of the limitations of the distance-based
    similarity search.  Provide these alternative questions
    separated by newlines.
    Original question: {question}"""
)

通过阅读这个 PromptTemplate 对象中的字符串,我们可以看到这是一个非常直白的请求,旨在解释我们想要解决的问题,并让 LLM 执行查询分解。我们还要求 LLM 以特定的格式返回结果。虽然这种方法可能会有风险,因为 LLM 有时会返回非预期的结果,即使明确要求特定格式,但在这个简单示例中,我们使用的 ChatGPT-4o-mini 模型似乎能够很好地以正确的格式返回结果。

接下来,我们设置链条,使用我们通常在链条中使用的各个元素,但在这里使用提示进行查询分解:

decompose_queries_chain = (
    prompt_decompose
    | llm
    | str_output_parser
    | (lambda x: x.split("\n"))
)

这是一个不言自明的链条,它使用了提示模板、之前定义的 LLM、输出解析器,并应用格式化处理,以便得到更易读的结果。

要调用这个链条,我们可以实现以下代码:

decomposed_queries = decompose_queries_chain.invoke(
    {"question": user_query})
print("Five different versions of the user query:")
print(f"Original: {user_query}")
for i, question in enumerate(decomposed_queries, start=1):
    print(f"{question.strip()}")

此代码会调用我们设置的链条,并为我们提供原始查询以及通过查询分解提示和 LLM 生成的五个新查询:

Five different versions of the user query:
Original: What are Google's environmental initiatives?
What steps is Google taking to address environmental concerns?
How is Google contributing to environmental sustainability?
Can you list the environmental programs and projects Google is involved in?
What actions has Google implemented to reduce its environmental impact?
What are the key environmental strategies and goals of Google?

LLM 在将我们的查询分解为多个相关问题时做得非常出色,这些问题涵盖了有助于回答原始查询的不同方面。

但是,这只是查询分解概念的一半!接下来,我们将通过检索所有这些问题,获取比以往更多、更有力的上下文信息。

我们将首先设置一个函数,用于格式化我们通过这些新查询检索到的文档:

def format_retrieved_docs(documents: list[list]):
    flattened_docs = [dumps(doc) for sublist in documents
        for doc in sublist]
    print(f"FLATTENED DOCS: {len(flattened_docs)}")
    deduped_docs = list(set(flattened_docs))
    print(f"DEDUPED DOCS: {len(deduped_docs)}")
    return [loads(doc) for doc in deduped_docs]

这个函数将返回一个包含多个文档的列表,其中每个子列表表示一组检索到的文档。我们首先将这些列表扁平化,然后使用 dumps 函数将每个文档对象转换为字符串进行去重,再将其还原为文档对象。我们还会打印出去重前后的文档数量,以便查看去重效果。在这个示例中,我们通过运行 decompose_queries_chain 链条,从 100 个文档减少到 67 个:

FLATTENED DOCS: 100
DEDUPED DOCS: 67

接下来,我们设置一个链条,运行之前的查询分解链条、对所有新查询进行检索,并使用我们刚刚创建的函数进行格式化:

retrieval_chain = (
    decompose_queries_chain
    | ensemble_retriever.map()
    | format_retrieved_docs
)

这行相对简短的代码完成了很多工作!最终结果是 67 个与我们从原始查询和分解生成的查询相关的文档。注意,我们已经将之前的 decompose_queries_chain 链条直接添加到了这个链条中,因此不需要单独调用它。

我们通过以下代码将该链条的结果分配给一个名为 docs 的变量:

docs = retrieval_chain.invoke({"question": user_query})

通过调用这个链条,我们检索到了比以前更多的文档(67 个),接下来我们需要通过我们的 RAG 步骤处理这些检索结果。大部分代码保持不变,但我们用之前创建的 retrieval_chain 链条替换了原来的集合链:

rag_chain_with_source = RunnableParallel(
    {"context": retrieval_chain,
     "question": RunnablePassthrough()}
).assign(answer=rag_chain_from_docs)

这将把我们的新代码整合到之前的 RAG 应用中。执行这一行代码后,所有新添加的链条都会一并运行,因此不需要像这个示例中那样单独运行它们。这个集合代码将我们之前的努力与这一新强大的 RAG 技术结合起来。我们邀请您将当前技术的结果与过去实验室的结果进行对比,以查看这项技术如何在更广泛的领域内提供更全面的覆盖:

Google has implemented a wide range of environmental initiatives aimed at improving sustainability and reducing its environmental impact. Here are some key initiatives based on the provided context.
1. Campus and Habitat Restoration:
Google has created and restored more than 40 acres of habitat on its campuses and surrounding urban landscapes, primarily in the Bay Area. This includes planting roughly 4,000 native trees and restoring ecosystems like oak woodlands, willow groves, and wetland habitats.
2. Carbon-Free Energy:
Google is working towards achieving net-zero emissions and 24/7 carbon-free energy (CFE) by 2030. This involves clean energy procurement strategies such as reducing carbon emissions across its operations and supply chain. Google has also entered into long-term renewable energy contracts to ensure 100% of its data centers are powered by renewable energy sources.
3. Greener Workplaces and Facilities:
Google is working to make its workplaces and facilities more sustainable by installing features such as energy-efficient buildings, reduced carbon footprints, and better waste management practices. For instance, Google’s Mountain View campus uses a blend of natural and renewable energy sources to reduce its environmental impact and improve energy efficiency.

这个例子展示了如何通过查询分解将一个简单的查询扩展为一系列更具信息量的检索查询,并且能够生成一个比传统 RAG 方法更为全面和有深度的答案。

通过使用查询分解、思维链和交替检索等技术,我们可以显著改进 RAG 过程中的检索效果,提供更相关、更多样化的上下文信息。通过结合多个小问题并逐一解决,查询分解不仅增加了我们得到答案的准确性,还提升了效率。

代码实验室 14.3 – MM-RAG

该实验的代码可以在 GitHub 仓库的 CHAPTER14 目录中的 CHAPTER14-3_MM_RAG.ipynb 文件中找到。

这是一个很好的例子,展示了缩写如何帮助我们更快地交流。试着大声说出“多模态检索增强生成”,你可能会立刻想从今以后直接使用 MM-RAG 这个缩写!不过,我有些跑题了。这是一种具有突破性的方式,未来可能会获得广泛的关注。它更好地代表了我们作为人类处理信息的方式,所以它一定很棒,对吧?让我们从回顾一下多模态的概念开始。

多模态

到目前为止,我们讨论的内容主要集中在文本上:将文本作为输入,基于该输入检索文本,并将检索到的文本传递给 LLM(大语言模型),然后生成最终的文本输出。但那如果是非文本数据呢?随着构建这些 LLM 的公司开始提供强大的多模态能力,我们该如何将这些多模态能力融入到我们的 RAG 应用中呢?

多模态意味着你正在处理多种“模式”,这些模式包括文本、图像、视频、音频以及任何其他类型的输入。这些多种模式可以体现在输入、输出或两者上。例如,你可以传入文本并获得图像,这是多模态;你也可以传入图像并获得文本(这叫做图像描述),这也是多模态。

更先进的方法还包括传入一个文本提示,比如“将这张图片转化为一个视频,视频进一步展示瀑布并加上瀑布的声音”,同时传入该瀑布的图像,最后返回一个视频,展示从图片中的瀑布出发,并加上瀑布的声音。这将代表四种不同的模式:文本、图像、视频和音频。考虑到现在已经有这些能力的模型,并且它们的 API 与我们在本书中使用的相似,考虑如何将这些能力应用到我们的 RAG 方法中,也就是利用 RAG 再次挖掘我们企业数据仓库中存储的其他类型内容,这是一个非常合乎逻辑的步骤。接下来,让我们讨论使用多模态方法的好处。

多模态的好处

这种方法利用了 RAG 技术在理解和利用多模态数据源方面的优势,从而能够创造出更具吸引力、更有信息量、并且具有更多上下文的输出。通过整合多模态数据,这些 RAG 系统可以提供更为细致和全面的答案,生成更丰富的内容,并与用户进行更复杂的互动。应用场景包括:增强型对话代理,能够理解并生成多媒体响应;以及高级内容创作工具,能够生成复杂的多模态文档和演示文稿。MM-RAG 代表了 RAG 系统的一个重要进展,使其更加多功能,并能够以一种与人类感官和认知体验相似的方式理解世界。

就像我们在第 7 章和第 8 章讨论的关于向量的内容一样,认识到向量嵌入在 MM-RAG 中的重要作用也非常关键。

多模态向量嵌入

MM-RAG 的实现得益于向量嵌入能够表示的不仅仅是文本;它们可以表示你传递给它的任何类型的数据。某些数据需要进行一定的准备工作才能将其转换为可向量化的形式,但所有类型的数据都有可能被向量化,并使其可供 RAG 应用使用。如果你还记得,向量化的本质是将数据转化为数学表示,而数学和向量是深度学习(DL)模型的主要语言,这些模型构成了我们所有 RAG 应用的基础。

你可能还记得关于向量的另一个概念,即向量空间,在这个空间中,类似的概念会比不相似的概念更加靠近。当你加入多个模式时,这个概念仍然适用,也就是说,像“海鸥”这样的概念,无论它是海鸥这个词、海鸥的图像、海鸥的视频,还是海鸥叫声的音频片段,都应该以相似的方式表现出来。这种跨模态表示相同上下文的多模态嵌入概念被称为模态独立性。这是向量空间概念的扩展,它为 MM-RAG 提供了基础,使其能够像单模态 RAG 一样发挥作用,但却可以处理多种数据模式。关键概念是,多模态向量嵌入在所有表示的模态中保持语义相似性。

在企业中使用 MM-RAG 时,重要的一点是要认识到很多企业数据都以多种模式存在,因此我们接下来要讨论的内容也至关重要。

图像不仅仅是“图片”

图像可以被看作是远不止漂亮的风景图或你上次度假拍摄的 500 张照片!在企业中,图像可以代表像图表、流程图、曾经转换为图像的文本等内容。图像是企业中重要的数据来源。

如果你还没有查看过我们在许多实验室中使用的 Google 2023 环境报告 PDF 文件,你可能会以为它只是基于文本的。但打开它,你会看到贯穿其中并伴随文本的精美图像。你看到的一些图表,尤其是那些设计精美的图表,实际上就是图像。如果我们有一个 RAG 应用,想要利用这些图像中的数据呢?让我们开始构建一个吧!

引入 MM-RAG 代码示例

在本实验中,我们将执行以下操作:

  1. 使用强大的开源包 unstructured 从 PDF 中提取文本和图像。
  2. 使用多模态 LLM 从提取的图像生成文本摘要。
  3. 将这些图像摘要(与我们已经使用的文本对象一起)嵌入并检索,并引用原始图像。
  4. 使用 Chroma 将图像摘要存储在多向量检索器中,同时存储原始文本和图像及其摘要。
  5. 将原始图像和文本块传递给相同的多模态 LLM 进行答案合成。

首先,我们需要安装一些新的包,这些包是 optical character recognition(OCR)组件所需的:

%pip install "unstructured[pdf]"
%pip install pillow
%pip install pydantic
%pip install lxml
%pip install matplotlib
%pip install tiktoken
!sudo apt-get -y install poppler-utils
!sudo apt-get -y install tesseract-ocr

以下是这些包在我们代码中所执行的功能:

  • unstructured[pdf]unstructured 是一个用于从无结构数据(如 PDF、图像和 HTML 页面)中提取结构化信息的 Python 库。这个安装包仅支持 PDF。如果需要支持其他类型的文档,可以安装更多支持,或使用 all 来获得所有文档的支持。
  • pillowpillow 是 Python 图像库(PIL)的一个分支,提供支持打开、处理和保存多种图像文件格式。在我们的代码中,当使用 unstructured 处理图像时,unstructured 会依赖 pillow。
  • pydanticpydantic 是一个数据验证和设置管理库,使用 Python 类型注解。它通常用于定义数据模型并验证输入数据。
  • lxmllxml 是一个处理 XML 和 HTML 文档的库。我们在代码中使用 lxml 和 unstructured 库一起解析和提取结构化文档的信息。
  • matplotlibmatplotlib 是一个著名的绘图库,用于在 Python 中创建可视化图表。
  • tiktokentiktoken 是 OpenAI 模型使用的字节对编码(BPE)分词器,最初是作为一种文本压缩算法开发的,并被 OpenAI 用于 GPT 模型的预训练。
  • poppler-utilspoppler-utils 是一组用于操作 PDF 文件的命令行工具,在我们的代码中,poppler 被 unstructured 用于从 PDF 文件中提取元素。
  • tesseract-ocrtesseract-ocr 引擎是一个开源 OCR 引擎,能够识别和提取图像中的文本。这是 unstructured 支持 PDF 时所需的另一个库,用于从图像中提取文本。

这些包提供了 langchain 和 unstructured 库所需的各种功能,支持文档解析、图像处理、数据验证、分词和 OCR,帮助处理 PDF 文件并生成响应。

接下来,我们导入这些包和其他包,以便在代码中使用:

from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain_community.document_loaders import UnstructuredPDFLoader
from langchain_core.runnables import RunnableLambda
from langchain.storage import InMemoryStore
from langchain_core.messages import HumanMessage
import base64
import uuid
from IPython.display import HTML, display
from PIL import Image
import matplotlib.pyplot as plt

这些是 Python 包的导入列表,接下来逐个解释它们的作用:

  • MultiVectorRetriever from langchain.retrievers.multi_vectorMultiVectorRetriever 是一个结合多个向量存储的检索器,允许基于相似性搜索高效地检索文档。在我们的代码中,MultiVectorRetriever 被用来创建一个结合了 vectorstoredocstore 的检索器,用于根据用户查询检索相关文档。
  • UnstructuredPDFLoader from langchain_community.document_loadersUnstructuredPDFLoader 是一个文档加载器,用于使用 unstructured 库从 PDF 文件中提取元素,包括文本和图像。在我们的代码中,UnstructuredPDFLoader 用于加载和提取指定 PDF 文件(short_pdf_path)中的元素。
  • RunnableLambda from langchain_core.runnablesRunnableLambda 类是一个实用工具类,允许将函数包装为 LangChain 管道中的可执行组件。在我们的代码中,RunnableLambda 用于将 split_image_text_typesimg_prompt_func 函数包装为 RAG 链中的可执行组件。
  • InMemoryStore from langchain.storageInMemoryStore 类是一个简单的内存存储类,用于存储键值对。在我们的代码中,InMemoryStore 用作文档存储,存储与每个文档 ID 相关的实际文档内容。
  • HumanMessage from langchain_core.messagesHumanMessage 是一个表示用户向语言模型发送消息的提示类型。在这个代码实验中,HumanMessage 用于构建图像摘要和描述的提示消息。
  • base64:用于将图像编码为 base64 字符串进行存储和检索。
  • uuid:用于生成唯一标识符(UUID),在代码中用于为添加到 vectorstoredocstore 中的文档生成唯一的文档 ID。
  • HTML 和 display from IPython.displayHTML 用于创建对象的 HTML 表示,display 用于在 IPython notebook 中显示对象。在我们的代码中,HTMLdisplay 用于在 plt_img_base64 函数中显示 base64 编码的图像。
  • Image from PIL:PIL 提供了打开、处理和保存多种图像文件格式的功能。
  • matplotlib.pyplot as pltmatplotlib 是一个绘图库,提供了用于创建可视化图表的功能。尽管在代码中 plt 并未直接使用,但可能在其他库或函数中隐式使用。

这些导入的包和模块提供了与文档加载、检索、存储、消息处理、图像处理和可视化相关的各种功能,整个代码将利用这些功能处理和分析 PDF 文件,并生成响应。

在导入包之后,我们在代码中建立了几个变量,它们将被整个代码使用。以下是几个亮点:

  • GPT-4o-mini:我们将使用 GPT-4o-mini,其中最后一个字符 "o" 代表 omni,表示它是多模态的!
llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
  • PDF 短版:请注意,我们现在使用的是一个不同的文件:
short_pdf_path = "google-2023-environmental-report-short.pdf"

完整文件较大,使用整个文件会增加处理成本,而演示价值不大。因此,我们鼓励使用此文件,它仍然可以展示 MM-RAG 应用,但 LLM 推理成本显著降低。

  • OpenAI 嵌入:需要注意的是,使用 OpenAI 嵌入时存在一个关键点:
embedding_function = OpenAIEmbeddings()

这个嵌入模型不支持多模态嵌入,也就是说,它无法将海鸥的图像与文本“海鸥”嵌入视为非常相似,这是多模态嵌入模型应该做到的。为了克服这一不足,我们将嵌入图像的描述而不是图像本身。这仍然被认为是多模态方法,但请注意,未来的多模态嵌入可能会在嵌入层面帮助我们解决这个问题。

接下来,我们将使用 UnstructuredPDFLoader 文档加载器加载 PDF 文件:

pdfloader = UnstructuredPDFLoader(
    short_pdf_path,
    mode="elements",
    strategy="hi_res",
    extract_image_block_types=["Image", "Table"],
    extract_image_block_to_payload=True,
)
pdf_data = pdfloader.load()

在这里,我们使用 LangChain 和 unstructured 提取 PDF 中的元素。这需要一些时间,通常在 1-5 分钟之间,具体取决于开发环境的性能。所以,这是一个很好的休息时间,阅读有关该包的参数,了解它是如何工作的!

接下来,我们将讨论如何使用文档加载器的参数,并准备好继续完成代码实验。

我们即将减少图像数量以节省处理成本,因此在此之前我们先打印出图像的数量,以确保操作正确!我们还想看看数据中包含了多少种元素类型。以下是输出结果:

TOTAL DOCS USED BEFORE REDUCTION: texts: 78 images: 17
CATEGORIES REPRESENTED: {'ListItem', 'Title', 'Footer', 'Image', 'Table', 'NarrativeText', 'FigureCaption', 'Header', 'UncategorizedText'}

目前,我们有17张图像。为了本次演示,我们希望将其减少,因为我们接下来将使用LLM对每张图像进行总结,而三张图像的成本约为17张图像的六分之一!我们还发现数据中包含了许多其他元素,除了我们正在使用的' NarrativeText'。如果我们想构建一个更健壮的应用程序,可以将'Title'、'Footer'、'Header'等元素加入到发送给LLM的上下文中,并告诉LLM相应地强调这些元素。例如,我们可以告诉它更多地强调'Title'。unstructured库在将PDF数据提供给我们时做得非常好,使其更适合LLM的处理!

好了——如承诺所言,我们将减少图像数量,以节省处理成本:

if len(images) > 3:
    images = images[:3]
print(f"total documents after reduction: texts: {len(texts)} images: {len(images)}")

我们基本上只是截取前三张图像,并将其存入images列表。打印输出结果,我们看到图像数量已被减少为三张:

total documents after reduction: texts: 78 images: 3

接下来的几个代码块将专注于图像总结,首先是我们的函数,通过该函数将提示应用到图像并获取总结:

def apply_prompt(img_base64):
    # Prompt
    prompt = """You are an assistant tasked with summarizing images for retrieval. \
        These summaries will be embedded and used to retrieve the raw image. \
        Give a concise summary of the image that is well optimized for retrieval."""
    return [HumanMessage(content=[
            {"type": "text", "text": prompt},
            {"type": "image_url", "image_url": {"url":
                    f"data:image/jpeg;base64,{img_base64}"},},
    ])]

这个函数接收一个img_base64参数,它代表一个图像的base64编码字符串。函数首先定义一个prompt变量,其中包含指示助手总结图像以便检索的提示字符串。然后,函数返回一个包含单个HumanMessage对象的列表,该对象表示图像的总结。HumanMessage对象包含一个content参数,该参数是一个包含两个字典的列表:

  • 第一个字典表示一个文本消息,内容为提示语。
  • 第二个字典表示一个图像URL消息,其中image_url键包含一个字典,该字典的url键设为以适当的数据URI方案(data:image/jpeg;base64)前缀的base64编码图像。

记住,当我们使用UnstructuredPDFLoader文档加载器时,我们已经将extract_image_block_to_payload设置为True,因此我们已经在元数据中获得了base64格式的图像,因此我们只需要将其传递到这个函数中!如果你在其他应用中使用这个方法,并且处理的是常规的图像文件(如.jpg.png文件),你只需要将它们转换为base64格式,然后就可以使用这个函数了。

但是在这个应用中,由于我们提取的图像已经是base64格式的,因此LLM可以直接处理这些base64图像,而这个函数也会以此为参数,因此我们不需要处理实际的图像文件!你会觉得遗憾没有看到实际的图像吗?不要担心!我们稍后会创建一个辅助函数,使用我们之前讨论过的HTML函数,将图像从其base64表示形式转换为HTML版本,以便在我们的笔记本中显示!

首先,我们准备好文本和图像,并设置列表来收集总结信息:

text_summaries = [doc.page_content for doc in texts]
# 存储base64编码的图像和图像总结
img_base64_list = []
image_summaries = []
for img_doc in images:
    base64_image = img_doc.metadata["image_base64"]
    img_base64_list.append(base64_image)
    message = llm.invoke(apply_prompt(base64_image))
    image_summaries.append(message.content)

请注意,我们没有对文本进行总结;我们直接将文本作为总结。你也可以对文本进行总结,这可能会改善检索结果,因为这是一种常见的提高RAG检索效果的方法。然而,为了节省更多的LLM处理成本,我们这里只对图像进行总结。你的钱包会感谢我们的!

对于图像来说,这就是全部内容——你刚刚完成了多模态的应用,使用了文本和图像!虽然我们不能完全称之为MM-RAG,因为我们还没有以多模态的方式进行检索。但很快我们就会实现这一目标——继续前进吧!

我们的数据准备工作已经结束;现在我们可以回到添加与RAG相关的元素,比如向量存储和检索器!在这里,我们设置向量存储:

vectorstore = Chroma(
    collection_name="mm_rag_google_environmental",
    embedding_function=embedding_function
)

我们设置了一个新的集合名称mm_rag_google_environmental,这表明这个向量存储的内容具有多模态的特点。我们添加了embedding_function链,用于嵌入我们的内容,这和我们在之前的代码实验中所见过的类似。然而,在这个例子中,我们不会立即将文档添加到向量存储中,而是等到设置好检索器之后再添加!如何将它们添加到检索器中呢?如我们之前所说,LangChain中的检索器只是向量存储的包装器,因此向量存储仍然存在,我们可以通过检索器将文档添加进去。

但首先,我们需要设置多向量检索器:

store = InMemoryStore()
id_key = "doc_id"
retriever_multi_vector = MultiVectorRetriever(
    vectorstore=vectorstore,
    docstore=store,
    id_key=id_key,
)

在这里,我们为vectorstore应用了MultiVectorRetriever包装器。但这个InMemoryStore是什么呢?InMemoryStore是一个简单的内存存储类,用于存储键值对。在这个代码中,它作为docstore对象使用,用于存储与每个文档ID相关的实际文档内容。我们通过定义id_keydoc_id来提供这些信息。

在此,我们将所有内容传递给 MultiVectorRetriever(...),这是一个结合多个向量存储并允许基于相似性搜索高效检索多种数据类型的检索器。我们之前多次看到 vectorstore 向量存储,但如您所见,您可以使用 docstore 对象来存储和检索文档内容。它被设置为 store 变量(InMemoryStore 实例),并将 id_key 字符串作为 id_key 参数传递给检索器。这使得通过使用 id_key 字符串,就像在关系型数据库中通过外键操作一样,能够轻松地从两个存储中检索与向量存储中的向量相关的额外内容。

不过,目前我们还没有在任何存储中添加数据!让我们构建一个函数,允许我们添加数据:

def add_documents(retriever, doc_summaries, doc_contents):
    doc_ids = [str(uuid.uuid4()) for _ in doc_contents]
    summary_docs = [
        Document(page_content=s, metadata={id_key: doc_ids[i]})
        for i, s in enumerate(doc_summaries)
    ]
    content_docs = [        Document(page_content=doc.page_content,        metadata={id_key: doc_ids[i]})
        for i, doc in enumerate(doc_contents)
    ]
    retriever.vectorstore.add_documents(summary_docs)
    retriever.docstore.mset(list(zip(doc_ids, doc_contents)))

这个函数是一个辅助函数,用于将文档添加到 retriever 对象的 vectorstore 向量存储和 docstore 对象中。它接受 retriever 对象、doc_summaries 列表和 doc_contents 列表作为参数。如我们之前所讨论的,每个类别(文本和图像)都有总结和内容。此函数使用 str(uuid.uuid4()) 为每个文档生成唯一的文档 ID,并通过遍历 doc_summaries 列表来创建 Document 对象,将总结作为页面内容,并将对应的文档 ID 作为元数据。它还通过遍历 doc_contents 列表创建 Document 对象,将文档内容作为页面内容,并将相应的文档 ID 作为元数据。然后,它使用 retriever.vectorstore.add_documents 函数将 summary_docs 列表添加到检索器的 vectorstore 向量存储中,并使用 retriever.docstore.mset 函数将 content_docs 列表添加到 docstore 对象中,将每个文档 ID 与其相应的文档内容关联。

接下来我们需要应用 add_documents 函数:

if text_summaries:
    add_documents(retriever_multi_vector, text_summaries, texts)
if image_summaries:
    add_documents(retriever_multi_vector, image_summaries, images)

这将根据需要为我们的 MM-RAG 流水线添加适当的文档和总结,添加表示文本和图像总结的嵌入向量。

接下来,我们将添加最终一轮的辅助函数,这些函数将用于我们最终的 MM-RAG 链。首先是一个将 base64 编码的图像和文本分离的函数:

def split_image_text_types(docs):
    b64_images = []
    texts = []
    for doc in docs:
        if isinstance(doc, Document):
            if doc.metadata.get("category") == "Image":
                base64_image = doc.metadata["image_base64"]
                b64_images.append(base64_image)
            else:
                texts.append(doc.page_content)
        else:
            if isinstance(doc, str):
                texts.append(doc)
    return {"images": b64_images, "texts": texts}

此函数接受包含图像相关文档的列表 docs 作为输入,并将它们分成 base64 编码的图像和文本。它初始化了两个空列表:b64_imagestexts。它遍历 docs 列表中的每个 doc,检查它是否是 Document 类的实例。如果 doc 是一个 Document 对象并且其元数据中有一个类别键,值为 "Image",则从 doc.metadata["image_base64"] 中提取 base64 编码的图像并将其添加到 b64_images 列表中。如果 docDocument 对象但没有 "Image" 类别,则将 doc.page_content 添加到 texts 列表中。如果 doc 不是 Document 对象而是字符串,则将该字符串添加到 texts 列表中。最后,函数返回一个字典,包含两个键:"images"(包含 base64 编码的图像的列表)和 "texts"(包含文本的列表)。

我们还创建了一个生成图像提示消息的函数:

def img_prompt_func(data_dict):
    formatted_texts = "\n".join(data_dict["context"]["texts"])
    messages = []
    if data_dict["context"]["images"]:
        for image in data_dict["context"]["images"]:
            image_message = {"type": "image_url",
                             "image_url": {"url": f"data:image/jpeg;base64,{image}"}}
            messages.append(image_message)
    text_message = {
        "type": "text",
        "text": (
            f"""You are a helpful assistant tasked with describing what is in an image. The user will ask for a picture of something. Provide text that supports what was asked for. Use this information to provide an in-depth description of the aesthetics of the image. Be clear and concise and don't offer any additional commentary.
User-provided question: {data_dict['question']}
Text and / or images: {formatted_texts}"""
        ),
    }
    messages.append(text_message)
    return [HumanMessage(content=messages)]

这个函数接受 data_dict 作为输入,并生成图像分析的提示消息。它从 data_dict["context"] 中提取文本,并使用 "\n".join 将其连接为一个字符串 formatted_texts。然后,它初始化一个空的 messages 列表。如果 data_dict["context"]["images"] 中有图像,它会遍历这些图像,并为每个图像创建一个包含 type: "image_url"image_url 字段的消息。接着,它创建一个文本消息,其中包含关于如何描述图像的指导,包含用户提供的问题以及格式化的文本。最后,它将所有消息添加到 messages 列表,并返回一个 HumanMessage 对象,包含这些消息的内容。

这些函数和步骤将帮助我们在 MM-RAG 链中处理文本和图像,以便通过相似性搜索高效地检索多模态数据。

如果 data_dict["context"]["images"] 存在,它将遍历列表中的每一张图片。对于每张图片,它会创建一个 image_message 字典,其中 "type" 键的值为 "image_url""image_url" 键包含一个字典,字典中的 "url" 存储了 base64 编码的图片链接。每个 image_message 实例将被追加到 messages 列表中。

接下来是最后的调整——在运行我们的 MM-RAG 应用之前,我们建立一个 MM-RAG 链,包括我们刚才设置的两个函数:

chain_multimodal_rag = ({"context": retriever_multi_vector
    | RunnableLambda(split_image_text_types),
    "question": RunnablePassthrough()}
    | RunnableLambda(img_prompt_func)
    | llm
    | str_output_parser
)

这创建了我们的 MM-RAG 链,包含以下几个组件:

  • {"context": retriever_multi_vector | RunnableLambda(split_image_text_types), "question": RunnablePassthrough()}:这类似于我们之前看到的其他检索器组件,提供一个包含两个键的字典:"context""question""context" 键被赋值为 retriever_multi_vector | RunnableLambda(split_image_text_types) 的结果。retriever_multi_vector 函数基于问题检索相关的文档,然后这些结果会通过 RunnableLambda(split_image_text_types) 传递,该函数是 split_image_text_types 函数的包装器。正如我们之前讨论的,split_image_text_types 函数会将检索到的文档分割成 base64 编码的图片和文本。"question" 键被赋值为 RunnablePassthrough,它仅将问题传递出去,不做任何修改。
  • RunnableLambda(img_prompt_func):前一个组件(分割后的图片和文本,以及问题)的输出将通过 RunnableLambda(img_prompt_func) 传递。正如我们之前讨论的,img_prompt_func 函数根据检索到的上下文和问题生成一个用于图像分析的提示消息,因此它会格式化出我们将在下一步传递给 LLM 的提示。
  • llm:生成的提示消息(包括 base64 格式的图片)将传递给我们的 LLM 进行处理。LLM 根据多模态提示消息生成响应,然后将其传递给下一个步骤:输出解析器。
  • str_output_parser:我们在代码实验中已经看到过输出解析器,这是与我们以往使用的一致的可靠 StrOutputParser 类,它将生成的响应解析为字符串。

总体而言,这个链条表示一个 MM-RAG 流水线,先检索相关文档,再将文档分割为图片和文本,生成提示消息,用 LLM 处理,然后将输出解析为字符串。

我们调用这个链条并实现完整的多模态检索:

user_query = "Picture of multiple wind turbines in the ocean."
chain_multimodal_rag.invoke(user_query)

注意,我们使用了不同于以前的 user_query 字符串。我们将其更改为与我们可用的图像相关的内容。

这是基于该用户查询的 MM-RAG 流水线输出:

'The image shows a vast array of wind turbines situated in the ocean, extending towards the horizon. The turbines are evenly spaced and stand tall above the water, with their large blades capturing the wind to generate clean energy. The ocean is calm and blue, providing a serene backdrop to the white turbines. The sky above is clear with a few scattered clouds, adding to the tranquil and expansive feel of the scene. The overall aesthetic is one of modernity and sustainability, highlighting the use of renewable energy sources in a natural setting.'

这个响应与用户查询字符串以及我们用来向 LLM 解释如何描述它所“看到”图像的提示相符合。由于我们只有三张图片,因此很容易找出图像 #2 是在描述的对象,可以通过以下方式获取:

def plt_img_base64(img_base64):
    image_html = f'<img src="data:image/jpeg;base64,{img_base64}" />'
    display(HTML(image_html))
plt_img_base64(img_base64_list[1])

这里的函数是我们承诺帮助你查看图像的辅助函数。它接受一个 base64 编码的图像 img_base64 作为输入,并通过 HTML 显示它。通过创建一个包含 <img> 标签的 image_html 字符串,并将 src 属性设置为 base64 编码的图片链接,它使用 IPython 的 display() 函数渲染 HTML 字符串并显示图像。你可以在你的代码实验中运行这个代码,查看从 PDF 中提取出来的图片,这将为 MM-RAG 响应提供基础!

作为参考,这是为这张图片生成的图像摘要,使用与 img_base64_list 列表中对应的索引生成的,因为它们是匹配的:

image_summaries[1]

该摘要应该看起来像这样:

'Offshore wind farm with multiple wind turbines in the ocean, text "What's inside" on the left side.'

根据 MM-RAG 链条的输出描述,我们可以看到,LLM 实际上能够“看到”这张图像并告诉你关于它的内容。你已经正式进入了多模态领域!

我们选择本章中的三个代码实验,是因为我们认为它们代表了大多数 RAG 应用中的潜在改进方向的广泛体现。但这些只是冰山一角,未来你可能会在自己的 RAG 流水线中应用更多技术。在下一部分,我们将提供一些建议,作为你在 RAG 流水线中可以考虑的技术的起点。

其他值得探索的高级 RAG 技术

正如我们讨论的其他与 RAG 和 GenAI 相关的内容一样,可用于应用于 RAG 应用程序的高级技术选项实在太多,无法列举或追踪。我们选择了专注于 RAG 特定方面的技术,并根据它们对 RAG 应用的不同影响领域进行分类。

我们将按照 RAG 流水线的操作顺序逐一介绍这些技术,从索引开始。

索引改进

以下是专注于 RAG 流水线索引阶段的高级技术:

  • 深度分块(Deep Chunking) :检索结果的质量通常依赖于在数据存储到检索系统之前,数据的分块方式。通过深度分块,您可以使用深度学习模型,包括变换器(transformers),来进行最佳且智能的分块。
  • 训练和利用嵌入适配器(Embedding Adapters) :嵌入适配器是轻量级模块,经过训练后可将现有的语言模型嵌入调整为特定任务或领域的需求,无需进行大量重新训练。应用到 RAG 系统时,这些适配器可以根据提示的细微差别,定制模型的理解和生成能力,从而促进更准确和相关的检索。
  • 多表示索引(Multi-representation Indexing) :命题索引使用 LLM 生成文档摘要(命题),这些摘要经过优化,适合用于检索。
  • 递归抽象处理树组织检索(RAPTOR) :RAG 系统需要处理“低层次”问题,这些问题涉及从单一文档中提取的具体事实,或者“高层次”问题,这些问题提炼出跨多个文档的观点。典型的 kNN 检索方式只能检索有限数量的文档块,因此很难同时应对这两类问题。RAPTOR 通过创建捕捉更高层次概念的文档摘要来解决这一问题。它将文档进行嵌入并聚类,然后对每个集群进行摘要处理。这个过程是递归进行的,生成一个摘要树,每次递归都产生更高层次的概念。最终,这些摘要和起始文档一起被索引,确保覆盖用户的问题。
  • 基于 BERT 的上下文延迟交互(ColBERT) :嵌入模型将文本压缩成固定长度(向量)表示,捕捉文档的语义内容。虽然这种压缩在高效的检索中非常有用,但它对单一向量表示承担了很大压力,要求它能够捕捉所有语义的细微差别。在某些情况下,无关或冗余的内容可能会稀释嵌入的语义价值。ColBERT 提供了一种方法,通过更细粒度的嵌入,专注于在文档和查询之间进行更细致的按令牌(token)相似性评估,从而解决这个问题。

检索

检索是我们最大的 RAG 高级技术类别,这也反映了检索在 RAG 流程中的重要性。以下是我们建议您考虑用于 RAG 应用的一些方法:

  • 假设性文档嵌入(HyDE) :HyDE 是一种检索方法,它通过生成一个假设性的文档来增强检索能力,用于处理传入的查询。假设文档来自于 LLM 的知识,并经过嵌入,用于从索引中检索文档。其理念是,假设性文档可能比原始用户问题与索引中的文档对齐得更好。
  • 句子窗口检索(Sentence-window Retrieval) :在句子窗口检索中,检索是基于更小的句子进行的,这样可以更好地匹配相关的上下文,并根据句子周围扩展的上下文窗口进行综合。
  • 自动合并检索(Auto-merging Retrieval) :自动合并检索解决了典型 RAG 中使用较小块可能导致数据碎片化的问题。它使用一种自动合并的启发式方法,将较小的块合并成一个更大的父块,从而帮助确保上下文更加连贯。
  • 多查询重写(Multi-query Rewriting) :多查询重写是一种从多个角度重写问题的方法,先对每个重写的问题执行检索,再取所有文档的独特集合。
  • 查询翻译倒退(Query Translation Step-back) :倒退提示是一种提升检索的技术,它基于 CoT 推理构建。通过一个问题,它生成一个“倒退”(更高层次、更抽象的)问题,作为正确回答原始问题的前提条件。这在背景知识或更基本的理解对回答特定问题有帮助的情况下尤其有效。
  • 查询结构化(Query Structuring) :查询结构化是将文本转换为领域特定语言(DSL)的过程,其中 DSL 是与给定数据库交互所需的专用语言。它将用户问题转化为结构化查询。

检索后/生成阶段

以下是专注于 RAG 流水线生成阶段的高级技术:

  • 交叉编码器重排序(Cross-encoder Re-ranking) :我们已经在混合 RAG 代码实验中看到了重排序所带来的改进,它是在检索结果发送到 LLM 之前应用的。交叉编码器重排序通过使用更为计算密集的模型,进一步发挥这一技术的优势,根据检索结果与原始提示的相关性重新评估和排序已检索的文档。这种细粒度的分析确保了最相关的信息被优先用于生成阶段,从而提高了整体输出的质量。
  • RAG 融合查询重写(RAG-fusion Query Rewriting) :RAG-fusion 是一种从多个角度重写问题的方法,先对每个重写的问题执行检索,然后对每次检索的结果进行互排名融合,最终给出一个综合排序。

整个 RAG 流水线覆盖

以下高级 RAG 技术专注于 RAG 流水线的整体过程,而非某一特定阶段:

  • 自反 RAG(Self-reflective RAG) :带有 LangGraph 的自反 RAG 技术通过结合自反机制和来自 LangGraph 的语言图结构,改善了简单 RAG 模型。在这种方法中,LangGraph 帮助深入理解上下文和语义,使 RAG 系统能够基于对内容及其相互关系的更精细理解来优化响应。这对于内容创作、问答和对话代理等应用尤为有用,因为它能够生成更准确、相关且具有上下文感知的输出,显著提升生成文本的质量。
  • 模块化 RAG(Modular RAG) :模块化 RAG 使用可互换组件提供更灵活的架构,可以根据您的 RAG 开发需求进行调整。这种模块化使研究人员和开发者能够实验不同的检索机制、生成模型和优化策略,从而定制 RAG 系统以满足特定的需求和应用。如同本书中的代码实验所示,LangChain 提供了支持这种方法的机制,其中 LLM、检索器、向量存储和其他组件可以在许多情况下轻松互换。模块化 RAG 的目标是朝着一个更加可定制、高效和强大的 RAG 系统迈进,能够更有效地处理更广泛的任务。

随着新研究的不断发布,这个技术列表正在快速增长。一个获取新技术的好资源是 Arxiv.org 网站:arxiv.org/
访问该网站并搜索与您的 RAG 应用相关的各种关键术语,包括 RAG、检索增强生成、向量搜索等相关术语。

总结

在本章的最后,我们探讨了几种提高 RAG 应用的高级技术,包括查询扩展、查询分解和 MM-RAG。这些技术通过扩展查询、将问题拆解为子问题以及融合多种数据模态,增强了检索和生成的能力。我们还讨论了涵盖索引、检索、生成以及整个 RAG 流水线的其他一系列高级 RAG 技术。

很高兴与您一起踏上这段 RAG 之旅,探索 RAG 的世界及其巨大潜力。随着本书的结束,我希望您已经具备了足够的知识和实践经验,能够应对自己的 RAG 项目。祝您在未来的 RAG 探索中好运——我相信您会创造出令人惊叹的应用,推动这一激动人心的新技术的边界!