本文是对高级检索增强式生成技术(Retrieval Augmented Generation,简称 RAG)和算法的全面研究,系统性地整合了各种方法。鉴于本文旨在概述和解释现有的 RAG 算法和技术,我将不深入探讨代码实现的细节,仅进行引用,推荐阅读相关文档和学习教程。
#01
引言
如果你已经熟悉 RAG 概念,请直接跳到高级 RAG 部分。
检索增强生成(Retrieval Augmented Generation,简称 RAG)向 LLM 提供了从特定数据源检索的信息,以此作为生成答案的基础。简而言之,RAG 结合了搜索和 LLM 的提示功能,在此基础上,模型根据搜索算法提供的信息,作为上下文来回答问题。这些查询和检索到的上下文会一并被注入到发送给 LLM 的提示中。
到了 2023 年,RAG 已成为基于 LLM 系统中最受欢迎的架构。许多产品基本上都是基于 RAG 构建的,范围从结合网络搜索引擎和 LLM 的问答服务,到数百款与数据互动的应用程序。
即便是向量搜索领域也因这股热潮而受到推动,尽管基于嵌入的搜索引擎早在 2019 年就已经运用了 faiss 技术。例如 chroma、weavaite.io 和 pinecone 这样的向量数据库初创公司,都是基于现有的开源搜索索引(主要是 faiss 和 nmslib)建立的,并在近期增加了输入文本的额外存储以及一些其他工具。
目前,有两个最著名的开源库专注于基于 LLM 的流程和应用开发 —— LangChain 和 LlamaIndex,它们分别在 2022 年的 10 月和 11 月成立,只相差一个月。这两个项目受到了 ChatGPT 发布的启发,并在 2023 年取得了巨大的成功。
本文旨在系统化介绍关键的高级 RAG 技术,并引用它们的实现案例 —— 主要是在 LlamaIndex 中 —— 以帮助其他开发者深入了解这项技术。
但是,大多数教程只选择介绍一种或几种技术,并详细说明如何实现,却未能全面介绍所有可用工具。
此外,LlamaIndex 和 LangChian 这两个惊人的开源项目发展速度极快,它们的文档内容已经比 2016 年的机器学习教科书还要丰富。
#02
基础 RAG
本文将从一组文本文档开始讲述 RAG 流程 —— 此处不涉及之前的所有步骤,而是留给众多可以连接到从 Youtube 到 Notion 的各种来源的优秀开源数据加载器。
作者设计的一个方案,以及文中接下来的所有方案
基础 RAG 案例的过程大致如下:首先,将文本分割成不同的段落;接着,使用某种 Transformer 编码器模型将这些段落转换成向量;然后,把这些向量存储到一个索引中;最后,为 LLM 创建一个提示,指引模型根据我们在搜索步骤中找到的上下文来回答用户的查询。
在实际运行时,我们使用相同的编码器模型将用户的查询转换成向量,然后对这个查询向量进行搜索,针对索引找到前 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 流程的一种经济有效的方法。不要忘了查阅 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 流程选择合适的 “大脑”。
#03
高级 RAG
接下来,我们将深入探讨高级 RAG 技术。 以下是展示核心步骤和涉及算法的示意图。 为了使示意图保持清晰易读,我们省略了一些逻辑循环和复杂的多步骤智能体行为。
高级 RAG 架构的一些关键组件。它更多的是对可用工具的选择,而不是蓝图。
在本文的方案中,绿色元素代表我们将要深入讨论的核心 RAG 技术,蓝色元素则表示文本。需要注意的是,并非所有高级 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
#04
编码器和 LLM 微调
这种方法涉及对 RAG 管道中的两个深度学习模型之一进行微调 —— 要么是负责嵌入质量从而影响上下文检索质量的 Transformer 编码器,要么是负责最优化利用提供的上下文以回答用户查询的 LLM。幸运的是,后者是一个出色的少样本学习者。
如今的一大优势是可以使用像 GPT-4 这样的高级 LLM 来生成高质量的合成数据集。
但需要注意的是,直接采用由专业团队在大型数据集上训练的开源模型,并使用小型合成数据集进行快速微调,可能会限制模型的整体能力。
编码器微调
关于编码器微调方法,我一直持保留态度,因为最新的为搜索优化的 Transformer 编码器已经相当高效。我在 LlamaIndex 笔记本环境中测试了对 bge-large-en-v1.5(当时 MTEB 排行榜前四)的微调性能,发现其检索质量提升了 2%。虽然提升不大,但了解这一选项是有益的,特别是当你在针对特定领域数据集构建 RAG 时。
排名器微调
另一个选择是使用交叉编码器重新排名检索结果,这适用于那些不完全信任基础编码器的情况。
其工作方式是将查询和每个检索到的前 k 个文本块传递给交叉编码器,之间用 SEP Token 分隔,然后微调它以输出 1 表示相关块和 0 表示非相关块。
LLM 微调
最近 OpenAI 开始提供 LLM 微调 API,LlamaIndex 提供了在 RAG 设置中微调 GPT-3.5-turbo 的教程,旨在 “提炼” GPT-4 的部分知识。这里的思路是取一个文档,用 GPT-3.5-turbo 生成若干问题,然后用 GPT-4 根据文档内容回答这些问题(构建一个 GPT-4 驱动的 RAG 管道),接着对 GPT-3.5-turbo 在这些问题 - 答案对数据集上进行微调。使用 RAG 管道评估的 ragas 框架表明,信实度指标提高了 5%,意味着微调后的 GPT-3.5-turbo 模型比原版更好地利用了提供的上下文来生成答案。
一种更复杂的方法在最近的论文 RA-DIT:Retrieval Augmented Dual Instruction Tuning 中被提出,由 Meta AI Research 提出,建议同时调整 LLM 和检索器(原论文中的双编码器),基于查询、上下文和答案的三元组。
论文链接:https://arxiv.org/pdf/2310.01352.pdf
这种技术被用于通过微调 API 对 OpenAI LLM 进行微调,以及对 Llama2 开源模型进行微调,在知识密集型任务的指标上导致了约 5% 的提升(与 RAG 的 Llama2 65B 相比),并在常识推理任务上增加了一些百分点。
#05
评估
针对 RAG 系统性能的评估通常包含几个独立的指标,如整体答案相关性、答案基础性、真实性和检索到的上下文相关性等。
前文提到的 Ragas 使用真实性和答案相关性作为生成答案质量的衡量标准,并对 RAG 方案的检索部分使用传统的上下文精度和召回率来评估。
在 Andrew NG 最近发布的精彩短课程《构建和评估高级 RAG》中,LlamaIndex 和评估框架 Truelens 提出了 RAG 三合一 —— 检索到的上下文与查询的相关性、基础性(LLM 答案由提供的上下文支持的程度)以及答案与查询的相关性。
课程链接:
https://learn.deeplearning.ai/building-evaluating-advanced-rag/
最关键且最可控的指标是检索到的上下文相关性 —— 本文上述的高级 RAG 管道的第 1-7 部分加上编码器和排名器微调部分旨在提高这个指标,而第 8 部分和 LLM 微调则专注于答案相关性和基础性。
关于检索器评估的简单管道例子请看:
https://github.com/run-llama/finetune-embedding/blob/main/evaluate.ipynb
它已应用于编码器微调部分。
一个更高级的方法,不仅考虑命中率,还包括平均倒数排名(一种常见的搜索引擎指标)以及生成答案的指标,如真实性和相关性,这在 OpenAI 的实用指南中有所展示。
LangChain 拥有一个高级的评估框架 LangSmith,它可以实现自定义评估器,并监控 RAG 管道中的运行轨迹,以提高系统的透明度。
如果你在使用 LlamaIndex 构建,可以使用 rag_evaluator llama pack,它提供了一个快速的工具,使用公共数据集评估你的管道。
#06
结论
我试图概述 RAG 的核心算法方法,并希望通过举例说明其中一些方法,激发在你的 RAG 管道中尝试新想法,或为今年出现的各种技术带来一些系统性 —— 对我来说,2023 年是迄今为止 ML 领域最激动人心的一年。
还有许多其他方面需要考虑,如基于网络搜索的 RAG(例如 LlamaIndex、webLangChain 的 RAGs)、更深入地研究 Agent 架构(以及最近 OpenAI 在这一领域的投入)和一些关于 LLM 长期记忆的想法。
除了答案相关性和真实性外,RAG 系统的主要挑战是速度,特别是对于更灵活的基于 Agent 的方案,但这是另一篇文章的内容啦。ChatGPT 和大多数其他助手使用的流媒体功能不是随机的赛博朋克风格,而是一种缩短感知答案生成时间的方法。这就是为什么我看到了较小的 LLM 以及最近 Mixtral 和 Phi-2 的发布有着非常光明的未来。
原文链接:https://pub.towardsai.net/advanced-rag-techniques-an-illustrated-overview-04d193d8fec6
出自:https://mp.weixin.qq.com/s/_GoMu8U-p1_mNa8dFwnTKw