RAG 全称 Retrieval-Augmented Generation,翻译成中文是检索增强生成。检索指的是检索外部知识库,增强生成指的是将检索到的知识送给大语言模型以此来优化大模型的生成结果,使得大模型在生成更精确、更贴合上下文答案的同时,也能有效减少产生误导性信息的可能。
为什么需要RAG?
之所以需要 RAG,是因为大语言模型本身存在一些局限性。
1.时效性 模型的训练是基于截至某一时间点之前的数据集完成的。这意味着在该时间点之后发生的任何事件、新发现、新趋势或数据更新都不会反映在模型的知识中。例如,我的训练数据在 2023 年底截止,之后发生的事情我都无法了解。另外,大型模型的训练涉及巨大的计算资源和时间。这导致频繁更新模型以包括最新信息是不现实的,尤其是在资源有限的情况下。
2.覆盖性 虽然大模型的训练数据集非常庞大,但仍可能无法涵盖所有领域的知识或特定领域的深度信息。例如,某些专业的医学、法律或技术问题可能只在特定的文献中被详细讨论,而这些文献可能未被包括在模型的训练数据中。另外,对于一些私有数据集,也是没有被包含在训练数据中的。当我们问的问题的答案没有包含在大模型的训练数据集中时,这时候大模型在回答问题时便会出现幻觉,答案也就缺乏可信度。
由于以上的一些局限性,大模型可能会生成虚假信息。为了解决这个问题,需要给大模型外挂一个知识库,这样大模型在回答问题时便可以参考外挂知识库中的知识,也就是 RAG 要做的事情。
RAG 的流程
RAG 的中文名称是检索增强生成,从字面意思来理解,包含三个检索、增强和生成三个过程。
检索:根据用户的查询内容,从外挂知识库获取相关信息。简单来说,就是将用户的查询通过嵌入模型转换成向量,以便与向量数据库中存储的知识相关的向量进行比对。通过相似性搜索,从向量数据库中找出最匹配的前 K 个数据。 增强:将用户的查询内容和检索到的相关知识一起嵌入到一个预设的提示词模板中发生给大模型。 生成:将经过检索增强的提示词内容输入到大语言模型(LLM)中,以此生成所需的输出。
流程图如下所示:
下面是可参考的RAG提示词模版:
## Known Information
{contents}
## Answer Requirements
- Answers must be based on the above known information and ensure accuracy and relevance.
- If the known information does not contain the content needed to answer the question or you cannot answer, please reply: {emptyDesc}.
- Answer in the same language as the question.
- Answers must be returned using Markdown syntax, including but not limited to titles, lists, bold, italics, etc.
- Any images, links, and script languages in the known information should be returned directly.
Question: {userMessage}
Please provide an accurate, comprehensive, and format-optimized answer based on the above requirements.
可以注意到模板中有三个占位符:
{contents}: 从向量数据库中查到的相关知识将被替换到这里
{emptyDesc}:这是一个预设的空回复,如果向量数据库里查询到的内容均不足以支撑大模型回答用户问题,根据上面提示词模板的要求,大模型将会固定回复此条内容
{userMessage}:这是用户的问题,用户提问的问题将会被替换到这里
我们将上面的提示词模板翻译成中文,你就明白了:
## 已知信息
{contents}
## 回答要求
- 回答必须基于上述已知信息,并确保准确性和相关性。
- 如果已知信息中不包含回答问题所需的内容或无法回答,请回复:{emptyDesc}。
- 用与问题相同的语言回答。
- 回答必须使用Markdown语法返回,包括但不限于标题、列表、粗体、斜体等。
- 已知信息中的任何图像、链接和脚本语言应直接返回。
问题: {userMessage}
请根据上述要求提供一个准确、全面且格式优化的回答。
说白了就是把已知信息和问题一起发给大模型,大模型负责根据已知信息,润色、生成一个更方便用户理解的答案
RAG技术分类
基础的 RAG 技术
RAG 系统的起点一般是一个文本文档的语料库,简单看起来是这样的: 把文本分割成块,然后把这些分块嵌入到向量与transformer编码器模型,把所有这些向量建立索引,最后创建一个 LLM 提示语,告诉模型回答用户的查询,给出在搜索步骤中找到的上下文。在运行时,我们用相同的编码器模型完成用户查询的向量化,然后执行这个查询向量的索引搜索,找到top-k 的结果,从数据库中检索到相应的文本块,并提供给 LLM 提示语Prompt作为上下文。
高级RAG技术
尽管并不是所有RAG系统中的高级技术都可以轻松地在一张图中可视化,但给出一个描述核心步骤和算法的方案还是有意义的。
分块和矢量化
首先,要创建一个向量索引表示我们的文档内容,然后在运行时搜索所有这些向量和查询向量之间最小距离对应的最接近语义。
由于transformer模型有固定的输入序列长度,即使输入上下文的窗口很大,一个或几个句子的向量也比一个在几页文本上取平均值的向量更能代表它们的语义意义 ,所以数据分块是一个有意义的技术。把初始文档分成一定大小的块,同时又不失去它们的意义,也就是把文本分成句子或段落,而不是把一个句子分成两部分。而且,已经有了各种能够执行此任务的文本分割器实现。例如,在 LlamaIndex 中,NodeParser 就提供了一些高级选项,如定义自己的文本分割器、元数据、节点/块关系等。
数据块的大小是一个需要考虑的参数,它取决于使用的嵌入模型及其token容量,标准的transformer编码模型,如BERT 的句子转换器,最多只能使用512个token,OpenAI ada-002能够处理更长的序列,如8191个token,但这里的折衷是足够的上下文,让 LLM 能够推理以及特定的足够文本嵌入,以便有效地执行搜索。
下一步是选择一个模型来生产所选块的嵌入,同样有很多方法,例如搜索优化的模型( bge-large 或者E5 系列),MTEB 排行榜可以得到最新的一些方法信息。关于文档分块和向量化步骤的端到端实现,可以具体地参考 https://docs.llamaindex.ai/en/latest/moduleguides/loading/ingestionpipeline/root.html。
搜索的索引
面向RAG的大模型应用的关键部分是用于搜索的索引,它存储前面得到的向量化内容。当然,查询总是首先向量化,对于 top k 分块也是一样的。最简单的实现使用一个平铺的索引,在查询向量和所有块向量之间进行距离计算并遍历。
一个合适的搜索索引,为了在一万多个元素的尺度上有效地检索而优化,需要一个向量索引, faiss,nmslib 或 annoy等使用一些近似最近邻方式实现,如聚类,树或 HNSW 算法。还有一些受管理的解决方案,比如 ElasticSearch以及向量数据库,它们负责处理数据摄取的流水线。
根据索引的选择,数据和搜索需求还可以将元数据与向量一起存储,然后使用元数据过滤器在某些日期或数据源中搜索信息。LlamaIndex 支持许多向量存储索引,也支持其他更简单的索引实现,如列表索引、树索引和关键字表索引。
如果有许多文档,就需要能够有效地在其中进行搜索,找到相关信息,并将其聚合在一个带有源引用的答案中。对于大型数据库,一个有效的方法是创建两个索引,一个由摘要组成,另一个由文档块组成,然后分两个步骤进行搜索,首先通过摘要过滤掉相关文档,然后再通过相关组进行搜索。
另一种方法是要求 LLM 为每个块生成一个问题,并将这些问题嵌入到向量中,在运行时对这个问题的向量索引执行查询搜索(在索引中用问题向量替换块向量) ,然后路由到原始文本块并将它们作为 LLM 获得答案的上下文发送。这种方法提高了搜索质量,因为与实际块相比,查询和假设问题之间具有更高的语义相似性。还有一种被称为 HyDE 的反向逻辑方法, 要求一个 LLM 生成一个假设的给定查询的响应,然后使用它的向量和查询向量来提高搜索质量。
为了获得更好的搜索质量而检索更小的块,就要为 LLM 添加周围的上下文。有两种选择,一个是句子窗口检索,即在检索到的较小块周围按句子展开上下文,另一个是父文档检索,即递归地将文档分割成若干较大的父块,其中包含较小的子块。
在句子窗口检索方案中,文档中的每个句子都是单独嵌入,这为上下文余弦距离搜索提供了很高的准确性。在获取最相关的单个句子之后,为了更好地推理找到的上下文,在检索到的句子之前和之后将上下文窗口扩展为k个句子,然后将这个扩展的上下文发送给 LLM。
父文档检索与句子窗口检索非常相似,都是搜索更细粒度的信息,然后在将上下文提供给 LLM 进行推理之前扩展过的上下文窗口。文档被拆分成引用较大父块中的较小子块。具体而言,文档被分割成块的层次结构,然后最小的叶子块被发送到索引。在检索期间,获取较小的块,然后如果在top-k 检索的块中有超过 n 个块链接到同一个父节点(较大的块) ,就用这个父节点替换提供给 LLM 的上下文。需要注意的是,搜索仅在子节点索引中执行。
还有一个相对较老的思路,可以像 tf-idf 或BM25这样的稀疏检索算法那样从现代语义或向量搜索中获取最佳结果,并将其结合在一个检索结果中。这里唯一的技巧是将检索到的结果与不同的相似度得分恰当地结合起来,这个问题通常借助于Reciprocal Rank 融合算法(RRF)来解决,对检索到的结果重新排序以得到最终的输出。
在 LangChain中,这是在集成检索器类中实现的,例如,一个 Faiss 矢量索引和一个基于 BM25的检索器,并使用 RRF 进行重新排序。在 LlamaIndex 中,也是以一种非常类似的方式完成的。
混合或融合搜索通常在考虑查询和存储文档之间有语义相似性和关键字匹配的情况下,将两种互补的搜索算法结合起来,提供更好的检索结果。
Rerank和过滤
在得到了检索结果后,需要通过过滤来重新排序。LlamaIndex 提供了多种可用的后处理程序,根据相似度评分、关键词、元数据过滤掉结果,或者用其他模型对结果进行重新排序,比如基于句子transformer的交叉编码器、 根据元数据(比如日期最近性)内聚重新排序等等。这是将检索到的上下文提供给 LLM 以获得结果答案之前的最后一步。
query变换
查询转换是一系列使用 LLM 作为推理引擎来修改用户输入以提高检索质量的技术,有很多不同的技术选择。
如果查询很复杂,LLM 可以将其分解为几个子查询。例如,如果问“ 在Github上Langchain 或 LlamaIndex 上哪个有更多颗星?”,不太可能在语料库中找到直接的对比,将这个问题分解为两个子查询是有意义的,前提是要有更简单和更具体的信息检索,例如 “ Langchain 在 Github 上有多少颗星?”“Llamaindex 在 Github 上有多少颗星?”它们将并行执行,然后将检索到的上下文组合在一个提示语中,以便 LLM 合成对初始查询的最终答案。在 Langchain 作为多查询检索器,在 Llamaindex 作为子问题查询引擎。
后退提示(Step-back prompting)使用 LLM 生成一个更一般的查询,为此检索获得一个更一般或更高级别的上下文,以便将原始查询的答案建立在这个上下文上。此外,还将执行对原始查询的检索,并在最后的应答生成步骤中将两个上下文提供给 LLM。
LangChain 有一个参考实现 https://github.com/langchain-ai/langchain/blob/master/cookbook/stepback-qa.ipynb。query重写使用 LLM 重新制定初始查询,以提高检索效率。LangChain 和 LlamaIndex 都有实现,但 LlamaIndex 参考实现更强大 https://llamahub.ai/l/llamapacks-fusionretriever-query_rewrite。
如果使用多个来源来生成一个答案,要么是由于初始查询的复杂性,需要必须执行多个子查询,然后将检索到的上下文合并到一个答案中,要么是在多个文档中发现了单个查询的相关上下文,能够准确地反向引用。可以将这个引用任务插入到提示语中,并要求 LLM 提供所使用源的 id,然后将生成的响应部分与索引中的原始文本块匹配,Llamaindex 为这种情况提供了一种有效的基于模糊匹配的解决方案。
Query 路由
Query路由是由 LLM 驱动的决策步骤,在给定用户查询的情况下,决定接下来做什么。这些选项通常是总结、针对某些数据索引执行搜索或尝试多种不同的路由,然后在一个答案中综合它们的输出。
Query路由还可以用于选择索引,或者更广泛的数据存储,将用户查询发送到何处,例如,经典的向量存储和图形数据库或关系数据库。对于多文档存储来说,一个非常经典的情况是一个摘要索引和另一个文档块向量索引。
定义Query路由包括设置它可以做出的选择。路由选择是通过一个 LLM 调用来执行的,它以预定义的格式返回结果,用于将查询路由到给定的索引。如果采用了代理的方式,则将查询路由到子链甚至其他代理,如下面的多文档代理方案所示。LlamaIndex 和 LangChain 都支持Query路由。
RAG中的智能体Agent
智能体Agent几乎自第一个 LLM API 发布以来就一直存在,其想法是为一个能够推理的 LLM 提供一套工具以及需要完成的任务。这些工具可能包括一些确定性函数,比如任何代码函数或外部 API,甚至包括其他代理,这种 LLM 链接思想就是 LangChain 来源。
代理本身就是一个巨大的话题,OpenAI 助手基本上已经实现了很多围绕 LLM 所需的工具,也许最重要的是函数调用 API。后者提供了将自然语言转换为对外部工具或数据库查询的 API 调用的功能。在 LlamaIndex 中,有一个 OpenAIAgent 类将这种高级逻辑与 ChatEngine 和 QueryEngine 结合在一起,提供基于知识和上下文感知的聊天功能,以及一次性调用多个 OpenAI 函数的能力,这确实带来了智能代理的使用方式。
以多文档代理为例,在每个文档上会初始化一个代理(OpenAIAgent) ,能够进行文档摘要和经典的 QA 机制,以及一个顶级总代理,负责将查询路由到文档代理和最终答案合成。每个文档代理都有两个工具ーー向量存储索引和摘要索引,并根据路由查询决定使用哪个工具。该体系结构由每个相关代理做出大量的路由决策。这种方法的好处是能够比较不同的解决方案或实体,这些解决方案或实体在不同的文档及其摘要以及经典的单一文档摘要和QA 机制中进行了描述,这基本上涵盖了最常见的与文档集聊天的使用场景。
该方案由于在内部使用 LLM 进行了多次来回迭代,因此速度有点慢。为了防万一,LLM 调用通过 RAG 流水线中最长的搜索操作来优化速度。因此,对于大型多文档存储,可以对该方案进行一些简化,使其具有可伸缩性。
响应合成
响应合成是任何 RAG 流水线的最后一步,根据检索的所有上下文和初始用户查询生成一个答案。最简单的方法是将所有获取的上下文(高于某个相关性阈值)与查询一起连接并提供给 LLM。但是,还有其他更复杂的选项涉及多个 LLM 调用,以细化检索到的上下文并生成更好的答案。响应合成的主要方法有:
通过逐块向LLM发送检索到的上下文来迭代地细化答案;
总结检索到的上下文以适应提示;
根据不同的上下文块生成多个答案,然后将其连接或总结。