大模型应用架构:从零解读检索增强生成 RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是 2023 年以来最流行的基于大语言模型(LLM)的应用系统架构。许多产品几乎完全建立在 RAG 之上,涵盖了结合网络搜索引擎和 LLM 的问答服务,到成千上万个私有数据聊天的应用程序。很多人将 RAG 和 Agent(智能体)作为大模型应用的两种主流架构,但什么是 RAG?它涉及了哪些具体的技术细节?本文将深入解析。
检索增强生成(RAG)是结合大语言模型与外部知识库的核心架构。本文详解 RAG 基础流程,包括文档分块、向量化索引构建及检索生成链路。深入探讨高级技术如混合搜索、重排序、查询变换及智能体路由。同时涵盖编码器与大模型微调策略,以及基于 Ragas 等框架的性能评估方法,为构建高效、可信的 RAG 系统提供完整技术指南。

RAG(Retrieval-Augmented Generation,检索增强生成)是 2023 年以来最流行的基于大语言模型(LLM)的应用系统架构。许多产品几乎完全建立在 RAG 之上,涵盖了结合网络搜索引擎和 LLM 的问答服务,到成千上万个私有数据聊天的应用程序。很多人将 RAG 和 Agent(智能体)作为大模型应用的两种主流架构,但什么是 RAG?它涉及了哪些具体的技术细节?本文将深入解析。
RAG 即检索增强生成,其核心思想是为 LLM 提供从特定数据源检索到的信息,并基于此修正生成的答案。RAG 本质上是 Search + LLM Prompt 的结合体。通过大模型回答查询,并将搜索算法所找到的信息作为大模型的上下文注入。查询语句和检索到的上下文都会被注入到发送给 LLM 的提示语中,从而让模型能够利用外部知识生成更准确、可溯源的回答。
在向量搜索领域,嵌入式搜索引擎可以通过 Faiss 等工具实现,这成为了 RAG 的重要助力。像 Pinecone 这样的服务可以构建开源搜索索引,为输入文本增加了额外的存储空间和管理工具。关于向量数据库和嵌入技术的更多细节,可以参考相关技术文档。
面向 RAG 的开发框架主要有两个最著名的开源工具——LangChain 和 LlamaIndex。它们分别创建于 2022 年 10 月和 11 月,随着 ChatGPT 的爆发,在 2023 年获得了大量采用。这两个项目发展速度极快,提供了丰富的组件来构建基于 LLM 的流水线和应用程序。
RAG 系统的起点通常是一个文本文档的语料库。基础流程大致如下:
在运行时,我们用相同的编码器模型完成用户查询的向量化,然后执行这个查询向量的索引搜索,找到 Top-K 的结果。从数据库中检索到相应的文本块,并提供给 LLM 提示语(Prompt)作为上下文。
在 OpenAI 平台上,提示词 Prompt 可以是这样的结构:
def question_answering(context, query):
prompt = f"""
请根据以下上下文回答问题:
上下文:{context}
问题:{query}
如果上下文中没有相关信息,请说明无法回答。
"""
response = get_completion(instruction=prompt, model="gpt-3.5-turbo")
answer = response.choices[0].message["content"]
return answer
关于提示词和提示词工程的更多介绍,可以参考 OpenAI 的官方手册。尽管 OpenAI 在 LLM 市场上处于领先地位,但还有很多替代方案,比如 Anthropic 的 Claude,以及最近流行的更小但功能强大的模型,如 Mistral、微软的 Phi-2 以及许多开源选项,如 Llama2、OpenLLaMA、Falcon 等都可以用来开发面向 RAG 的大模型产品。
虽然并非所有 RAG 系统中的高级技术都可以轻松地在一张图中可视化,但描述核心步骤和算法的方案对于理解至关重要。
首先,要创建一个向量索引来表示我们的文档内容,然后在运行时搜索所有这些向量和查询向量之间最小距离对应的最接近语义。
由于 Transformer 模型有固定的输入序列长度,即使输入上下文的窗口很大,一个或几个句子的向量也比在一个几页文本上取平均值的向量更能代表它们的语义意义。因此,数据分块是一项有意义的技术。把初始文档分成一定大小的块,同时又不失去它们的意义,也就是把文本分成句子或段落,而不是把一个句子分成两部分。目前已有各种能够执行此任务的文本分割器实现。例如,在 LlamaIndex 中,NodeParser 就提供了一些高级选项,如定义自己的文本分割器、元数据、节点/块关系等。
数据块的大小是一个需要考虑的参数,它取决于使用的嵌入模型及其 Token 容量。标准的 Transformer 编码模型,如 BERT 的句子转换器,最多只能使用 512 个 Token;OpenAI ada-002 能够处理更长的序列,如 8191 个 Token。这里的折衷在于:足够的上下文让 LLM 能够推理,以及特定的足够文本嵌入以便有效地执行搜索。
下一步是选择一个模型来生产所选块的嵌入。有很多方法,例如搜索优化的模型(bge-large 或者 E5 系列),MTEB 排行榜可以得到最新的一些方法信息。关于文档分块和向量化步骤的端到端实现,可以参考 LlamaIndex 的官方文档。
面向 RAG 的大模型应用的关键部分是用于搜索的索引,它存储前面得到的向量化内容。当然,查询总是首先向量化,对于 Top-K 分块也是一样的。最简单的实现使用一个平铺的索引,在查询向量和所有块向量之间进行距离计算并遍历。
一个合适的搜索索引,为了在一万多个元素的尺度上有效地检索而优化,需要一个向量索引。Faiss、NMSLIB 或 Annoy 等使用一些近似最近邻方式实现,如聚类、树或 HNSW 算法。还有一些受管理的解决方案,比如 Elasticsearch 以及向量数据库,它们负责处理数据摄取的流水线。
根据索引的选择,数据和搜索需求还可以将元数据与向量一起存储,然后使用元数据过滤器在某些日期或数据源中搜索信息。LlamaIndex 支持许多向量存储索引,也支持其他更简单的索引实现,如列表索引、树索引和关键字表索引。
如果有许多文档,就需要能够有效地在其中进行搜索,找到相关信息,并将其聚合在一个带有源引用的答案中。对于大型数据库,一个有效的方法是创建两个索引,一个由摘要组成,另一个由文档块组成,然后分两个步骤进行搜索:首先通过摘要过滤掉相关文档,然后再通过相关组进行搜索。
另一种方法是要求 LLM 为每个块生成一个问题,并将这些问题嵌入到向量中。在运行时对这个问题的向量索引执行查询搜索(在索引中用问题向量替换块向量),然后路由到原始文本块并将它们作为 LLM 获得答案的上下文发送。这种方法提高了搜索质量,因为与实际块相比,查询和假设问题之间具有更高的语义相似性。还有一种被称为 HyDE(Hypothetical Document Embeddings)的反向逻辑方法,要求一个 LLM 生成一个假设的给定查询的响应,然后使用它的向量和查询向量来提高搜索质量。
为了获得更好的搜索质量而检索更小的块,就要为 LLM 添加周围的上下文。有两种选择:一个是句子窗口检索,即在检索到的较小块周围按句子展开上下文;另一个是父文档检索,即递归地将文档分割成若干较大的父块,其中包含较小的子块。
在句子窗口检索方案中,文档中的每个句子都是单独嵌入,这为上下文余弦距离搜索提供了很高的准确性。在获取最相关的单个句子之后,为了更好地推理找到的上下文,在检索到的句子之前和之后将上下文窗口扩展为 K 个句子,然后将这个扩展的上下文发送给 LLM。
父文档检索与句子窗口检索非常相似,都是搜索更细粒度的信息,然后在将上下文提供给 LLM 进行推理之前扩展过的上下文窗口。文档被拆分成引用较大父块中的较小子块。具体而言,文档被分割成块的层次结构,然后最小的叶子块被发送到索引。在检索期间,获取较小的块,然后如果在 Top-K 检索的块中有超过 N 个块链接到同一个父节点(较大的块),就用这个父节点替换提供给 LLM 的上下文。需要注意的是,搜索仅在子节点索引中执行。
还有一个相对较老的思路,可以像 TF-IDF 或 BM25 这样的稀疏检索算法那样从现代语义或向量搜索中获取最佳结果,并将其结合在一个检索结果中。这里唯一的技巧是将检索到的结果与不同的相似度得分恰当地结合起来,这个问题通常借助于 Reciprocal Rank Fusion(RRF)算法来解决,对检索到的结果重新排序以得到最终的输出。
混合或融合搜索通常在考虑查询和存储文档之间有语义相似性和关键字匹配的情况下,将两种互补的搜索算法结合起来,提供更好的检索结果。在 LangChain 中,这是在集成检索器类中实现的,例如,一个 Faiss 矢量索引和一个基于 BM25 的检索器,并使用 RRF 进行重新排序。在 LlamaIndex 中,也是以一种非常类似的方式完成的。
在得到了检索结果后,需要通过过滤来重新排序。LlamaIndex 提供了多种可用的后处理程序,根据相似度评分、关键词、元数据过滤掉结果,或者用其他模型对结果进行重新排序,比如基于 Sentence Transformer 的交叉编码器、根据元数据(比如日期最近性)内聚重新排序等等。这是将检索到的上下文提供给 LLM 以获得结果答案之前的最后一步。
查询转换是一系列使用 LLM 作为推理引擎来修改用户输入以提高检索质量的技术,有很多不同的技术选择。
如果使用多个来源来生成一个答案,要么是由于初始查询的复杂性需要必须执行多个子查询,将检索到的上下文合并到一个答案中,要么是在多个文档中发现了单个查询的相关上下文,能够准确地反向引用。可以将这个引用任务插入到提示语中,并要求 LLM 提供所使用源的 ID,然后将生成的响应部分与索引中的原始文本块匹配。LlamaIndex 为这种情况提供了一种有效的基于模糊匹配的解决方案。
构建一个可以在单个查询中多次运行 RAG 系统的一个重要特性是聊天逻辑,考虑到对话上下文,就像在 LLM 时代之前的经典聊天机器人一样。这是支持后续问题、重复指代,或任意用户命令相关的以前对话上下文所必需的。查询压缩技术可以同时考虑聊天上下文和用户查询。有几种方法可以实现上下文压缩,一种流行且相对简单的 ContextChatEngine,首先检索与用户查询相关的上下文,然后将其连同聊天历史从缓存发送给 LLM,让 LLM 在生成下一个答案时能够意识到前一个上下文。
更复杂的实现是 CondensePlusContextMode,在每次交互中,聊天历史记录和最后一条消息被压缩成一个新的查询,然后这个查询进入索引,检索到的上下文被传递给 LLM 连同原始用户消息来生成一个答案。
Query 路由是由 LLM 驱动的决策步骤,在给定用户查询的情况下,决定接下来做什么。这些选项通常是总结、针对某些数据索引执行搜索或尝试多种不同的路由,然后在一个答案中综合它们的输出。
Query 路由还可以用于选择索引,或者更广泛的,将用户查询发送到何处,例如,经典的向量存储和图形数据库或关系数据库。对于多文档存储来说,一个非常经典的情况是一个摘要索引和另一个文档块向量索引。定义 Query 路由包括设置它可以做出的选择。路由选择是通过一个 LLM 调用来执行的,它以预定义的格式返回结果,用于将查询路由到给定的索引。如果采用了代理的方式,则将查询路由到子链甚至其他代理,如下面的多文档代理方案所示。LlamaIndex 和 LangChain 都支持 Query 路由。
智能体 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 调用,以细化检索到的上下文并生成更好的答案。响应合成的主要方法有:
对 RAG 流水线中涉及的深度学习模型进行一些微调,一个是负责嵌入质量从而提高上下文检索质量的 Transformer Encoder,另一个负责利用提供的上下文来回答用户查询的 LLM。可以使用 GPT-4 这样的高端 LLM 来生成高质量的合成数据集。但是应该始终注意到,采用一个大型数据集进行训练的开源模型,并使用小型合成数据集进行快速调优,可能会削弱模型的总体能力。
较新版本的 Transformer 编码器优化搜索是相当有效的,bge-large-en-v1.5 即便在笔记本电脑环境中仍能够有较大的检索质量提升。
一个很好的老选择是有一个交叉编码器。如果不完全信任基本编码器,交叉编码器可以对检索到的结果重新排序。它的工作原理是把查询和每个最高 K 个检索到的文本块传递给交叉编码器,用一个标记分隔,然后对它进行微调,相关的块输出为 1,不相关的块输出为 0。这种调整过程可以参考相关技术文档。
最近 OpenAI 开始提供 LLM 微调 API,LlamaIndex 有一个关于在 RAG 设置中微调 GPT-3.5-turbo 以'提取'一些 GPT-4 知识的教程。基本思路是获取一个文档,使用 GPT-3.5-turbo 生成一系列问题,然后使用 GPT-4 根据文档内容生成这些问题的答案即构建一个基于 GPT4 的 RAG 流水线,然后在问答对的数据集上对 GPT-3.5-turbo 进行微调。通过对 RAG 流水线的评估,可以初步确定经过微调的 GPT 3.5-turbo 模型比原始模型能够更好地利用所提供的上下文来生成其答案。
在论文 RA-DIT: Meta AI Research 的检索增强双指令优化中,有一种更为复杂的方法,提出了一种在查询、上下文和答案这个三元组上同时优化 LLM 和检索器(原论文中的双重编码器)的技术。这种技术被用于通过微调 API 和 Llama2 开源模型来微调 OpenAI LLM(在原论文中),导致知识密集型任务指标增加约 5%(与使用 RAG 的 Llama2 65B 相比),并且常识推理任务增加了几个百分点。
有几个框架都可以应用于 RAG 系统的性能评估,指标包括总体答案相关性、答案溯源性、可信度和检索到的上下文相关性等等。
RAG 系统的主要挑战除了答案的相关性和可信度之外,还有就是速度。然而,还有很多其他事情需要考虑,比如基于网络搜索的 RAG,与 Agent 架构的深度融合,以及关于 LLM 长期记忆的一些方式方法。即便如此,RAG 仍然有着广泛的应用范围,我们在使用 RAG 落地应用的时候,希望本文中提到的这些技术能够对大家有所帮助。
在实际落地过程中,开发者需要注意以下几点:
通过综合运用上述技术栈和优化策略,可以构建出既高效又可靠的检索增强生成系统,充分发挥大语言模型的能力。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online