Press "Enter" to skip to content

高级RAG技术:图解概览 (Gāojí RAG jìshù tújiě gàilǎn)

在Noorderplatsoen的宁静中撰写文章的Groningen Martinitoren

对高级检索增强生成技术和算法的全面研究,系统化各种方法。本文附带了一个链接收藏夹,其中引用了各种实现和研究。

由于本文的目标是概述和解释可用的RAG算法和技术,我不会深入代码的实现细节,只是对其进行引用,并将其留给广泛的文档和教程。

介绍

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

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

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

即使是向量检索领域也受到了这种热潮的推动,尽管基于嵌入的搜索引擎早在2019年就应用了faiss。以现有的开源搜索索引(主要是faiss和nmslib)为基础构建的向量数据库初创公司,如chroma、weavaite.io和pinecone,最近还增加了输入文本的额外存储以及其他一些工具。

有两个最著名的开源库适用于基于LLM的流水线和应用程序——LangChain和LlamaIndex,它们分别于2022年10月和11月创立,受到ChatGPT发布的启发,并在2023年获得了巨大的采用。

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

问题是大多数教程选取一种或几种技术并详细解释如何实现它们,而不是描述可用工具的全部种类。

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

朴素的RAG

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

作者提供的一个图表,以及文中后续的所有图表

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

提示可以是这样的:

一个RAG提示的例子

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

显然,尽管OpenAI是LLM提供商中的市场领导者,但还有一些其他选择,例如Anthropic的Claude、Mistral的最新潮流较小但非常有能力的模型Mixtral、Microsoft的Phi-2,以及许多开源选项,如Llama2OpenLLaMAFalcon,这样您就可以为RAG流程选择人工大脑。

高级RAG

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

高级RAG架构的一些关键组件。它更像是可用工具的选择而不是蓝图。

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

1. 分块与向量化

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

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

要考虑块的大小参数-它取决于您使用的嵌入模型及其在标记上的容量,标准的Transformer编码器模型如基于BERT的句子变换器最多可以处理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 分层索引

高级RAG技术:图解概览 (Gāojí RAG jìshù tújiě gàilǎn) 四海 第5张

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

2.3 假设性问题和HyDE

另一种方法是要求LLM为每个块生成一个问题,并将这些问题嵌入向量中,运行时对该问题向量索引进行查询搜索(将块向量替换为问题向量),然后在检索后将其作为LLM获取答案的上下文发送给原始文本块。与实际块相比,这种方法提高了搜索质量,因为查询和假设性问题之间的语义相似度更高。

还有一个反向逻辑的方法叫做HyDE,它要求LLM根据查询生成一个假设性响应,然后将其向量与查询向量一起用于提高搜索质量。

2.4 上下文丰富化

这里的概念是为了提高搜索质量而检索较小的块,但同时添加上下文以供LLM进行推理。有两个选择-按句子扩展上下文或将文档递归地分割为多个较大的父块,这些父块包含较小的子块。

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

高级RAG技术:图解概览 (Gāojí RAG jìshù tújiě gàilǎn) 四海 第6张

绿色部分是在索引搜索时找到的句子嵌入,整个黑色+绿色段落被送给LLM,用于扩大其推理时的上下文

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

这里的思想与句子窗口检索器非常相似-搜索更细粒度的信息,然后在将上下文发送给LLM进行推理之前扩展上下文窗口。将文档分割为较小的子块,这些子块与较大的父块相关联。

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

首先在检索时提取较小的块,然后如果在前k个检索到的块中有多于n个块链接到同一父节点(更大的块),则用这个父节点替换LLM所接收的上下文-这类似于将几个检索到的块自动合并为一个较大的父块,因此称为此方法。只需注意-搜索仅在子节点索引中执行。深入了解递归检索器+节点引用,请参阅有关LlamaIndex教程

一个相对旧的想法,即结合关键字为基础的传统搜索(如tf-idf或搜索行业标准BM25)和现代语义或向量搜索,将其组合在一个检索结果中。唯一的技巧在于正确地组合具有不同相似度得分的检索结果-这个问题通常可以通过Reciprocal Rank Fusion算法来解决,该算法对检索结果进行重新排序以生成最终结果。

高级RAG技术:图解概览 (Gāojí RAG jìshù tújiě gàilǎn) 四海 第8张

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

在LlamaIndex中以相似的方式完成

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

3. 重新排序和过滤

因此,我们使用上述任何算法得到了我们的检索结果,现在是时候通过过滤、重新排序或某些转换来对其进行精炼了。在LlamaIndex中,有各种可用的后处理器,可以根据相似度得分、关键词、元数据进行结果过滤,或使用其他模型进行重新排序,比如LLM、sentence-transformer交叉编码器,Cohere重新排序端点或基于元数据(例如日期)进行重新排序 – 基本上,您可以想象的所有情况。

这是将我们检索到的上下文馈送给LLM以获取结果答案之前的最后一步。

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

4. 查询转换

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

Query transformation principles illustrated

如果查询复杂,LLM可以将其分解为几个子查询。例如,如果您问:“GitHub上哪个框架的星星更多,LangChain还是LlamaIndex?”,我们在我们的语料库中不太可能找到直接的比较,所以将该问题分解为两个假设更简单和更具体的信息检索的子查询是有意义的:“LangChain在GitHub上有多少颗星星?” – “LlamaIndex在GitHub上有多少颗星星?”,它们将同时执行,然后将检索到的上下文合并为单个提示,供LLM综合生成最终答案。这两个库都已经实现了这个功能 – 在LangChain中作为多查询检索器,在LlamaIndex中作为子问题查询引擎

  1. Step-back prompting 使用LLM生成更通用的查询,从中检索到更通用或高级的上下文,有助于我们原始查询的答案。也执行原始查询的检索,并将两个上下文馈送到LLM的最终答案生成步骤。这是LangChain的实现
  2. 查询重写使用LLM重新构造初始查询以提高检索。LangChain和LlamaIndex都有实现,虽然有些不同,但我认为LlamaIndex的解决方案在这方面更强大。

参考文献引用

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

有几种方式可以实现这一点:

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

5. 聊天引擎

建立一个可以对单个查询进行多次工作的RAG系统的下一个重要功能是聊天逻辑,考虑到对话的上下文,就像在LLM时代之前的经典聊天机器人一样。这需要支持后续问题、代词或与之前对话上下文相关的任意用户命令。它通过查询压缩技术,同时考虑聊天上下文和用户查询来解决。

和往常一样,有几种方法可以进行上下文压缩 — — 一种流行且相对简单的ContextChatEngine,首先获取与用户查询相关的上下文,然后将其与记忆缓冲区中的聊天历史一起发送给LLM,在生成下一个答案时让LLM知道先前的上下文。

还有一个稍微复杂一点的情况是CondensePlusContextMode——每个操作中,聊天历史和最后一条消息被压缩成一个新的查询,然后该查询发送到索引中,并将检索到的上下文与原始用户消息一起传递给LLM以生成答案。

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

不同聊天引擎类型和原则的插图

还有其他聊天引擎类型,比如ReAct Agent,但我们将直接跳到第7节中的代理。

6. 查询路由

查询路由是LLM决策的一部分,决定了下一步要做什么 — — 通常的选项是进行摘要、对某个数据索引进行搜索,或者尝试多条不同路线,然后将它们的输出合成一个单一的答案。

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

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

无论是LlamaIndex还是LangChain都支持查询路由器。

7. RAG中的代理

代理(由LangChainLlamaIndex支持)几乎自从LLM API首次发布以来就存在-想法是为具备推理能力的LLM提供一组工具和完成任务的能力。这些工具可能包括一些确定性函数,如任何代码函数、外部API甚至其他代理-这个LLM链接的想法正是LangChain名称的来源。

代理本身是一个非常庞大的事物,在RAG概述中不可能深入探讨这个话题,所以我只会继续讨论基于代理的多文档检索案例,对OpenAI Assistants进行简短介绍,因为它是一个相对比较新的东西,在最近的OpenAI开发者大会上作为GPTs进行了介绍,并在下面描述的RAG系统的核心处于工作状态。

OpenAI Assistants基本上已经实现了我们以前在开源中拥有的大量所需的工具-聊天历史记录、知识存储、文件上传接口,以及最重要的函数调用API。这个API可以将自然语言转换为对外部工具或数据库查询的API调用。

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

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

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

此方案说明了一个高级的RAG架构,其中每个涉及的代理都做出了许多路由决策。这种方法的优点是能够比较不同文档中描述的不同解决方案或实体及其摘要,同时也包括了经典的单文档摘要和问答机制-基本上涵盖了最常见的与文档集进行聊天的用例。

一个图表,说明了涉及查询路由和代理行为模式的多文档代理。

这种复杂方案的缺点可以从图片中猜到-由于我们的代理内部与LLMs之间的多次往返迭代,它的速度有点慢。请注意,LLM调用始终是RAG流水线中最耗时的操作-搜索是通过设计进行了速度优化的。因此,对于大型多文档存储,建议考虑对此方案进行一些简化,以实现可扩展性。

8. 响应综合器

这是任何RAG管道的最后一步 – 基于我们精心检索的所有上下文和初始用户查询生成答案。最简单的方法就是一次性将所有获取的上下文(超过某个相关度阈值)与查询一起串联并输入LLM中。但是,始终存在其他更复杂的选项,涉及多次LLM调用来细化检索的上下文并生成更好的答案。

响应综合的主要方法有:1. 通过逐个分块将检索到的上下文发送到LLM来迭代地完善答案 2. 将检索到的上下文进行概括,以适应提示3. 基于不同的上下文块生成多个答案,然后进行串联或概括它们。更多详细信息请查看响应综合器模块文档

编码器和LLM微调

这种方法涉及对我们的RAG管道中涉及的两个DL模型中的一些进行微调 – 无论是Transformer编码器(负责嵌入质量和因此上下文检索质量)还是LLM(负责最佳使用提供的上下文来回答用户查询) – 幸运的是,后者是良好的少样本学习器。

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

编码器微调

我对编码器微调方法有点怀疑,因为最新的优化用于搜索的Transformer编码器非常高效。因此,我测试了在LlamaIndex笔记本设置中的<bge-large-en-v1.5的微调提供的性能增加,并显示出2%的检索质量提升。虽然没有惊人之处,但了解该选项还是很不错的,尤其是如果您正在为狭窄的领域数据集构建RAG。</bge-large-en-v1.5

排序器微调

另一个古老的好选择是使用交叉编码器对检索结果进行重新排序,如果您不完全信任基础编码器。它的工作方式是 – 您将查询和每个前 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框架显示忠实度指标增加了5%,这意味着经过微调的GPT 3.5-turbo模型在利用提供的上下文生成答案方面比原始模型更好。

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

如果您对于RAG的LLM微调有更好的方法,请在评论区分享您的专业知识,特别是如果它们应用于较小的开源LLMs。

评估

几种RAG系统性能评估框架共享着一种将整体答案相关性、答案可靠性、忠实性和检索上下文相关性等数个独立的度量标准的思想。

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

在最近发布的由Andrew NG提供的一门很好的短期课程构建和评估高级RAG中,提到了LlamaIndex和评估框架Truelens,他们提出了RAG三要素——与查询相关的检索上下文相关性可靠度(LLM答案在提供的上下文中的支持程度)和与查询相关的答案相关性

关键且最可控的度量标准是检索上下文相关性——基本上是上述高级RAG流程中的1-7部分加上编码器和排序器微调部分,旨在改善此度量标准,而第8部分和LLM微调则专注于答案相关性和可靠度。

一个非常简单的检索器评估流程示例可以在此处找到,它在编码器微调部分中应用。更高级的方法考虑的不仅仅是命中率,还包括常见的搜索引擎度量标准Mean Reciprocal Rank以及生成答案的忠实性和相关性等指标,这在OpenAI的cookbook中展示。

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

如果您正在使用LlamaIndex构建,可以找到一个rag_evaluator llama pack,它提供了一个快速工具来评估您的流程,并使用公共数据集。

结论

我尝试概述了RAG的核心算法方法,并且希望通过展示其中一些方法来激发一些新的创意,可以在您的RAG流程中尝试,或者对今年发明的各种技术方法进行系统化的整理。对我来说,2023年是迄今为止最令人兴奋的机器学习年份。

还有许多其他需要考虑的事情,例如基于网络搜索的RAG(LlamaIndex的RAGswebLangChain等),深入研究主体架构(以及最近OpenAI对该领域的利益投资)以及有关LLMs长期记忆的一些想法。

除了回答的相关性和可靠性之外,RAG系统的主要生产挑战是速度,特别是对于更灵活的基于代理的方案来说,但这是另一个帖子中要讨论的内容。 ChatGPT和大多数其他助手使用的实时生成功能并非随机的赛博朋克风格,而只是缩短了感知的答案生成时间的一种方式。这就是为什么我认为小型LLMs和最近发布的Mixtral和Phi-2有着非常光明的前景,它们正在引领我们朝着这个方向发展。

非常感谢您阅读这篇长篇文章!

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

在LinkedIn上找到我:LinkedInTwitter

Leave a Reply

Your email address will not be published. Required fields are marked *