Published on

【译】高级RAG技术:图解概述

简介

如果您熟悉RAG概念,请跳到高级RAG部分。

检索增强生成,即RAG,为LLMs提供了从某些数据源检索到的信息,以便在生成的答案上进行基础。基本上,RAG是搜索 + LLM提示,您可以要求模型根据搜索算法找到的信息来回答查询。查询和检索到的上下文都被注入到发送给LLM的提示中。

RAG是2023年LLM基础系统中最流行的架构。有许多产品几乎完全基于RAG构建 — 从将网络搜索引擎与LLMs结合的问答服务到数百个与您的数据聊天的应用程序。

甚至向量搜索领域也受到了这种热情的推动,尽管基于嵌入的搜索引擎早在2019年就已经使用faiss构建。向量数据库初创公司,如chromaweaviate.iopinecone,都是基于现有的开源搜索索引 — 主要是faiss和nmslib — 并最近添加了额外的存储输入文本以及其他工具。

LLM(Large Language Models)基于管道和应用程序的两个最显著的开源库是 LangChainLlamaIndex,分别于2022年10月和11月相继成立,受 ChatGPT 发布的启发,并在2023年获得了广泛的采用。

本文的目的是系统化关键的高级RAG技术,并引用它们的实现——主要是在LlamaIndex中——以便促进其他开发人员对该技术的深入了解。

问题在于大多数教程都是挑选了一项或几项技术,并详细解释了如何实现它们,而不是描述所有可用工具的完整多样性。

另一件事是,LlamaIndex 和 LangChain 都是令人惊叹的开源项目,它们的发展速度如此之快,以至于它们的文档已经比2016年的机器学习教科书还要厚。

Naive RAG

本文中 RAG 管道的起点将是一组文本文档的语料库——我们跳过此之前的所有内容,将其留给连接到任何可想象的来源,从Youtube到Notion的令人惊叹的开源数据加载器。

作者提供的一个方案,以及本文后续的所有方案

Naive RAG 情况 简要地看起来是这样的:您将文本分成块,然后使用某个 Transformer 编码器模型将这些块嵌入向量,将所有这些向量放入索引,最后为 LLM 创建提示,告诉模型根据我们在搜索步骤中找到的上下文来回答用户的查询。
在运行时,我们使用相同的编码器模型对用户的查询进行向量化,然后执行该查询向量与索引的搜索,找到前 k 个结果,从我们的数据库中检索相应的文本块,并将其作为上下文提供给LLM。

提示可能如下所示:

def question_answering(context, query):
    prompt = f"""
                Give the answer to the user query delimited by triple backticks ```{query}```\
                using the information given in context delimited by triple backticks ```{context}```.\

                If there is no relevant information in the provided context, try to answer yourself,
                but tell user that you did not have any relevant context to base your answer on.
                Be concise and output the answer of size less than 80 tokens.
                """

    response = get_completion(instruction, prompt, model="gpt-3.5-turbo")
    answer = response.choices[0].message["content"]
    return answer

一个 RAG 提示的示例

提示工程 是您可以尝试以改进 RAG 流程的最便宜的方法。确保您已经查看了相当全面的 OpenAI 提示工程指南

显然,尽管 OpenAI 是 LLM 提供商中的市场领导者,但还有一些替代方案,例如来自 Anthropic 的 Claude,最近流行的较小但非常有能力的模型,例如来自 Mistral 的 Mixtral,来自 Microsoft 的 Phi-2,以及许多开源选项,例如 Llama2OpenLLaMAFalcon,因此您可以选择用于您的 RAG 流程的大脑。

高级 RAG

现在我们将深入了解高级 RAG 技术的概述。
这里是一个描述核心步骤和涉及的算法的方案。
为了保持方案的可读性,省略了一些逻辑循环和复杂的多步行为。

高级 RAG 结构的一些关键组成部分。这更像是可用工具的选择,而不是蓝图。

方案中的绿色元素是进一步讨论的核心 RAG 技术,蓝色元素是文本。并非所有高级 RAG 想法都能在单个方案上轻松可视化,例如,各种上下文扩展方法被省略了 — 我们将在途中深入讨论。

1. 分块和向量化

首先,我们希望创建一个表示文档内容的向量索引,然后在运行时搜索所有这些向量与最接近语义含义的查询向量之间的最小余弦距离。

1.1 分块

变换器模型具有固定的输入序列长度,即使输入上下文窗口很大,一个句子或几个句子的向量更好地表示它们的语义含义,而不是在几页文本上平均的向量(也取决于模型,但通常是真实的),因此 分块您的数据 — 将初始文档分成一些大小的块,而不会失去它们的含义(将您的文本分成句子或段落,而不是将单个句子分成两部分)。有各种文本分割器实现可以完成这个任务。

块的大小是一个需要考虑的参数 — 它取决于您使用的嵌入模型及其在标记中的容量,像基于BERT的句子转换器这样的标准Transformer编码器模型最多可以处理512个标记,OpenAI ada-002能够处理更长的序列,比如8191个标记,但这里的折衷是足够的上下文供LLM推理,而特定的文本嵌入足够有效地执行搜索这里您可以找到一项研究,说明了块大小选择的相关问题。在LlamaIndex中,这由NodeParser类来处理,具有一些高级选项,比如定义自己的文本分割器、元数据、节点/块关系等。

1.2 向量化

下一步是选择一个 模型来嵌入我们的块 — 有相当多的选择,我选择了 搜索优化模型,比如bge-largeE5嵌入系列 — 只需查看MTEB排行榜获取最新更新。

要了解块化和向量化步骤的端到端实现,请查看LlamaIndex中完整数据摄入管道的示例

2. 搜索索引

2.1 向量存储索引


在此方案中以及文本中的任何其他地方,我省略了编码器块,并直接将查询发送到索引以简化方案。当然,查询总是首先进行向量化处理。对于前k个块也是一样 — 索引检索前k个向量,而不是块,但我用块替换它们,因为获取它们是一个微不足道的步骤。

RAG管道的关键部分是搜索索引,存储我们在上一步中得到的向量化内容。最简单的实现使用平面索引 — 在查询向量和所有块向量之间进行蛮力距离计算。

一个适当的搜索索引,针对高效检索进行了优化,在10000+元素规模上是一个向量索引,比如faissnmslibannoy,使用了一些近似最近邻居实现,比如聚类、树或HNSW算法。

还有一些托管解决方案,比如OpenSearch或ElasticSearch和向量数据库,负责在幕后处理步骤1中描述的数据摄入管道,比如PineconeWeaviateChroma

根据您的索引选择、数据和搜索需求您也可以将元数据与向量一起存储,然后使用元数据过滤器来搜索例如某些日期或来源内的信息。

LlamaIndex支持许多向量存储索引,但也支持其他更简单的索引实现,比如列表索引、树索引和关键词表索引 — 我们将在Fusion检索部分讨论后者。

2.2 分层索引

如果您有许多文档需要检索,您需要能够在其中进行高效搜索,找到相关信息,并将其综合成一个带有来源引用的单一答案。在大型数据库的情况下,一个高效的方法是 创建两个索引 — 一个由摘要组成,另一个由文档块组成,然后进行两步搜索,首先通过摘要过滤出相关文档,然后仅在这个相关组内进行搜索。

2.3 假设问题和HyDE

另一种方法是要求LLM 为每个块生成一个问题并将这些问题嵌入向量,在运行时对这些问题向量进行查询搜索(在我们的索引中用问题向量替换块向量),然后在检索后路由到原始文本块并将它们作为LLM获取答案的上下文。
这种方法提高了搜索质量,因为 查询和假设问题之间的语义相似性更高,相比于我们对实际块所拥有的情况。

还有一种反向逻辑方法,称为HyDE__ __— 你要求一个LLM根据查询生成一个假设的响应,然后使用它的向量以及查询向量来增强搜索质量。

2.4 上下文丰富化

这里的概念是检索更小的片段以获得更好的搜索质量,但同时添加周围的上下文供LLM进行推理。
有两个选项 —— 通过围绕较小的检索到的片段的句子来扩展上下文,或者将文档递归地分割成多个较大的父级片段,其中包含较小的子片段。

2.4.1 句子窗口检索
在这种方案中,文档中的每个句子都被单独嵌入,这提供了查询到上下文余弦距离搜索的很高准确性。
为了更好地对找到的上下文进行推理,我们在获取最相关的单个句子之后,通过在检索到的句子之前和之后发送k个句子来扩展上下文窗口,然后将这个扩展的上下文发送给LLM。

绿色部分是在索引中搜索到的句子嵌入,整个黑色 + 绿色段落被送到LLM以在提供的查询上进行推理时扩大其上下文

2.4.2 自动合并检索器 (又名 父文档检索器)

这里的想法与句子窗口检索器非常相似 —— 搜索更精细的信息片段,然后在将上下文发送给LLM进行推理之前扩展上下文窗口。文档被分割成较小的子片段,指向较大的父片段。

文档被分割成一个层次结构的块,然后最小的叶子块被发送到索引。在检索时,我们检索 k 个叶子块,如果有 n 个块指向同一个父块,我们将用这个父块替换它们,并将其发送到 LLM 进行答案生成。

在检索时首先获取较小的块,然后如果前 k 个检索到的块中有超过 n 个块链接到相同的父节点(较大的块),我们将用这个父节点替换传递给 LLM 的上下文 —— 就像自动将几个检索到的块合并成一个较大的父块一样,因此得名。只需注意 —— 搜索仅在子节点索引中执行。在 递归检索器 + 节点引用 上查看 LlamaIndex 教程,以深入了解。

2.5 融合检索或混合搜索

一个相对古老的想法,你可以 从两个世界中取长处 —— 基于关键词的老式搜索 — 稀疏检索算法,比如 tf-idf 或搜索行业标准 BM25 —— 和现代的语义或向量搜索,并将它们结合在一个检索结果中。 唯一的诀窍在于正确地结合具有不同相似度分数的检索结果 —— 这个问题通常是通过 互惠排名融合 算法来解决的,重新排列最终输出的检索结果。

在 LangChain 中,这是在 Ensemble Retriever 类中实现的,它结合了您定义的一系列检索器,例如 faiss 向量索引和基于 BM25 的检索器,并使用 RRF 进行重新排名。

在 LlamaIndex 中,这是以非常相似的方式 完成的

混合或融合搜索通常会提供更好的检索结果,因为两种互补的搜索算法被结合起来,考虑了查询和存储文档之间的语义相似性和关键字匹配。

3. 重新排序和过滤

所以我们得到了任何上述描述的算法的检索结果,现在是通过过滤、重新排序或某些转换来完善它们的时候了。在 LlamaIndex 中有各种可用的后处理器,__根据相似度分数、关键字、元数据进行结果过滤,或者使用其他模型进行重新排序,比如 LLM、sentence-transformer cross-encoder,Cohere 重新排序 endpoint
或者基于日期最新性的元数据 — 基本上,你能想象到的所有情况。

这是在将我们检索到的内容馈送给LLM之前的最后一步,以便获得最终的答案。

现在是时候进入更复杂的RAG技术,比如查询转换和路由,两者都涉及LLM,因此代表着 主动行为 — 在我们的RAG管道中涉及LLM推理的一些复杂逻辑。

4. 查询转换

查询转换是一系列使用LLM作为推理引擎的技术,用于修改用户输入以提高检索质量。 有不同的选项可以做到这一点。

查询转换原理示例

如果查询很复杂,LLM可以将其分解为几个子查询。 例如,如果你问:
*— “Github上哪个框架星标更多,Langchain还是LlamaIndex?”,*那么在我们的语料库中很可能找不到直接的比较,因此将这个问题分解为两个子查询是有意义的,假设更简单和更具体的信息检索:
*— “Langchain在Github上有多少星标?”
— “LlamaIndex在Github上有多少星标?”*它们将并行执行,然后检索到的内容将被合并成一个单一提示,供LLM综合出对初始查询的最终答案。这两个库都已经实现了这个功能 — Langchain 中作为多查询检索器,LlamaIndex 中作为子问题查询引擎

  1. Step-back prompting** 使用LLM生成更一般的查询**,以检索我们获得更一般或高层次的上下文,有助于在原始查询上找到答案。
    还会执行原始查询的检索,并将两个上下文都提供给LLM进行最终的答案生成步骤。
    这是LangChain的实现

  2. 查询重写使用LLM重新构造初始查询 以改善检索。LangChainLlamaIndex 都有实现,虽然略有不同,但我发现LlamaIndex的解决方案在这里更加强大。

参考引用

这个没有编号,因为这更像是一种工具而不是一种检索改进技术,尽管它非常重要。
如果我们使用多个来源生成答案,要么是由于初始查询的复杂性(我们不得不执行多个子查询,然后将检索到的上下文组合成一个答案),要么是因为我们在各种文档中找到了单个查询的相关上下文,那么问题就是我们是否能够准确地引用我们的来源

有几种方法可以做到这一点:

  1. 将这个引用任务插入到我们的提示中,并要求LLM提到所使用的来源的ID。
  2. 将生成的响应部分与我们的索引中的原始文本块进行匹配 — llamaindex为这种情况提供了一种高效的基于模糊匹配的解决方案。如果您还没有听说过模糊匹配,这是一种非常强大的字符串匹配技术

5. Chat Engine

关于构建一个能够多次为单个查询工作的良好RAG系统的下一个重要方面是 聊天逻辑,考虑到对话上下文 ,就像在LLM时代之前的经典聊天机器人一样。 这是为了支持后续问题、指代或与先前对话上下文相关的任意用户命令。它通过 查询压缩技术解决,考虑到聊天上下文 以及用户查询。

与以往一样,有几种方法可以进行上下文压缩 —
一种流行且相对简单的方法是ContextChatEngine,首先检索与用户查询相关的上下文,然后将其与来自 memory 缓冲区的聊天历史一起发送到LLM,以便LLM在生成下一个答案时了解先前的上下文。

更复杂的情况是CondensePlusContextMode — 在每次交互中,聊天历史和最后一条消息被压缩成一个新的查询,然后将此查询发送到索引,检索到的上下文与原始用户消息一起传递给LLM以生成答案。

值得注意的是,LlamaIndex还支持基于OpenAI代理的聊天引擎,提供了更灵活的聊天模式,而Langchain也支持OpenAI功能API。

不同聊天引擎类型和原理的示意图

还有其他聊天引擎类型,如ReAct Agent,但让我们跳到第7节中的代理本身。

6. 查询路由

查询路由是LLM决策的一步,根据用户查询决定下一步要做什么 — 通常的选项包括总结、对某些数据索引进行搜索,或尝试多种不同的路线,然后将它们的输出合成为一个答案。

查询路由器也用于选择索引,或者更广泛地说,数据存储,用于发送用户查询的位置 — 无论是有多个数据源,例如经典的向量存储和图数据库或关系型数据库,还是有索引的层次结构 — 对于多文档存储,一个非常经典的情况是摘要索引和另一个文档块向量索引的情况。

定义查询路由器包括设置它可以做出的选择。
通过LLM调用执行路由选项的选择,以预定义格式返回其结果,用于将查询路由到给定的索引,或者,如果我们正在谈论磁性行为,则路由到子链或甚至其他代理,如下所示的 多文档代理方案

LlamaIndexLangChain都支持查询路由器。

7. RAG中的代理

代理(由LangchainLlamaIndex支持)自从第一个LLM API发布以来就存在 — 想法是为具有推理能力的LLM提供一组工具和要完成的任务。 这些工具可能包括一些确定性函数,如任何代码函数或外部API,甚至其他代理 — 这种LLM链接的想法正是LangChain得名的来源。

代理本身是一个很大的东西,在RAG概述中不可能对该主题进行足够深入的探讨,因此我将继续讨论基于代理的多文档检索案例,并在 OpenAI助手站 稍作停留,因为它是一个相对较新的东西,在最近的OpenAI开发者大会上作为GPTs进行了介绍,并在下面描述的RAG系统的内部运行。

OpenAI助手基本上实现了我们之前在开源中拥有的许多围绕LLM所需的工具 — 聊天记录、知识存储、文档上传界面,可能最重要的是函数调用API。后者提供了将自然语言转换为对外部工具或数据库查询的API调用的能力。

在 LlamaIndex 中,有一个 OpenAIAgent 类,它将这种先进的逻辑与 ChatEngine 和 QueryEngine 类结合起来,提供基于知识和上下文的聊天,以及在一次对话中进行多个 OpenAI 函数调用的能力,这真的带来了智能的代理行为。

让我们来看看 多文档代理 方案 **— 一个非常复杂的设置,涉及初始化一个 代理 (OpenAIAgent) 在每个文档上,能够进行文档摘要和经典的问答机制,**以及一个顶级代理__,负责将查询路由到文档代理,并进行最终答案的综合。

每个文档代理都有两个工具 —— 一个向量存储索引和一个摘要索引,并根据路由的查询决定使用哪一个。
对于顶级代理来说,所有文档代理都是相应的工具。

这个方案展示了一个带有许多由每个涉及代理做出的路由决策的高级 RAG 架构。这种方法的好处在于能够比较描述在不同文档及其摘要中的不同解决方案或实体,以及经典的单文档摘要和问答机制 —— 这基本上涵盖了最常见的与文档集合聊天的用例。

一个展示了多文档代理的方案,涉及查询路由和代理行为模式。

这种复杂方案的缺点可以从图片中猜出 —— 由于我们的代理内部与 LLM 进行多次来回迭代,所以速度有点慢。以防万一,LLM 调用始终是 RAG 管道中最耗时的操作 —— 搜索是根据设计进行了速度优化的。因此,对于大型多文档存储,我建议考虑对这个方案进行一些简化,使其可扩展。

8. 响应合成器

这是任何 RAG 管道的最后一步 —— 根据我们仔细检索的所有上下文和初始用户查询生成答案。
最简单的方法就是一次性连接并将所有获取的上下文(超过某个相关性阈值)与查询一起输入到 LLM 中。
但是,像往常一样,还有其他更复杂的选项,涉及多次调用 LLM 来优化检索到的上下文并生成更好的答案。

响应合成的主要方法包括:

  1. 通过逐块发送检索到的上下文来迭代地优化答案
  2. 总结检索到的上下文 以适应提示
  3. 基于不同的上下文块生成多个答案,然后连接或 总结它们
    更多详细信息,请查看响应合成器模块文档

编码器和 LLM 微调

这种方法涉及微调我们的 RAG 管道中涉及的两个 DL 模型中的一些部分 —— 要么是 Transformer 编码器,负责嵌入质量和因此上下文检索质量,要么是 LLM,负责最佳地利用提供的上下文来回答用户查询 — 幸运的是,后者是一个很好的 few-shot 学习者。

如今的一个巨大优势是高端 LLM(如 GPT-4)的可用性,可以生成高质量的合成数据集。
但您应该始终意识到,使用专业研究团队在精心收集、清理和验证的大型数据集上训练的开源模型,并使用小型合成数据集进行快速调整,可能会普遍限制模型的能力。

编码器微调

我对编码器微调方法也持有一些怀疑,因为最新的为搜索优化的 Transformer 编码器非常高效。
因此,我在 LlamaIndex notebook 设置中测试了通过对 bge-large-en-v1.5(在撰写本文时排名 MTEB 排行榜 前 4)进行微调所提供的性能提升,结果显示检索质量提高了 2%。虽然没有什么惊人的变化,但了解这个选项还是很不错的,特别是如果您正在为一个狭窄领域的数据集构建 RAG。

排名器微调

如果您不完全信任基本编码器,另一个不错的选择是为重新排列您检索到的结果使用交叉编码器。
它的工作方式如下:您将查询和前 k 个检索到的文本块分别传递给交叉编码器,用 SEP 标记分隔,并微调它,使其对相关块输出 1,对不相关块输出 0。
一个很好的示例可以在这里找到,结果显示,通过交叉编码器微调,成对分数提高了 4%。

LLM 微调

最近,OpenAI 开始提供 LLM 微调API,而 LlamaIndex 在教程中介绍了在 RAG 设置中对 GPT-3.5-turbo 进行微调以“提炼”一些 GPT-4 知识的过程。这里的想法是拿一份文档,用 GPT-3.5-turbo 生成若干问题,然后使用 GPT-4 基于文档内容生成这些问题的答案(构建一个由 GPT4 驱动的 RAG 管道),然后对问题-答案对的数据集对 GPT-3.5-turbo 进行微调。用于 RAG 管道评估的 ragas 框架显示,微调后的 GPT 3.5-turbo 模型在忠实度指标上提高了 5%,这意味着微调后的 GPT 3.5-turbo 模型比原始模型更好地利用了提供的上下文生成答案。

最近的一篇论文《RA-DIT: 检索增强双指令微调》由 Meta AI Research 展示了更复杂的方法,建议一种调整 LLM 和检索器(原始论文中的双编码器)的技术,用于查询、上下文和答案的三元组。有关实现细节,请参阅指南
这种技术既用于通过微调 API 微调 OpenAI LLM,也用于微调 Llama2 开源模型(在原始论文中),结果是知识密集型任务指标提高了约 5%(与带有 RAG 的 Llama2 65B 相比),常识推理任务也提高了几个百分点。

评估

对于 RAG 系统的性能评估,存在几种框架,它们都采用了一些独立的指标,比如整体的答案相关性、答案的基础性、忠实度和检索上下文的相关性。

在前面提到的 Ragas 中,使用了 忠实度答案相关性 作为生成答案质量的指标,以及经典上下文 精确度召回率 作为 RAG 方案检索部分的指标。

在最近发布的由 Andrew NG 提供的 Building and Evaluating Advanced RAG 短期课程中,LlamaIndex 和评估框架 Truelens 提出了 RAG 三元组 — 检索上下文相关性、基础性(LLM 答案在提供的上下文中的支持程度)和答案相关性。

最关键且可控的指标是 检索上下文相关性 — 基本上是上述高级 RAG 流程的第 1 到 7 部分,再加上编码器和排序器的微调部分,旨在改进此指标,而第 8 部分和 LLM 微调则侧重于答案相关性和基础性。

一个相当简单的检索器评估流程的良好示例可以在 此处 找到,并且它已经应用在编码器微调部分。
更加先进的方法不仅考虑了 命中率,还考虑了 平均倒数排名,这是一种常见的搜索引擎指标,以及生成的答案指标,比如忠实度和相关性,这在 OpenAI 的 cookbook 中有所展示。

LangChain拥有一个非常先进的评估框架LangSmith,可以实现自定义评估器,并监视RAG管道内运行的轨迹,以使系统更加透明。

如果您正在使用LlamaIndex构建,可以使用rag_evaluator llama pack来快速评估您的管道,使用公共数据集。

结论

我尝试概述了RAG的核心算法方法,并希望通过示例展示其中一些,希望这能激发一些新的想法,尝试在您的RAG管道中使用,或者为今年发明的各种技术带来一些系统性 — 对我来说,2023年是迄今为止最令人兴奋的ML年份。

还有许多其他事情需要考虑,比如基于 网络搜索的RAG(由LlamaIndex的RAGswebLangChain等),深入研究 主体架构(以及最近OpenAI在这场游戏中的赌注)以及一些关于LLMs长期记忆的想法。

RAG系统的主要生产挑战除了回答相关性和忠实度外,还有速度,特别是如果您对更灵活的基于代理的方案感兴趣,但这是另一篇文章的事情。这种流式特性ChatGPT和大多数其他助手使用的方式并不是随机的赛博朋克风格,而只是一种缩短感知答案生成时间的方式。
这就是为什么我看到较小的LLMs和最近发布的Mixtral和Phi-2在这个方向上引领我们,有一个非常光明的未来。

主要参考资料都收集在我的知识库中,有一个合作伙伴可以与这些文档进行交流:https://app.iki.ai/playlist/236.

原文:https://pub.towardsai.net/advanced-rag-techniques-an-illustrated-overview-04d193d8fec6