微信扫码
与创始人交个朋友
我要投稿
格罗宁根,马丁尼托伦塔,作者在这里写下了这篇文章,享受着北广场的宁静。
由于本文的目的是对现有的RAG算法和技术进行概述和解释,因此我不会深入探讨代码实现细节,而只是参考了它们,让读者自行查阅丰富的_documentation & tutorials。
如果您熟悉RAG概念,请跳过简介部分。
检索增强生成(Retrieval Augmented Generation,简称RAG),为LLM提供了从某些数据源检索到的信息,以支撑其生成的答案。基本上,RAG就是搜索+LLM提示,您要求模型根据搜索算法所检索到的上下文来回答提出的查询。查询本身和检索到的上下文会被注入到发送给LLM的提示中。
RAG是2023年基于LLM的系统中最受欢迎的架构。有许多产品几乎完全是基于RAG构建的——从将网络搜索引擎与LLM结合的问答服务,到数百种与您的数据进行对话的应用程序。
即使是向量搜索领域也因为这种热潮而受到了推动,尽管基于嵌入的搜索引擎早在2019年就已经有了faiss。像chroma、weavaite.io和pinecone这样的向量数据库创业公司是建立在现有开源搜索索引之上的——主要是faiss和nmslib——最近还增加了存储输入文本的功能以及其他一些工具。
目前有两个最受欢迎的开源LLM管道和应用程序库——LangChain和LlamaIndex,它们分别在2022年10月和11月相继诞生,受到了ChatGPT发布的启发,并在2023年获得了大规模采用。
本文的目的是系统化关键的高级RAG技术,并参考它们在LlamaIndex中的实现——主要是为了帮助其他开发者更好地深入这项技术。
问题在于,大多数教程只挑选一种或几种技术,详细解释了如何实现它们,而没有描述所有可用工具的全貌。
另一件事是,无论是LlamaIndex还是LangChain都是令人惊叹的开源项目,它们发展如此迅速,以至于它们的文档已经比2016年的机器学习教材还要厚。
在本文中,RAG管道的起点将是一组文本文档——我们略过了之前的所有步骤,把它们留给了那些连接到从YouTube到Notion的任何可想象数据源的惊人开源数据加载器。
作者的示意图,以及下文中所有进一步的示意图
最简单的RAG案例简要概述如下:您将文本拆分为块,然后使用某个Transformer Encoder模型将这些块嵌入为向量,将所有这些向量放入一个索引中,最后为LLM创建一个提示,告诉它根据搜索步骤中找到的上下文来回答用户的查询。在运行时,我们用相同的Encoder模型对用户的查询进行向量化,然后在索引中执行此查询向量的搜索,找到前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提示示例
_documentation & tutorials (https://docs.llamaindex.ai/en/latest/examples/prompts/prompts_rag.html) 是您试图改进RAG管道时最简单的方式。请确保您已经查看了OpenAI的提示工程指南 (https://platform.openai.com/docs/guides/prompt-engineering/strategy-write-clear-instructions),这是一份相当全面的指南。
显然,尽管OpenAI是LLM提供商的市场领导者,但还有许多其他选择,例如Anthropic的Claude、近期非常热门的但能力出众的小型模型,如Mistral的Mixtral、微软的Phi-2以及许多开源选项,如Llama2、OpenLLaMA和Falcon,所以您可以选择适合自己RAG管道的"大脑"。
现在我们将深入探讨高级RAG技术的概述。下面是一个描述了所涉及的核心步骤和算法的示意图。为了保持示意图的可读性,一些逻辑循环和复杂的多步骤主动行为都被省略了。
高级RAG架构的一些关键组件。这更多是可用工具的选择,而不是一个蓝图。
示意图中的绿色元素是下文将讨论的核心RAG技术,蓝色的是文本。并非所有高级RAG理念都能够在单个示意图上轻易可视化,例如,各种上下文扩展方法都被省略了——我们将在过程中深入探讨。
首先,我们希望创建一个向量索引,表示我们的文档内容,然后在运行时,在所有这些向量和查询向量之间搜索最小的余弦距离,这对应于最接近的语义含义。
1.1 分块 Transformer模型有固定的输入序列长度,即使输入上下文窗口很大,单个句子或几个句子的向量比在几页文本上平均的向量(这也取决于模型,但通常如此)更好地表示了它们的语义含义,所以将您的数据分块——将初始文档分割成一些大小的块,而不会丢失其含义(按句子或段落划分,而不是将单个句子分成两部分)。有各种文本分割器实现能够完成此任务。
块的大小是需要考虑的一个参数——它取决于您使用的嵌入模型及其令牌容量,标准的Transformer Encoder模型(如基于BERT的Sentence Transformers)最多可以处理512个令牌,而OpenAI的ada-002则能够处理更长的序列,如8191个令牌,但这里的权衡是为LLM提供足够的上下文来进行推理,与执行高效搜索以获得足够具体的文本嵌入之间的平衡。这里(https://www.pinecone.io/learn/chunking-strategies/)您可以找到一项说明块大小选择问题的研究。在LlamaIndex中,这由NodeParser类涵盖,其中包含了一些高级选项,如定义自己的文本分割器、元数据、节点/块关系等。
1.2 向量化 下一步是选择一个模型来嵌入我们的块——有相当多的选择,我选用针对搜索优化的模型,如bge-large或E5嵌入系列——只需查看MTEB排行榜获取最新更新。
对于分块和向量化步骤的端到端实现,请查看LlamaIndex中完整数据摄取管道的示例。
2.1 向量存储索引
在这个示意图和下文的所有示意图中,我省略了Encoder块,直接将我们的查询发送到索引,以简化示意图。当然,查询总是先被向量化。对于前k个块的情况也是如此——索引检索前k个向量,而不是块,但我用块替换了它们,因为获取它们是一个很简单的步骤。
RAG管道的关键部分是搜索索引,用于存储我们在上一步中获得的向量化内容。最简单的实现使用平面索引——对查询向量和所有块向量进行暴力距离计算。
一个针对高效检索优化的适当搜索索引,可缩放至10000多个元素,就是向量索引,如faiss、nmslib或annoy,使用某种近似最近邻算法,如聚类、树或HNSW算法。
还有像OpenSearch或Elasticsearch这样的托管解决方案,以及像Pinecone、Weaviate或Chroma这样的向量数据库,它们在引擎盖下处理了第1步中描述的数据摄取管道。
根据您的索引选择、数据和搜索需求,您还可以与向量一起存储元数据,然后使用元数据过滤器,例如根据日期或来源来搜索信息。
LlamaIndex支持大量向量存储索引,但也有其他更简单的索引实现,如列表索引、树索引和关键词表索引——我们将在"Fusion retrieval"部分讨论后者。
如果您有许多文档需要检索,您需要能够高效地在其中搜索,找到相关信息并将其综合到单个答案中,并引用信息源。对于大型数据库,一种有效的方式是创建两个索引——一个由摘要组成,另一个由文档块向量组成,并分两步搜索,首先通过摘要过滤出相关文档,然后只在这个相关组内搜索。
另一种方法是让LLM为每个块生成一个问题并将这些问题嵌入为向量,在运行时,对这些问题向量索引(用问题向量替换块向量)执行查询搜索,然后在检索后路由到原始文本块,并将它们作为上下文发送给LLM以获得答案。这种方法提高了搜索质量,因为查询与假设性问题之间的语义相似性比我们对实际块所能获得的要高。
还有一种被称为_documentation & tutorials的反向逻辑方法——您要求LLM根据查询生成一个假设的响应,然后使用它的向量以及查询向量来提高搜索质量。
这个概念是为了检索更小的块以获得更好的搜索质量,但会为LLM添加周围的上下文,以便更好地进行推理**。有两个选择——通过获取检索到的较小块周围的几句来扩展上下文,或者将文档递归分割为许多较大的父块,其中包含较小的子块。
_documentation & tutorials 在此示意图中,文档中的每个句子都单独嵌入,这为查询与上下文之间的余弦距离搜索提供了很高的精度。为了在获取最相关的单个句子后更好地对所提供的上下文进行推理,我们将上下文窗口扩展到该句子前后各k句,然后将此扩展后的上下文发送给LLM。
绿色部分是在索引中进行搜索时找到的句子嵌入,而整个黑色+绿色段落则被馈送到LLM,以在推理提供的查询时扩大其上下文
2.4.2 _documentation & tutorials (又名_documentation & tutorials)
这个想法与Sentence Window Retriever非常相似——搜索更细粒度的信息片段,然后扩展上下文窗口,再将该上下文馈送到LLM进行推理。文档被分割成较小的子块,引用较大的父块。
文档被分割成一个块层次结构,然后最小的叶子块被发送到索引。在检索时,我们首先获取k个叶子块,如果在前k个检索到的块中有超过n个块与同一个父节点(较大的块)相关联,我们就将馈送到LLM的上下文替换为该父节点——就像自动将几个检索到的块合并为一个较大的父块一样,因此得名。只需注意——搜索仅在子节点索引内执行。查看LlamaIndex中关于Recursive Retriever + Node References的教程以深入了解。
这是一个相对较旧的想法,即您可以从两个世界中获取最佳选择——基于关键词的老式搜索——稀疏检索算法,如tf-idf或搜索行业标准BM25——以及现代语义或向量搜索,并将它们组合到一个检索结果中。 唯一的技巧是正确组合具有不同相似性分数的检索结果——这个问题通常使用Reciprocal Rank Fusion算法来解决,对最终输出的检索结果进行重新排序。
在LangChain中,这在Ensemble Retriever类中实现,它组合了您定义的一系列检索器,例如faiss向量索引和基于BM25的检索器,并使用RRF进行重新排序。
在LlamaIndex中,做法也非常相似。
混合或融合搜索通常会提供更好的检索结果,因为它结合了两种互补的搜索算法,同时考虑了查询与存储文档之间的语义相似性和关键词匹配。
因此,我们使用上述任一算法获得了检索结果,现在是时候通过过滤、重新排序或其他转换来对它们进行改进了。在LlamaIndex中,有各种可用的_documentation & tutorials,基于相似度分数、关键词、元数据过滤结果或使用其他模型重新排序,如LLM、 句子-transformer交叉编码器、Cohere重新排序端点 或基于元数据如日期等最新信息——基本上,您能想到的所有情况都可以。
这是在将检索到的上下文馈送到LLM以获取最终答案之前的最后一步。
现在是时候讨论更精细的RAG技术了,比如查询转换和路由,它们都涉及LLM,因此代表了主动行为——在我们的RAG管道中涉及LLM推理的一些复杂逻辑。
**查询转换是一系列使用LLM作为推理引擎来修改用户输入从而提高检索质量的技术。**有不同的选择来实现这一点。
查询转换原理示意图
**如果查询很复杂,LLM可以将其分解为多个子查询。**例如,如果你问: _-- "Langchain和LlamaIndex在Github上哪个有更多星星?" _而我们不太可能在我们的语料库中找到一个直接的比较,因此将这个问题分解为两个更简单、更具体的子查询来进行信息检索是有意义的: _-- "Langchain在Github上有多少星星?" -- "Llamaindex在Github上有多少星星?" _它们将并行执行,然后检索到的上下文将在单个提示中组合,供LLM综合最终回答初始查询。Langchain和LlamaIndex都实现了这一功能——分别是Langchain中的Multi Query Retriever和LlamaIndex中的Sub Question Query Engine。
_documentation & tutorials使用LLM生成一个更一般的查询,对其进行检索我们将获得更一般或高级的上下文,有助于为原始查询的答案提供依据。对原始查询的检索也会执行,最后这两种上下文都将被馈送到LLM以生成最终答案。这里是LangChain中的实现。
查询重写使用LLM来重新表述初始查询,以提高检索质量。Langchain和LlamaIndex都有实现,尽管略有不同,我发现LlamaIndex在这方面的解决方案更有力。
这一点没有编号,因为它更多是一种工具,而不是一种检索改进技术,尽管它是一种非常重要的技术。 如果我们使用了多个来源来生成一个答案,无论是由于初始查询的复杂性(我们不得不执行多个子查询,然后在一个答案中组合检索到的上下文),还是因为我们在不同的文档中为单个查询找到了相关的上下文,那么如何准确地回引我们的来源就成了一个问题。
有几种方法可以做到这一点:
将这个引用任务插入到我们的提示中,并要求LLM提及所使用的来源id。
将生成的响应的部分与我们索引中的原始文本块进行匹配——对于这种情况,LlamaIndex提供了一个基于模糊匹配的高效解决方案。如果您之前没有听说过模糊匹配,这确实是一种 非常强大的字符串匹配技术。
下一个关于构建一个不仅能为单个查询工作一次的良好RAG系统的大问题是对话逻辑,顾及对话上下文,就像在LLM时代之前的经典聊天机器人中一样。这是为了支持后续问题、同指、或与之前对话上下文相关的任意用户命令。它通过查询压缩技术解决,该技术会考虑聊天上下文。
像往常一样,对于上下文压缩也有几种方法—— 一种流行且相对简单的方法是ContextChatEngine,首先检索与用户查询相关的上下文,然后将其与来自_memory_缓冲区的聊天历史记录一起发送给LLM,以使LLM在生成下一个答案时知道之前的上下文。
一种稍微复杂一些的情况是CondensePlusContextMode——在每次交互中,聊天历史记录和最后一条消息都会被压缩成一个新的查询,然后该查询被发送到索引,检索到的上下文与原始用户消息一起传递给LLM以生成答案。
需要注意的是,LlamaIndex中也支持基于OpenAI Agents的Chat Engine,提供了一种更灵活的聊天模式,而Langchain也支持 OpenAI 功能性API。
对不同Chat Engine类型和原理的示意
还有其他类型的Chat Engine,如ReAct Agent,但让我们跳到第7节的Agents本身。
查询路由是LLM驱动的决策过程,根据用户查询决定下一步的操作——选项通常是对某些数据索引进行摘要、搜索或尝试多种不同的路线,然后将它们的输出综合到单个答案中。
查询路由器也用于选择要将用户查询发送到哪个索引或更广义地说是数据存储——无论您是有多个数据源(例如,经典的向量存储和图数据库或关系数据库)、还是有一个索引层次结构(对于多文档存储,一个经典案例就是摘要索引和文档块向量索引)。
定义查询路由器包括设置它可以做出的选择。 使用LLM调用来选择路由选项,并以预定义的格式返回其结果,用于将查询路由到给定的索引,或者,如果我们讨论的是主动行为,则路由到子链或甚至其他代理,如下面的多文档代理方案所示。
LlamaIndex和LangChain都支持查询路由器。
代理(同时受到Langchain和LlamaIndex支持)自从第一个LLM API发布以来就一直存在——其思想是为能够推理的LLM提供一组工具和要完成的任务。这些工具可能包括一些确定性函数,如任何代码函数或外部API,甚至其他代理——这种LLM链接理念就是LangChain名称的来源。
代理本身就是一个巨大的话题,在RAG概述中不可能对其进行足够深入的探讨,因此我将继续讨论基于代理的多文档检索案例,并简要介绍一下OpenAI助手站,因为这是一个相对较新的事物,在最近的 OpenAI 开发者大会上作为 GPTs 被展示,并在下文描述的 RAG 系统的背后运作。
(https://platform.openai.com/docs/assistants/overview)基本上实现了围绕 LLM 需要的许多工具,我们之前在开源中已经拥有了——聊天历史、知识存储、文档上传界面,或许最重要的是,(https://platform.openai.com/docs/assistants/tools/function-calling)。后者提供了将自然语言转换为对外部工具或数据库查询的 API 调用的功能。
在 LlamaIndex 中,有一个 OpenAIAgent 类(https://docs.llamaindex.ai/en/stable/examples/agent/openai_agent.html) ,将这种高级逻辑与 ChatEngine 和 QueryEngine 类结合起来,提供基于知识和上下文感知的聊天以及在一次对话中调用多个 OpenAI 函数的能力,这真正带来了智能代理行为。
让我们看一下 (https://docs.llamaindex.ai/en/stable/examples/agent/multi_document_agents.html) 的示意图——一个相当复杂的设置,涉及初始化一个代理(OpenAIAgent)在每个文档上,能够进行文档摘要和经典的 QA 机制,以及一个顶级代理,负责将查询路由到文档代理和最终答案合成。
每个文档代理拥有两个工具——一个向量存储索引和一个摘要索引,根据路由的查询决定使用哪一个。而对于顶级代理,所有文档代理都尊重地作为工具。
这个方案展示了一个高级的 RAG 架构,每个涉及的代理都进行了很多路由决策。这种方法的好处是能够比较不同的解决方案或实体,这些解决方案或实体在不同的文档及其摘要中被描述,以及经典的单文档摘要和 QA 机制——这基本上涵盖了最常见的与文档集合交谈的用例。
一个展示多文档代理的示意图,涉及查询路由和代理行为模式。
这种复杂方案的缺点可以从图片中猜测到——由于与代理内部的 LLM 进行多次来回迭代,它有点慢。以防万一,LLM 调用总是 RAG 管道中最长的操作——搜索通过设计优化了速度。所以对于一个大型多文档存储,我建议考虑一些简化这个方案以使其可扩展的方法。
这是任何 RAG 管道的最后一步——根据我们仔细检索的所有上下文和初始用户查询生成答案。最简单的方法可能只是将所有提取的上下文(超过某个相关性阈值)与查询一起连接起来,一次性输入到 LLM 中。但是,通常还有其他更复杂的选项,涉及多次 LLM 调用以优化检索的上下文并生成更好的答案。
**响应合成的主要方法有:
**1. 通过将检索到的上下文逐块发送给 LLM 来迭代完善答案
**2. 总结检索到的上下文以适应提示
3. 基于不同的上下文块生成多个答案,然后将它们连接或总结它们。有关更多详情,请查阅 响应合成器模块文档] (https://docs.llamaindex.ai/en/stable/module_guides/querying/response_synthesizers/root.html)。。
这种方法涉及微调我们 RAG 管道中涉及的两个 DL 模型中的某一个——要么是负责嵌入质量和因此上下文检索质量的 Transformer** 编码器,要么是一个LLM,负责最佳利用提供的上下文来回答用户查询——幸运的是,后者是一个很好的少数样本学习器。如今,拥有像 GPT-4 这样的高端 LLM 来生成高质量的合成数据集是一个巨大的优势。但是,您应该始终意识到,使用专业研究团队在仔细收集、清洁和验证的大型数据集上训练的开源模型,并使用小型合成数据集进行快速调整,可能会一般地缩小模型的能力。
我对编码器微调的方法也有些怀疑,因为最新的 Transformer 编码器针对搜索进行了优化,已经相当高效。因此,我在 LlamaIndex 笔记本 设置中测试了 bge-large-en-v1.5 (在撰写时位于 MTEB 排行榜 前四)的性能提升,结果显示检索质量提升了 2%。虽然不是很戏剧性,但是了解这一选择特别是当您为 RAG 构建特定领域数据集时是很好的。
另一个老方法是使用交叉编码器来重新排名您检索的结果,如果您不完全信任您的基础编码器。其工作方式如下——您将查询和每个排名前 k 的检索文本块传递给交叉编码器,由 SEP 令牌分隔,并对其进行微调,以对相关块输出 1,对不相关块输出 0。一个很好的调整过程示例可以在这里
(https://docs.llamaindex.ai/en/latest/examples/finetuning/cross_encoder_finetuning/cross_encoder_finetuning.html#)找到,结果显示交叉编码器微调提高了 4% 的配对得分。
最近 OpenAI 开始提供 LLM 微调 API,而 LlamaIndex 有一个关于 在 RAG 设置中微调 GPT-3.5-turbo 的教程,以“提取”一些 GPT-4 的知识。这里的想法是,取一个文档,用 GPT-3.5-turbo 生成一系列问题,然后用 GPT-4 根据文档内容生成这些问题的答案(构建一个以 GPT-4 为动力的 RAG 管道),然后对 GPT-3.5-turbo 进行微调,使用这组问题-答案对数据集。ragas 框架用于评估 RAG 管道,显示 微调后的 GPT 3.5-turbo 模型比原始模型更好地利用提供的上下文 来生成其答案,信度指标提高了 5%。
一种更复杂的方法在最近的论文 RA-DIT: 检索增强双指导调整 中得到展示,由 Meta AI 研究提出,建议一种同时调整 LLM 和检索器的技术(原论文中的双编码器)针对查询、上下文和答案的三元组。关于实现细节,请参阅这个指南。这种技术既用于通过微调 API 微调 OpenAI LLM,也用于开源模型 Llama2(在原论文中),在知识密集任务指标中带来了约 5% 的提升(与 RAG 的 Llama2 65B 相比),并在常识推理任务中带来了几个百分点的提升。
如果您了解 RAG 的更好的 LLM 微调方法,请在评论部分分享您的专业知识,特别是如果它们适用于较小的开源 LLM。
有几个框架用于评估 RAG 系统的性能,共享的想法是有几个独立的指标,如总体答案相关性、答案可靠性、信度和检索的上下文相关性。
ragas 在前一部分中提到,使用信度和答案相关性作为生成答案质量指标,以及 RAG 方案的检索部分的经典上下文精确度和召回率。
在最近发布的精彩短期课程构建和评估高级 RAG中,由 Andrew NG、LlamaIndex 和评估框架 Truelens 提出了 RAG 三元组——检索的上下文相关性与查询的关系、基础性(LLM 答案有多少是由提供的上下文支持的)和答案相关性与查询的关系。
检索的上下文相关性是最关键且最可控的指标——基本上从上文描述的高级 RAG 管道的第 1-7 部分加上编码器和排名器的微调部分就是为了改善这一指标,而第 8 部分和 LLM 的微调则集中在答案相关性和基础性上。
一个关于相当简单的检索器评估管道的好例子可以在这里找到,并已应用于编码器微调部分。一种更高级的方法不仅考虑了命中率,还考虑了平均倒数排名,一种常见的搜索引擎指标,以及生成答案的信度和相关性等指标,在 OpenAI cookbook中展示了这种方法。
LangChain 拥有一个相当高级的评估框架 LangSmith,可以实现自定义评估器,同时还监控您的 RAG 管道中运行的迹象,以使您的系统更透明。
如果您使用 LlamaIndex 构建,那么有一个 rag_evaluator llama pack,提供一个快速工具来评估您的管道与公共数据集。
我试图概述 RAG 的核心算法方法,并展示其中的一些,希望这可以激发一些在您的 RAG 管道中尝试的新颖思想,或为今年发明的众多技术带来一些系统性——对我来说,2023年是迄今为止最激动人心的一年。
还有许多其他需要考虑的事项,例如基于网络搜索的 RAG (如 LlamaIndex 的 RAGs、webLangChain 等),深入探讨代理架构**(以及最近OpenAI 对这个游戏的投注)和一些关于_文档与教程的想法。
除了答案相关性和信度外,RAG 系统的主要生产挑战是速度,尤其是如果您进入更灵活的基于代理的方案,但这是另一篇文章的内容。ChatGPT 和大多数其他助手使用的这种流式功能并不是随机的赛博朋克风格,而仅仅是为了缩短感知到的答案生成时间。这就是为什么我看到较小的 LLM 和 Mixtral 与 Phi-2 的最近发布将引领我们走向这个方向的一个非常光明的未来。
53AI,企业落地应用大模型首选服务商
产品:大模型应用平台+智能体定制开发+落地咨询服务
承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-03-30
2024-04-26
2024-05-10
2024-04-12
2024-05-28
2024-05-14
2024-04-25
2024-07-18
2024-04-26
2024-08-13
2024-12-22
2024-12-21
2024-12-21
2024-12-21
2024-12-21
2024-12-20
2024-12-20
2024-12-19