RAG核心算法

一、分块与向量化

首先,我们的目标是创建一个向量索引,用以代表我们文档的内容,然后在运行时寻找所有这些向量与查询向量之间的最小余弦距离,以匹配最接近的语义含义。

1、分块

由于 Transformer 模型具有固定的输入序列长度,即便输入上下文窗口很大,一个句子或几个句子的向量通常能比几页文本的平均向量更好地表达其语义含义(这也取决于模型,但一般情况下是这样)。
因此,我们需要对数据进行分块 —— 将原始文档分割成某个大小的块,同时保留它们的含义(如将文本分割成句子或段落,而不是将单个句子切分成两部分)。目前有许多能够实现这一任务的文本分割器。
**块的大小是一个需要考虑的重要参数。**它取决于你使用的嵌入模型及其在 Token 上的处理能力。例如,标准的基于 BERT 的 Transformer 编码器模型(如 Sentence Transformers)最多处理 512 个 Token,而 OpenAI 的 ada-002 能够处理更长的序列,如 8191 个 Token。这里的权衡在于为 LLM 提供足够的上下文以进行推理,同时确保文本嵌入足够具体,以便有效执行搜索。
关于块大小选择的考虑因素,你可以参考下面链接里的研究。在 LlamaIndex 中,这些问题由 NodeParser 类以及一些高级选项(如自定义文本分割器、元数据、节点 / 块关系等)来处理。
https://www.pinecone.io/learn/chunking-strategies/

2、向量化

下一步是选择一个模型来嵌入这些块。市面上有不少选择,我倾向于使用像 bge-large 或 E5 嵌入系列这样的搜索优化模型。可以查看 MTEB 排行榜以获取最新的更新信息。
排行榜:https://huggingface.co/spaces/mteb/leaderboard
要了解分块和向量化步骤的端到端实现,可以参考 LlamaIndex 中完整数据摄入流程的示例:
https://docs.llamaindex.ai/en/latest/module_guides/loading/ingestion_pipeline/root.html

二、搜索索引

1、向量存储索引


在这个架构及后续内容中,为了简化描述,我们不考虑编码器部分,直接把查询内容送入索引。显然,查询内容会首先被向量化。类似地,尽管索引是根据向量而不是具体的块来进行检索的,但最终我们还是以块的形式展现结果,因为获取这些块相对简单。
搜索索引是 RAG 流程中的核心部分,它存储了我们在前一步骤中生成的向量化内容。最基本的实现方法是使用平面索引,即对查询向量和所有文本块的向量进行直接的距离计算。
一个高效的搜索索引,专为超过 10000+ 元素的高效检索优化,会采用像 faiss、nmslib 或 annoy 这样的向量索引,它们利用了近似最近邻算法,如聚类、树结构或 HNSW 算法。
还有像 OpenSearch 或 ElasticSearch 这样的托管解决方案,以及像 Pinecone、Weaviate 或 Chroma 这样的向量数据库,它们在底层处理了第 1 步中描述的数据摄入流程。
根据你的索引选择、数据和搜索需求,你还可以将元数据与向量一起存储,然后使用元数据过滤器来搜索特定日期或来源的信息。
LlamaIndex 支持许多向量存储索引,但也支持其他更简单的索引实现,如列表索引、树索引和关键字表索引 —— 我们将在后面的融合检索部分进一步讨论。

2、层次索引


如果你需要从大量文档中检索信息,高效的内部搜索、找到相关信息并将其合成为一个带有来源引用的单一答案是必要的。在大型数据库的情况下,一种有效的方法是创建两个索引 —— 一个由摘要组成,另一个由文档块组成,然后分两步进行搜索,首先通过摘要过滤出相关文档,再在这个筛选出的相关组内进行具体搜索。

3、假设性问题和 HyDE

另一种方法是让 LLM 为每个文本块生成一个问题,并将这些问题转化为向量。在运行时,我们对这个问题向量索引进行查询搜索(将索引中的文本块向量替换为问题向量),检索后将原始文本块作为上下文发送给 LLM 以获取答案。
这种方法因为查询和假设性问题之间更高的语义相似性而提高了搜索质量,相比于直接使用实际文本块的方式效果更好。
还有一种逆向逻辑的方法称为 HyDE —— 你让 LLM 针对查询生成一个假设性回应,然后将其向量与查询向量一起用于提高搜索质量。

4、上下文丰富化

这里的概念是检索较小的文本块以提高搜索质量,但在 LLM 推理时增加周围的上下文。
有两种方式:通过在检索到的较小块周围扩展句子来增加上下文,或者递归地将文档分割成包含较小子块的较大父块。

a、句子窗口检索

在这种方案中,文档中的每个句子都被单独向量化,这极大地提高了查询到上下文余弦距离搜索的准确性。
为了在找到最相关的单个句子后更好地进行推理,我们会通过在检索到的句子前后各添加 k 个句子来扩大上下文窗口,然后将这个扩展的上下文发送给 LLM。

绿色部分表示在索引中搜索到的句子嵌入,整个黑色加绿色的段落将被送到 LLM,以在对所提供的查询进行推理时扩展其上下文。

b、自动合并检索器(也称为父文档检索器)

这里的思路与句子窗口检索器类似 —— 搜索更细粒度的信息片段,然后在将上下文提供给 LLM 进行推理之前扩展上下文窗口。文档被分割成较小的子块,这些子块又与较大的父块相关联。

文档被分割成层次化的块结构,最小的叶子块被送至索引。在检索时,我们会找出 k 个叶子块,如果存在 n 个块都指向同一父块,我们就用这个父块替换它们,并把它送给 LLM 用于生成答案。
在检索过程中,我们首先获取较小的块,然后如果在检索到的前 k 个块中有超过 n 个块与同一个父节点(较大的块)相关联,我们就用这个父节点作为上下文发送给 LLM —— 这相当于自动将几个检索到的块合并成一个较大的父块。需要注意的是,搜索仅在子节点索引内进行。有关递归检索器 + 节点引用的更深入介绍,请查看 LlamaIndex 教程:
https://docs.llamaindex.ai/en/stable/examples/retrievers/recursive_retriever_nodes.html

5、融合检索或混合搜索

这是一个结合了两种搜索方式精华的相对传统想法 —— 一方面是基于关键词的传统搜索方法,如 tf-idf 或搜索行业标准的 BM25;另一方面是现代的语义或向量搜索。
将这两种方法结合在一个检索结果中。唯一的挑战是如何恰当地结合具有不同相似度得分的检索结果 —— 这个问题通常通过使用互惠等级融合(Reciprocal Rank Fusion)算法来解决,对检索结果进行重新排列,生成最终输出。

在 LangChain 中,这一功能通过 Ensemble Retriever 类实现,它结合了你定义的多个检索器,比如一个基于 faiss 的向量索引和一个基于 BM25 的检索器,并使用 RRF 算法进行结果的重排序。
LlamaIndex 采用了类似的方法实现这一功能。
混合或融合搜索通常能提供更优的检索结果,因为它结合了两种互补的搜索算法,既考虑了查询与存储文档之间的语义相似性,又考虑了关键字匹配。

三、重新排序和过滤

使用上述任何算法得到检索结果后,现在需要通过过滤、重新排序或某些转换来对结果进行精细化处理。LlamaIndex 提供了多种后处理器,可以根据相似性得分、关键字、元数据过滤结果,或使用 LLM 等其他模型进行重新排序, 比如句子 - 变换器交叉编码器、Cohere 的重新排序端点或基于日期的最新性等元数据进行重新排序 —— 你可以想象到的几乎所有内容。
这是在将检索到的上下文提供给 LLM 以获得最终答案之前的最后一步。
现在是时候应用更高级的 RAG 技术了,如查询转换和路由,这些技术都涉及到 LLM,因此代表了我们 RAG 流程中涉及 LLM 推理的复杂逻辑。

四、查询转换

查询转换是一组技术,利用 LLM 作为推理引擎来修改用户输入,从而提高检索质量。有多种方法可以实现。

如果查询复杂,LLM 可以将其分解为几个子查询。例如,如果你问:“在 Github 上,Langchain 和 LlamaIndex 哪个有更多的星星?” 由于我们不太可能在语料库中找到直接的比较,因此将这个问题分解为两个更简单、更具体的子查询是有意义的:

  • “Langchain 在 GitHub 上有多少星星?”
  • “Llamaindex 在 GitHub 上有多少星星?”

这两个查询将并行执行,然后将检索到的上下文合并成一个提示,由 LLM 合成对初始查询的最终答案。LangChain 和 LlamaIndex 都实现了这一功能。

  • 步退提示是指使用 LLM 生成一个更广泛的查询,为我们的原始查询提供更全面或更高层次的上下文。这是 LangChain 的一种实现方式。
  • 查询重写是指使用 LLM 重新构建初始查询以改善检索。LangChain 和 LlamaIndex 都有各自的实现,但我认为在这里 LlamaIndex 的解决方案更强大。

如果我们使用了多个来源来生成答案,无论是因为初始查询的复杂性(我们需要执行多个子查询,然后将检索到的上下文合并成一个答案),还是因为我们在不同文档中找到了单个查询的相关上下文,就会出现如何准确引用我们来源的问题。
有几种方法可以做到这一点:

  • 将引用任务包含在我们的提示中,并要求 LLM 提及所使用的来源 ID。
  • 将生成的回应的部分与我们索引中的原始文本块匹配 —— LlamaIndex 提供了一个基于模糊匹配的高效解决方案。如果你还没听说过模糊匹配,这是一种非常强大的字符串匹配技术。

五、聊天引擎

在构建一个对单个查询能多次有效回应的优秀 RAG 系统方面,接下来的关键是聊天逻辑,这需要考虑到像 LLM 出现之前的传统聊天机器人那样的对话上下文。
这对于处理后续提问、语境中的指代消解,或者与之前的对话相关的任意用户命令都是必不可少的。这一挑战通过 “查询压缩技术” 解决,它在考虑用户查询的同时,也考虑了聊天的上下文。
就像通常的做法一样,有几种处理上下文压缩的方法 —— 一种受欢迎且相对简单的方法是 ContextChatEngine,它首先提取与用户查询相关的上下文,然后将这个上下文连同聊天历史一起发送给 LLM,以便在生成下一个回答时,LLM 能够考虑到之前的上下文。
更复杂的一个例子是 CondensePlusContextMode—— 在每次互动中,聊天历史和最新的消息被压缩成新的查询内容,然后这个内容被发送到索引中,检索到的上下文会与原始用户消息一起传递给 LLM,以此来生成回答。
值得一提的是,LlamaIndex 还支持基于 OpenAI Agent 的聊天引擎,提供了一种更灵活的聊天模式,而 Langchain 也支持 OpenAI 的功能性 API。

不同聊天引擎类型和原理的说明
当然,还有其他类型的聊天引擎,比如 ReAct Agent,但我们先跳过这部分,直接看看第 7 节中的 Agent 本身。

六、查询路由

查询路由是基于 LLM 的决策步骤,它根据用户的查询来决定下一步该做什么 —— 常见的选择包括概括总结、对某些数据索引进行搜索,或者尝试多种不同的路径,然后将它们的输出合并成一个答案。
查询路由器还被用来选择发送用户查询的索引或更广泛的数据存储地点 —— 你可能有多个数据来源,比如传统的向量存储、图形数据库或关系型数据库,或者你可能有一个索引的层级结构 —— 对于多文档存储来说,一个典型的案例可能是一个摘要索引和另一个文档块向量的索引。
定义查询路由器包括设定它可以做出的选择。
选择路由选项是通过 LLM 调用完成的,它以预定格式返回结果,用于将查询引导到给定的索引,或者,在我们讨论类似于 Agent 的行为时,引导到子链甚至其他 Agent,如下面的多文档 Agent 方案所示。
LlamaIndex 和 LangChain 都支持查询路由器。

七、RAG 中的 Agent

Agent(由 Langchain 和 LlamaIndex 支持)自从第一个 LLM API 推出以来几乎就存在了 —— 它的想法是为具备推理能力的 LLM 提供一套工具和一个待完成的任务。这些工具可能包括像任何代码函数那样的确定性函数、外部 API,甚至是其他智能体 —— 这种 LLM 链接的想法是 LangChain 名称的来源。
Agent 本身是一个庞大的领域,要在 RAG 的概述中深入探讨它是不可能的,所以我会继续介绍基于 Agent 的多文档检索案例,并在 OpenAI Assistants 这个相对较新的概念上稍作停留,因为它是在最近的 OpenAI 开发者大会上作为 GPT 提出的,并在下面描述的 RAG 系统的底层运作。
OpenAI Assistants 基本上实现了我们之前在开源软件中拥有的围绕 LLM 的许多工具 —— 包括聊天历史、知识存储、文档上传接口,最重要的是,它还有一个功能调用 API。这个 API 提供将自然语言转换为对外部工具或数据库查询的 API 调用的能力。
在 LlamaIndex 中,OpenAIAgent 类将这种先进的逻辑与 ChatEngine 和 QueryEngine 类结合在一起,实现了基于知识和上下文的智能聊天,以及在单次对话中调用多个 OpenAI 功能的能力,从而真正实现了智能的代理行为。
让我们来看看多文档 Agent 方案 —— 这是一个复杂而精巧的配置,它涉及在每个文档上初始化一个 Agent(OpenAIAgent),这些 Agent 不仅能进行文档摘要,还能处理常规的问答任务。同时,还有一个顶级 Agent,负责将用户查询指向不同的文档智能体,并最终合成答案。
每个文档智能体配备了两个主要工具:一个是向量存储索引,另一个是摘要索引。根据接收到的查询,它会决定使用哪一个工具。而对于顶级 Agent 而言,所有的文档 Agent 都相当于其下的工具。
这个方案展现了一个先进的 RAG 架构,其中每个参与的 Agent 都要做出许多路由决策。这种方法的优势在于,它可以比较不同文档及其摘要中描述的不同解决方案或实体,同时还包括经典的单文档摘要和问答机制。这基本上涵盖了大多数涉及文档集合的聊天场景。

说明多文档 Agent 的方案,涉及查询路由和代理行为模式。
这种复杂方案的劣势在于其处理速度。由于需要与每个 Agent 内部的 LLM 进行多次交互,整个过程可能较慢。值得一提的是,在 RAG 流程中,LLM 调用通常是最耗时的步骤,而搜索则是为了速度而优化的。因此,对于大型多文档存储系统,我建议考虑简化这一方案,以便扩展。

八、响应合成器

响应合成器是任何 RAG 流程的最后一步 —— 基于我们精心检索的所有上下文和初始用户查询来生成答案。
最简单的方法是将所有检索到的相关上下文(超过某个相关性阈值)与查询一起合并,并一次性输入到 LLM 中。但是,还有其他更复杂的选择,涉及多次调用 LLM 来精炼检索到的上下文,并生成更好的答案。
响应合成的主要方法包括:

  • 通过分块将检索到的上下文发送到 LLM,迭代精炼答案。
  • 摘要化检索到的上下文,使其适应提示。
  • 基于不同上下文块生成多个答案,然后将它们连接或摘要。

更多详情请参考:https://docs.llamaindex.ai/en/stable/module_guides/querying/response_synthesizers/root.html

  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

科技之歌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值