原文:https://pub.towardsai.net/advanced-rag-techniques-an-illustrated-overview-04d193d8fec6
文章知识结构图
一、简介
检索增强生成(RAG)通过结合从数据源检索到的信息,为大语言模型(大语言模型)生成的回答提供依据。简而言之,RAG 结合了搜索和大语言模型提示功能,即在模型回答问题时,以搜索算法发现的信息作为上下文环境。这里,查询请求和检索到的上下文同时被注入发送至大语言模型的提示中。
截至 2023 年,RAG 成为基于大语言模型系统中最流行的架构。许多产品几乎完全基于 RAG 构建,涵盖了从结合网络搜索引擎和大语言模型的问答服务,到各种数据交互聊天应用。
即便是向量搜索领域也受到了这一趋势的影响,尽管基于嵌入的搜索引擎在 2019 年就已经使用了 faiss 技术。诸如 chroma、weavaite.io 和 pinecone 这样的向量数据库初创企业,都是在现有的开源搜索索引——主要是 faiss 和 nmslib——的基础上搭建的,并在近期加入了用于存储输入文本的额外存储空间以及其他工具。
LangChain
和 LlamaIndex 是两个最著名的基于大语言模型的流水线和应用开源库,它们分别在
2022 年 10 月和 11 月成立,仅相差一个月,受到 ChatGPT 发布的启发,在 2023 年迅速获得广泛应用。
本文旨在系统整理关键的高级 RAG 技术,并主要参考
LlamaIndex 中的实现案例,以帮助其他开发者更深入地了解这一技术。
然而,大多数教程只关注一种或几种技术的具体实现细节,而未能全面描述所有可用工具的多样性。
此外,LlamaIndex 和 LangChain 作为迅速发展的杰出开源项目,其文档的详细程度已经超越了 2016 年的机器学习教科书。
二、原始 RAG
本文讨论的 RAG 流水线起始于一个文本文档语料库 。我们不涉及此前搜集文本资料的步骤,把它们交给那些强大的开源数据加载器,它们能连接到各种来源,从 Youtube 到 Notion 都不在话下。
简单地说,标准的 RAG 案例包括以下步骤:
1. 将文本切分成多个段落,使用 Transformer Encoder 模型将这些段落转换成向量,然后把这些向量存入一个索引里。
2. 为大语言模型(大语言模型)创建一个提示,指引模型根据我们在搜索步骤中找到的上下文来回答用户的问题。在实际运行时,我们会用同一个 Encoder 模型对用户的查询进行向量化处理,在索引中搜索这个查询向量,找出最相关的前 k 个结果。
3. 我们从数据库中提取对应的文本段落,并将它们作为上下文融入到大语言模型的提示中。
提示工程是改进 RAG 流水线的一种非常经济实惠的方法。你应该查看 OpenAI 非常全面的提示工程指南。
显然,虽然 OpenAI 是大语言模型(大语言模型)供应商的市场领头羊,但也有很多其他选择,比如
Anthropic 的 Claude,最近流行的小型但功能强大的模型如 Mistral 的 Mixtral、微软的 Phi-2,还有诸如 Llama2、OpenLLaMA、Falcon 等多种开源选择。因此,你可以为你的 RAG 流水线挑选最合适的“大脑”。
三、高级 RAG 技术概览
接下来,我们将深入探讨高级 RAG 技术的综述。这里展示了一个方案,描述了核心步骤和涉及的算法。为了保持方案的清晰可读,我们省略了一些逻辑循环和复杂的多步骤智能体行为。
方案中的绿色部分代表我们将重点讨论的核心 RAG 技术,而蓝色部分则是文本。并非所有高级 RAG 的想法都能轻松地在一张图表中展示出来,例如,一些扩展上下文的方法就被省略了——我们会在后面详细讨论。
1.
分块和向量化
我们需要创建一个向量索引,这个索引代表了我们文档的内容。然后在运行时,我们要在所有这些向量与查询向量之间寻找最小的余弦距离,以找到最接近的语义含义。
1.1
分块
Transformer
模型的输入序列长度是固定的。即便输入上下文窗口较大,单个句子或几个句子的向量通常比覆盖几页文本的平均向量更能准确反映它们的语义意义(这也取决于具体模型,但总体而言是这样)。因此,我们需要将数据进行分块处理——把原始文档分割成一定大小的部分,同时保持其原有意义(如将文本分割成句子或段落,而不是将一个句子切成两半)。市面上有各种能够完成此任务的文本分割器。
块的大小是一个需要考虑的因素——它取决于你所使用的嵌入模型以及其在 Token 上的容量。标准的 Transformer Encoder 模型(例如基于 BERT 的句子转换器)最多处理 512 个 Token,而像
OpenAI 的 ada-002 则能处理更长的序列,例如
8191 个 Token。这里需要权衡的是,为大语言模型提供足够的上下文以便于推理,与保持文本嵌入具体到足以高效执行搜索之间的平衡。在 LlamaIndex 中,通过 NodeParser 类及其一些高级选项(例如定义你自己的文本分割器、元数据、节点/块关系等)来解决这一问题。
1.2
向量化
下一步是选择一个模型来对我们的块进行嵌入——有很多可选模型,我选择了像 bge-large 或 E5 嵌入系列这样优化搜索的模型。你可以查看 MTEB 排行榜,了解最新的更新情况。
想要实现分块和向量化步骤的端到端实现,可以参考 LlamaIndex 中完整数据摄取流程的一个示例。
2.
搜索索引
2.1.
向量存储索引
RAG 流水线的核心部分是搜索索引,用于存储我们在前一步骤中得到的向量化内容。最基本的实现方法是使用平面索引 —— 直接计算查询向量与所有数据块向量之间的距离。
更高效的搜索索引,专为处理超过 10000 个元素的大规模检索而优化,通常采用向量索引,如 faiss、nmslib 或 annoy,它们采用近似最近邻算法,比如聚类、树形结构或
HNSW 算法。
还有一些托管解决方案,如 OpenSearch 或
ElasticSearch,以及像 Pinecone、Weaviate
或 Chroma 这样的向量数据库,它们在后台处理第
1 步中描述的数据摄取流程。
根据你选择的索引类型、数据和搜索需求,你还可以存储元数据,并利用元数据过滤功能来搜索特定日期或来源的信息。
LlamaIndex 支持多种向量存储索引,但也支持其他更简单的索引实现,如列表索引、树索引和关键词表索引——我们将在融合检索部分进一步讨论这些实现方式。
2.2. 分层索引
如果你需要从大量文档中检索信息,你需要能够高效地搜索这些文档,找到相关信息,并将其整合成一个包含参考来源的统一答案。对于大型数据库,一个有效的方法是创建两个索引:一个包含摘要,另一个包含所有的文档块,并通过两步搜索,先利用摘要筛选相关文档,再在这些相关文档中进行具体搜索。
2.3. 假设性问题和 HyDE
另一个策略是让大语言模型为每个数据块生成一个问题,并将这些问题转换为向量。在运行时,对这些问题向量的索引进行查询搜索(将索引中的数据块向量替换为问题向量),然后在检索后将原始文本块作为上下文发送给大语言模型,以获取答案。这种方法由于查询和假设性问题之间的语义相似性更高,相较于实际数据块,可以提升搜索质量。
还有一种反向逻辑的方法叫做 HyDE —— 让大语言模型根据查询生成一个假设性回应,然后使用其向量与查询向量共同提高搜索质量。
这里的想法是通过检索更小的数据块来提高搜索质量,同时添加额外上下文,以供大语言模型进行推理。有两种方法:一是在检索到的小块周围扩展上下文,二是将文档递归分割为包含小块的更大父块。
·
句子窗口检索 :在这个方法中,文档的每个句子都单独进行嵌入,这提高了查询到上下文的余弦距离搜索的准确度。为了在检索到最相关的单个句子之后更好地进行推理,我们会在检索到的句子前后扩展 k 个句子作为上下文窗口,然后将这个扩展的上下文发送给大语言模型。绿色部分表示搜索索引时找到的句子嵌入,而整个黑色和绿色的段落一起被送给大语言模型,用于在处理提供的查询时扩展其上下文。
·
自动合并检索器(也称为父文档检索器):这里的思路与句子窗口检索器类似 —— 首先寻找更具体的信息片段,然后在将这些上下文提供给大语言模型进行推理前,扩展上下文范围。文档被划分为较小的子块,这些子块都与更大的父块相关联。在检索过程中,首先获取较小的数据块。如果在检索到的前 k 个数据块中,超过 n 个数据块与同一父节点 (更大的数据块)相关联,我们会用这个父节点来替换提供给大语言模型的上下文。这个过程类似于自动合并几个检索到的小数据块为一个更大的父数据块,因此得名。值得注意的是,搜索仅在子节点的索引内进行。要深入了解,可以查看 LlamaIndex 关于递归检索器和节点引用的教程。
2.5. 融合检索或混合搜索
一个相对较旧的想法,即你可以从两个世界中汲取精华 —— 基于关键词的老式搜索 —— 稀疏检索算法,如 tf-idf 或搜索行业标准 BM25
—— 以及现代的语义或向量搜索,并将其结合在一个检索结果中。这里唯一的技巧是如何恰当地结合不同相似度评分的检索结果 —— 这个问题通常通过使用互惠排名融合算法来解决,对检索结果进行重新排名以得出最终输出。
在 LangChain 中,这一过程通过
Ensemble Retriever 类实现,它结合了你定义的多个检索器,如基于 faiss 的向量索引和基于 BM25 的检索器,并使用 RRF 算法进行结果的重新排名。
LlamaIndex 中的实现方式与此相似。
混合或融合搜索一般能提供更优的检索结果,因为它结合了两种互补的搜索算法,既考虑了查询与存储文档之间的语义相似性,又考虑了关键词匹配。
在使用上述任一算法获取检索结果后,现在是时候通过过滤、重新排名或一些转换来优化这些结果。LlamaIndex 提供了多种后处理器,可以基于相似度评分、关键词、元数据进行过滤,或使用大语言模型、句子转换器跨编码器、Cohere 重新排名端点等其他模型进行重新排名,甚至可以基于日期新近性等元数据进行排序——几乎包括了所有你能想到的方法。
这是在将检索到的上下文提供给大语言模型以获取最终答案之前的最后一步。
现在,我们将探讨更高级的 RAG 技术,如查询转换和路由,这两种技术都涉及到大语言模型,从而展现了智能体行为——在我们的 RAG 流水线中,涉及到大语言模型的复杂逻辑推理。
3. 重排与过滤
在使用上述任一算法获取检索结果后,现在是时候通过过滤、重新排名或一些转换来优化这些结果。LlamaIndex 提供了多种后处理器,可以基于相似度评分、关键词、元数据进行过滤,或使用大语言模型、句子转换器跨编码器、Cohere 重新排名端点等其他模型进行重新排名,甚至可以基于日期新近性等元数据进行排序——几乎包括了所有你能想到的方法。
这是在将检索到的上下文提供给大语言模型以获取最终答案之前的最后一步。
现在,我们将探讨更高级的 RAG 技术,如查询转换和路由,这两种技术都涉及到大语言模型,从而展现了智能体行为——在我们的 RAG 流水线中,涉及到大语言模型的复杂逻辑推理。
4. 查询转换
查询转换是利用大语言模型(大语言模型)作为推理引擎,对用户输入进行调整,以提升检索效果的技术。实现这一目标有多种方法。
如果查询内容复杂,大语言模型可以将其分解为几个子查询。例如,当你问:“在 Github 上,Langchain 和 LlamaIndex 哪个框架星星更多?” 我们不太可能在语料库的某些文本中直接找到答案,因此把这个问题分解成两个更具体、更简单的子查询是合理的:
·
“Langchain 在 Github 上有多少星?”
·
“LlamaIndex 在 Github 上有多少星?”
这些子查询将并行执行,然后将检索到的上下文合并成一个整体,供大语言模型综合出最初查询的终极答案。Langchain 作为多查询检索器和
LlamaIndex 作为子问题查询引擎都实现了这一功能。
·
回溯提示技术通过大语言模型生成更通用的查询,从而检索出更广泛或更高层次的上下文,有助于为原始查询的答案提供基础。同时,也会对原始查询进行检索,并在最终答案生成步骤中将两种上下文一起输入大语言模型。这是 LangChain 的实现方式。
·
查询重写则利用大语言模型重新构造初始查询,以提高检索效果。LangChain 和
LlamaIndex 都有各自的实现方式,虽然略有不同,但我认为 LlamaIndex 的解决方案在这方面更为强大。
参考引用
这部分没有编号,因为它更像是一种辅助工具而非检索改进技术,但仍然非常重要。如果我们由于初始查询的复杂性(需要执行多个子查询并将检索到的上下文合并成一个答案),或因为在不同文档中找到了单个查询的相关上下文而使用了多个来源来生成答案,就会出现如何准确引用这些来源的问题。
有几种方法可以做到这一点:
·
将引用任务嵌入到我们的提示中,并要求大语言模型提及所用来源的 ID。
·
将生成的回应的部分与索引中的原始文本块进行匹配。LlamaIndex 提供了一种基于模糊匹配的高效解决方案。如果你不熟悉模糊匹配,它是一种极其强大的字符串匹配技术。
5. 聊天引擎
构建高效的 RAG 系统的下一个关键是引入聊天逻辑,这与大语言模型(大语言模型)出现之前的传统聊天机器人同样重要,都需要考虑对话上下文。这对于处理后续问题、指代或与先前对话上下文相关的用户命令至关重要。这一问题通过结合用户查询和聊天上下文的查询压缩技术得以解决。
常见的上下文压缩方法有几种,例如:
·
流行且相对简单的 ContextChatEngine,它首先检索与用户查询相关的上下文,然后将其连同聊天历史记录从内存缓冲区发送给大语言模型,使其在生成下一个答案时能够参考之前的上下文。
·
更复杂的 CondensePlusContextMode,在每次交互中,将聊天历史和最后一条消息压缩成一个新查询,然后将该查询发送到索引,检索到的上下文连同原始用户消息一起传递给大语言模型,以生成答案。
值得一提的是,LlamaIndex 还支持基于
OpenAI 智能体的聊天引擎,提供了更灵活的聊天模式,而 Langchain 也支持
OpenAI 的功能性 API。
还有像 ReAct 智能体 这样的其他聊天引擎类型,但我们接下来将直接跳转到第 7 节,讨论智能体本身
6. 查询路由
查询路由是大语言模型驱动的决策步骤,用于在接收到用户查询后决定下一步的行动 —— 通常的选择包括进行摘要、对某些数据索引执行搜索,或尝试多种不同的路由后将其输出综合成单一答案。
查询路由器还用于选择发送用户查询的索引或数据存储位置。这可能是因为有多个数据来源,如经典的向量存储、图数据库或关系数据库,或者是因为有多层索引结构 —— 在多文档存储的常见情况下,会有一个摘要索引和另一个文档块向量索引。
定义查询路由器包括设定其可能的选择。路由选项的选择通过大语言模型调用来执行,返回的结果采用预定义格式,用于将查询路由至指定索引。在涉及智能体行为时,可能还会将查询路由至子链甚至其他智能体,如下面多文档智能体方案所示。
LlamaIndex 和 LangChain 都提供了查询路由器的支持。
7. RAG 中的代理
智能体(由 Langchain 和
LlamaIndex 支持)自大语言模型 API 首次发布以来就已经存在。其核心思想是为具备推理能力的大语言模型提供一套工具和待完成的任务。这些工具可能包括确定性函数(如代码函数或外部 API)或其他智能体——正是这种大语言模型链接的理念促成了
LangChain 的命名。
智能体本身是一个庞大的领域,在 RAG 概览中无法深入探讨,所以我将直接介绍基于智能体的多文档检索案例,并简要介绍 OpenAI 助理,这是 OpenAI 开发者大会上作为 GPTs 提出的相对新颖的概念,在下面描述的 RAG 系统中发挥作用。
OpenAI 助理实现了许多围绕大语言模型所需的工具,这些工具之前是开源的,包括聊天历史、知识存储、文档上传界面,以及可能最重要的,功能调用 API。这些 API 能将自然语言转换为对外部工具或数据库查询的调用。
LlamaIndex 中有一个 OpenAIAgent 类,将这些高级逻辑与
ChatEngine 和 QueryEngine 类结合,提供基于知识和上下文感知的聊天以及在一次对话轮次中调用多个 OpenAI 功能的能力,这真正带来了智能的代理行为。
让我们来看看多文档智能体方案——这是一个复杂的设置,涉及在每个文档上初始化一个智能体(OpenAIAgent),负责文档摘要和经典问答机制,以及一个负责将查询路由至文档智能体并合成最终答案的顶级智能体。
每个文档智能体拥有两种工具——向量存储索引和摘要索引,并根据路由的查询决定使用哪一个。对于顶级智能体而言,所有文档智能体都是其工具。
这个方案展示了一个高级 RAG 架构,涉及每个参与智能体做出的众多路由决策。这种方法的优势在于能够比较不同文档及其摘要中描述的不同解决方案或实体,并结合经典的单文档摘要和问答机制——这基本涵盖了与文档集合互动的最常见聊天用例。
这种复杂的方案可能存在的缺点是,由于需要与智能体内的大语言模型(大语言模型)进行多次迭代,其速度可能较慢。值得注意的是,大语言模型的调用通常是 RAG 流水线中最耗时的操作,而搜索设计上是优化了速度的。因此,对于大型多文档存储,建议考虑对这个方案进行简化,使其更具可扩展性。
8. 响应合成器
响应合成器是任何 RAG 流水线的最后一步——基于我们仔细检索到的所有上下文和初始用户查询生成答案。最简单的方法是将所有检索到的上下文(超过一定相关性阈值)与查询一起一次性输入大语言模型。然而,还有其他更复杂的选项,涉及多次调用大语言模型来精炼检索到的上下文并生成更优答案。
响应合成的主要方法包括:
1.
通过将检索到的上下文分块发送给大语言模型来迭代精炼答案。
2.
摘要化检索到的上下文以适应提示。
3.
基于不同上下文块生成多个答案,然后将它们连接或摘要化。更多详情请参考响应合成器模块的文档。
四、编码器和大语言模型的微调
这种方法涉及对 RAG 流水线中的两个深度学习模型之一进行微调——要么是负责嵌入质量和上下文检索质量的 Transformer 编码器,要么是负责充分利用提供的上下文以回答用户查询的大语言模型(大语言模型)。幸运的是,后者擅长少样本(甚至是零样本)学习。
现在的一个重要优势是,可以利用像 GPT-4 这样的高端大语言模型生成高质量的合成数据集。
但需要意识到的是,使用专业研究团队在精心收集、清洗和验证的大型数据集上训练的开源模型,并用小型合成数据集进行快速调整,可能会在总体上限制模型的能力。
1. 编码器微调
我曾对编码器微调方法持有一定怀疑,因为最新的为搜索优化的 Transformer 编码器已经相当高效。然而,在 LlamaIndex 的笔记本环境中,我测试了对 bge-large-en-v1.5(写作时位于 MTEB 排行榜前四)的微调效果,发现它带来了 2% 的检索质量提升。尽管提升不是很显著,但了解这个选项仍然有价值,特别是在你为特定领域数据集构建 RAG 时。
2. 排名器微调
另一个常见选择是使用交叉编码器重新排列检索结果,特别是当你对基础编码器的信任度不足时。这个过程包括将查询和每个前 k 个检索到的文本块传递给交叉编码器,并用 SEP 令牌分隔,然后微调它以对相关块输出 1,对不相关块输出 0。可以在这里找到一个很好的微调过程示例,结果显示通过交叉编码器微调,成对得分提高了 4%。
3. 大语言模型微调
最近 OpenAI 开始提供大语言模型微调
API,LlamaIndex 也有关于在
RAG 设置中微调 GPT-3.5-turbo 的教程,旨在“提炼”GPT-4 的部分知识。这里的想法是获取一个文档,用 GPT-3.5-turbo 生成一些问题,然后使用 GPT-4 根据文档内容生成这些问题的答案(构建一个由 GPT-4 驱动的 RAG 流水线),接着在这个问题-答案对数据集上微调 GPT-3.5-turbo。用于 RAG 流水线评估的 ragas 框架显示了 5% 的忠实度指标提升,意味着微调后的
GPT-3.5-turbo 模型比原始模型更好地利用提供的上下文生成答案。
一种更复杂的方法在最近的论文《RA-DIT: Retrieval Augmented Dual
Instruction Tuning》中展示,提出了一种同时调整大语言模型和检索器(原论文中的双编码器)的技术,基于查询、上下文和答案的三元组。关于实现细节,请参考这个指南。这种技术用于通过微调 API 微调 OpenAI 大语言模型,也用于微调开源 Llama2 模型(在原论文中),在知识密集型任务指标上提升了约 5%(与搭载 RAG 的 Llama2 65B 相比),在常识推理任务上也有几个百分点的提升。
五、评估
RAG 系统的性能评估通常涉及几个框架,它们的共同理念是设定几个独立指标,如整体答案相关性、答案基础性、忠实度和检索到的上下文相关性。
如前文所提到的 Ragas 使用真实性和答案相关性作为生成答案的质量指标,以及用于 RAG 方案检索部分的传统上下文精确度和召回率。
在 Andrew NG 最近发布的短课程《构建和评估高级
RAG》中,LlamaIndex 和评估框架
Truelens 提出了 RAG 三元组:
·
检索到的上下文与查询的相关性
·
基于性(大语言模型答案被提供的上下文所支持的程度)
·
答案与查询的相关性。
最关键且最可控的指标是检索到的上下文相关性——基本上上文所述的高级 RAG
流水线的第 1-7 部分及编码器和排名器微调部分都旨在提升这一指标,而第 8 部分和大语言模型微调则专注于答案相关性和基础性。
关于相对简单的检索器评估流程的一个好例子可以在这里找到,它在编码器微调部分中得到应用。一种更高级的方法不仅考虑命中率,还考虑平均倒数排名(一种常见的搜索引擎指标)以及生成答案的忠实度和相关性,这在 OpenAI 的烹饪书中得到展示。
LangChain 有一个较为高级的评估框架 LangSmith,可以实施自定义评估器,并监控
RAG 流水线内部的跟踪,以增强系统的透明度。
如果你正在使用 LlamaIndex 构建系统,可以使用
rag_evaluator llama pack 这个工具,它提供了一个快速评估你的流水线的方法,适用于公共数据集。
六、结论
我尝试勾勒出 RAG 的核心算法方法,并通过示例来阐述其中一些,希望这能激发你在 RAG 流水线中尝试新的想法,或为今年发明的各种技术带来一定的系统性。对我来说,2023 年是迄今为止最令人激动的一年。
还有许多其他方面需要考虑,例如基于网络搜索的 RAG(如 LlamaIndex、webLangChain 的 RAGs)、更深入地探索智能体架构(包括 OpenAI 最近在这方面的投入)以及关于大语言模型长期记忆的一些想法。
RAG 系统面临的主要生产挑战之一,除了答案相关性和忠实度之外,还有速度问题,尤其是在采用更灵活的基于智能体的方案时。但这是另一篇文章的主题。ChatGPT 和大多数其他助手使用的流媒体功能并非无缘无故采用赛博朋克风格,而是为了缩短感知的答案生成时间。这也是我认为小型大语言模型,以及最近发布的 Mixtral 和 Phi-2
有着光明的未来,它们正在引领我们朝这个方向前进。
出自:https://mp.weixin.qq.com/s/QbMBQewCTVO_gYHh8ahtNw