查看原文
其他

图解 RAG 高级算法和技术

程序员叶同学 HelloTech技术派 2024-03-16

RAG 概念

Retrieval Augmented Generation,又名 RAG,从某些数据源检索到的信息提供给 LLMs ,以作为其生成答案的上下文信息。基本上,RAG 是搜索 + LLM 提示,简单来说就是,要求模型回答用户问题,先以搜索算法检索与问题相关的信息作为上下文信息,将查询和检索到的上下文信息注入到 Prompt 中,然后发送到给 LLM 。

RAG 是 2023 年基于 LLM 的系统中最流行的架构。以下分别对 RAG 算法和技术进行概述和解释。

Naive RAG

将文本拆分为块,然后 Transformer Encoder 模型将这些块 Embedding 到向量中,将所有这些向量放入索引中,最后创建一个提示 LLM,告诉模型根据我们在搜索步骤中找到的上下文回答用户的查询。

在运行时中,我们使用相同的 Encoder 模型对用户的查询进行向量化,然后根据索引执行此查询向量的搜索,找到前 k 个结果,从我们的数据库中检索相应的文本块,并将它们作为上下文输入到提示中LLM。

RAG Prompt 示例:

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.
"""

Advanced RAG

1、分块和向量化

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

1.1、分块

Transformer 模型具有固定的输入序列长度,即使输入上下文窗口很大,一个句子或几个句子的向量也比在几页文本上取平均值的向量更能表示它们的语义含义,因此对数据进行分块 — 将初始文档拆分为一定大小的块,而不会失去其含义(将文本拆分为句子或段落, 不要将一个句子分成两部分)。有各种文本拆分器实现能够完成此任务。

块的大小是一个需要考虑的参数——它取决于我们使用的嵌入模型及其在令牌中的容量,标准转换器编码器模型,如基于 BERT 的句子转换器,最多需要 512 个令牌,OpenAI ada-002 能够处理更长的序列,如 8191 个令牌,但这里的折衷是足够的上下文LLM对与足够具体的文本嵌入进行推理,以便有效地执行搜索。

1.2、向量化

下一步是选择一个模型来 embeddings 我们的块 - 有很多选择,我选择搜索优化的模型,如 bge-large 或 E5 embeddings 系列 - 只需查看 MTEB 排行榜以获取最新更新。

2、搜索索引

2.1、向量存储索引

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

一个适当的搜索索引,针对 10000+ 个元素尺度的高效检索进行了优化,是一个向量索引,如 faiss、nmslib 或 annoy,使用一些近似最近邻实现,如 clustring、trees 或 HNSW 算法。

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

2.2、分层索引

如果我们有许多文档要检索,需要能够有效地在其中进行搜索,找到相关信息并将其综合为一个答案,并引用来源。对于大型数据库,一个有效的方法是创建两个索引 - 一个由摘要组成,另一个由文档块组成,并分两步进行搜索,首先通过摘要过滤掉相关文档,然后仅在这个相关组中搜索。

2.3、假设问题和 HyDE

另一种方法是要求为 LLM 每个块生成一个问题,并将这些问题 embed 到向量中,在运行时对这个问题向量的索引执行查询搜索(在我们的索引中用问题向量替换块向量),然后在检索后路由到原始文本块并将它们作为上下文 LLM 发送以获得答案。

这种方法提高了搜索质量,因为与实际块相比,查询和假设问题之间的语义相似性更高(即 Q2Q)。

还有一种称为 HyDE 的反向逻辑方法 - 即要求 LLM 生成给定查询的假设响应,然后将其向量与查询向量一起使用以提高搜索质量。

2.4、上下文扩充

这里的概念是检索较小的块以获得更好的搜索质量,但将周围的上下文相加以LLM进行推理。

有两种选择:通过围绕较小的检索到的块的句子扩展上下文,或者以递归方式将文档拆分为多个较大的父块,其中包含较小的子块。

2.4.1、句子窗口检索

在此方案中,文档中的每个句子都是单独嵌入的,这为上下文余弦距离搜索提供了极大的查询精度。

为了在获取最相关的单个句子后更好地推理找到的上下文,我们将上下文窗口扩展了检索到的句子之前和之后的 k 个句子,然后将此扩展上下文发送到 LLM。

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

2.4.2、父文档检索器

这里的想法与句子窗口检索器非常相似——搜索更细粒度的信息片段,然后在将所述上下文提供给LLM推理之前扩展上下文窗口。文档被拆分为较小的子块,引用较大的父块。

首先在检索过程中获取较小的块,然后如果前 k 个检索到的块中有 n 个以上的块链接到同一个父节点(较大的块),我们将替换该父节点提供给 的LLM上下文——就像自动将一些检索到的块合并为一个更大的父块,因此得名方法。请注意,搜索仅在子节点索引中执行。

2.5、融合检索或混合搜索

一个相对传统的想法,你可以从两个部分基于关键字的老式搜索和现代语义或向量搜索中获取最好的东西,比如 tf-idf 或搜索行业标准 BM25,以及现代语义或向量搜索,并将其组合在一个检索结果中。

这里唯一的诀窍是将检索到的结果与不同的相似性分数正确地组合在一起——这个问题通常在 Reciprocal Rank Fusion 的帮助下解决,对检索到的结果进行重新排序以获得最终输出。

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

3、重新排名和过滤

因此,我们使用上述任何算法获得了检索结果,现在是时候通过过滤、重新排名或某种转换来优化它们了。如:根据相似性分数、关键字、元数据过滤掉结果,或使用其他模型或者基于元数据等方法。

这是将检索到的上下文LLM提供给以获得结果答案之前的最后一步。

4、查询转换

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

如果查询很复杂,LLM可以将其分解为多个子查询。对于考试,如果你问:

在 Github 上哪个框架 Langchain 或 LlamaIndex 上有更多的星?

而且我们不太可能在语料库中的某些文本中找到直接的比较,因此将这个问题分解为两个子查询是有意义的,前提是更简单、更具体的信息检索:

_ “Langchain 在 Github 上有多少颗星?”_

_ “Llamaindex 在 Github 上有多少颗星?”_

它们将并行执行,然后检索到的上下文将合并到一个提示中,以 LLM 合成初始查询的最终答案。这两个库都实现了这个功能——作为 Langchain 中的多查询检索器和 Llamaindex 中的子问题查询引擎。

  • Step-back prompting 使用 LLM 生成更一般的查询,并检索该查询,我们获得更一般或高级的上下文,可用于为原始查询提供答案。还执行原始查询的检索,并且在最终答案生成步骤中将两个上下文馈送到 LLM。
  • 查询重写使用 LLM 重新制定初始查询以改进检索。

5、聊天引擎

构建一个可以针对单个查询多次工作的优秀 RAG 系统的下一个重要事项是聊天逻辑,考虑到对话上下文,与前文中的经典聊天机器人相同LLM时代。

这需要支持与先前对话上下文相关的后续问题、照应或任意用户命令。它是通过查询压缩技术解决的,将聊天上下文与用户查询一起考虑在内。

一个流行且相对简单的 ContextChatEngine,首先检索与用户查询相关的上下文,然后将其与内存缓冲区中的聊天记录一起发送到 LLM,以便 LLM 了解之前的上下文,同时生成下一个答案。

更复杂的情况是 CondensePlusContextMode — 在每次交互中,聊天历史记录和最后一条消息都会压缩为一个新查询,然后该查询转到索引,检索到的上下文连同用于生成答案的原始用户消息。

6、查询路由

查询路由是由 LLM 驱动的决策步骤,根据给定的用户查询下一步该做什么——这些选项通常是总结、针对某些数据索引执行搜索或尝试多种不同的路由然后将他们的输出综合为一个答案。

查询路由器还用于选择索引或更广泛的数据存储,将用户查询发送到何处 - 要么你有多个数据源,例如经典向量存储和图形数据库或关系数据库,要么你有索引的层次结构 - 对于多文档存储,一个非常经典的情况是摘要索引和文档块向量的另一个索引。

路由选项的选择是通过 LLM 调用执行的,以预定义的格式返回其结果,用于将查询路由到给定的索引,或者,如果我们采用父系行为,则路由到 sub -链甚至其他 Agent,如下面的多文档 Agent 方案所示。

7、Agents in RAG

Agent 几乎自第一个 LLM API 发布以来就已经存在了 - 其想法是提供一个能够推理的 LLM,具有一组工具和要完成的任务。这些工具可能包括一些确定性函数,例如任何代码函数或外部 API 甚至其他 Agent。

让我们看一下多文档 Agent 方案——一个非常复杂的设置,涉及在每个文档上初始化一个Agent,能够进行文档摘要和经典的 QA 机制,以及一个顶级 Agent,负责将查询路由到文档 Agent 和最终答案合成。

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

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

从图片中可以猜到这种复杂方案的缺点 - 由于与我们的 LLMs 内部进行多次来回迭代,它有点慢。为了以防万一,LLM 调用始终是 RAG pipeline 中最长的操作 - 搜索在设计上针对速度进行了优化。因此,对于大型多文档存储,建议考虑对这个方案进行一些简化,使其可扩展。

8、响应合成器

这是任何 RAG pipeline的最后一步 - 根据我们仔细检索的所有上下文和初始用户查询生成答案。

最简单的方法是将所有获取的上下文(高于某个相关性阈值)与查询一起连接并提供给 一次LLM。

但是,与往常一样,还有其他更复杂的选项涉及多个LLM调用,以优化检索到的上下文并生成更好的答案。

响应合成的主要方法是:

  • 通过将检索到的上下文逐块发送到 LLM 来迭代地细化答案
  • 总结检索到的上下文以适应提示
  • 根据不同的上下文块生成多个答案,然后将它们连接或总结。

总结

试图描述 RAG 的核心算法方法,并举例说明其中的一些方法,RAG 系统的主要生产挑战是速度,希望这些能激发一些新颖的想法,以便在实际的 RAG pipeline 中进行尝试。

推荐阅读

参考资料:

[MTEB Leaderboard - a Hugging Face Space by mteb](https://huggingface.co/spaces/mteb/leaderboard)

[HyDE](https://boston.lti.cs.cmu.edu/luyug/HyDE/HyDE.pdf)

[Reciprocal Rank Fusion](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf)

[Step-back prompting](https://arxiv.org/pdf/2310.06117.pdf?ref=blog.langchain.dev)

[ContextChatEngine](https://docs.llamaindex.ai/en/stable/examples/chat_engine/chat_engine_context.html)

[CondensePlusContextMode](https://docs.llamaindex.ai/en/stable/examples/chat_engine/chat_engine_condense_plus_context.html)

[Response synthesizer module docs](https://docs.llamaindex.ai/en/stable/module_guides/querying/response_synthesizers/root.html)

[Advanced RAG Techniques: an Illustrated Overview](https://pub.towardsai.net/advanced-rag-techniques-an-illustrated-overview-04d193d8fec6)

🧐分享、点赞、在看,给个3连击呗!👇

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存