RAG 高级管道架构完全指南
详解 RAG 系统优化方案,是构建复杂 AI 应用的必读。
详解 RAG 系统优化方案,是构建复杂 AI 应用的必读。
由大语言模型(LLM)驱动的检索增强生成(Retrieval-Augmented Generation,RAG)Pipeline,正在成为构建端到端问答系统的热门方案。LlamaIndex 和 Haystack 等框架在降低 RAG Pipeline 使用门槛方面取得了显著进展。不过,这些框架在为高级 RAG Pipeline 提供优秀抽象的同时,也牺牲了透明度。从用户视角来看,底层究竟发生了什么并不直观,尤其是在出现错误或不一致时。
在这个 EvaDB 应用中,我们将通过分析高级 RAG Pipeline 的运行机制、局限和成本,揭示那些通常隐藏在抽象背后的实现细节。
在笔记本电脑上工作的 Llama 🙂
如果你想直接上手,可以使用以下命令运行该应用:
pip install -r requirements.txt
echo OPENAI_API_KEY='yourkey' > .env
python complex_qa.py
检索增强生成(RAG)是一种面向 LLM 问答的前沿 AI 范式。一个典型的 RAG Pipeline 通常包含:
数据仓库(Data Warehouse)——由一系列数据源组成,例如文档、表格等,其中包含与问答任务相关的信息。
数据仓库(Data Warehouse)——由一系列数据源组成,例如文档、表格等,其中包含与问答任务相关的信息。
向量检索(Vector Retrieval)——给定一个问题,找出与该问题最相似的前 K 个数据块。这个过程通过向量存储完成,例如 Faiss。
向量检索(Vector Retrieval)——给定一个问题,找出与该问题最相似的前 K 个数据块。这个过程通过向量存储完成,例如 Faiss。
回答生成(Response Generation)——给定最相似的前 K 个数据块,使用大语言模型生成回答,例如 GPT-4。
回答生成(Response Generation)——给定最相似的前 K 个数据块,使用大语言模型生成回答,例如 GPT-4。
与传统的基于 LLM 的问答方案相比,RAG 有两项关键优势:
信息实时更新——数据仓库可以实时更新,因此其中的信息始终保持最新。
信息实时更新——数据仓库可以实时更新,因此其中的信息始终保持最新。
来源追踪——RAG 提供了清晰的可追溯性,让用户能够识别信息来源。这对于验证准确性以及减少 LLM 幻觉至关重要。
来源追踪——RAG 提供了清晰的可追溯性,让用户能够识别信息来源。这对于验证准确性以及减少 LLM 幻觉至关重要。
为了回答更加复杂的问题,LlamaIndex 等近期出现的 AI 框架引入了更高级的抽象,例如子问题查询引擎(Sub-question Query Engine)。
在这个应用中,我们将以 Sub-question Query Engine 为例,揭开复杂 RAG Pipeline 的神秘面纱。我们会分析 Sub-question Query Engine 的内部工作机制,并将其抽象简化为最核心的组件。与此同时,我们还会指出高级 RAG Pipeline 面临的一些挑战。
数据仓库由一系列数据源组成,例如文档、表格等,其中包含与问答任务相关的信息。
在这个示例中,我们将使用一个简单的数据仓库,其中包含多个热门城市的 Wikipedia 文章,这一设计受到 LlamaIndex 示例用例的启发。每个城市的 Wiki 都是一个独立的数据源。为了简化问题,我们会限制每篇文档的大小,确保它能够放进 LLM 的上下文窗口。
我们的目标是构建一个能够回答以下问题的系统:
“芝加哥的人口是多少?”
“概括一下亚特兰大的优点。”
“哪个城市的人口最多?”
可以看到,这些问题既可以是基于单个数据源的简单事实型或总结型问题(Q1/Q2),也可以是涉及多个数据源的复杂事实型或总结型问题(Q3)。
我们可以使用以下检索方法:
向量检索(vector retrieval)——给定一个问题和一个数据源,从该数据源中找出与问题最相似的前 K 个数据块,将它们作为上下文,并使用 LLM 生成回答。我们使用 EvaDB 中开箱即用的 FAISS 向量索引进行向量检索,不过这些概念同样适用于任何向量索引。
向量检索(vector retrieval)——给定一个问题和一个数据源,从该数据源中找出与问题最相似的前 K 个数据块,将它们作为上下文,并使用 LLM 生成回答。我们使用 EvaDB 中开箱即用的 FAISS 向量索引进行向量检索,不过这些概念同样适用于任何向量索引。
总结检索(summary retrieval)——给定一个总结型问题和一个数据源,将整个数据源作为上下文,并使用 LLM 生成回答。
总结检索(summary retrieval)——给定一个总结型问题和一个数据源,将整个数据源作为上下文,并使用 LLM 生成回答。
我们的核心洞察是:高级 RAG Pipeline 中的每个组件,本质上都由一次 LLM 调用驱动。整个 Pipeline 就是一系列 LLM 调用,每次调用都使用精心设计的 prompt 模板。正是这些 prompt 模板,让高级 RAG Pipeline 能够完成复杂任务。
事实上,任何高级 RAG Pipeline 都可以拆解成一系列独立的 LLM 调用,并且这些调用都遵循一种通用的输入模式:
Prompt 模板——针对特定任务精心设计的 prompt 模板,例如生成子问题、生成总结。
上下文——执行任务时使用的上下文,例如与问题最相似的前 K 个数据块。
问题——需要回答的问题。
接下来,我们将通过分析 Sub-question Query Engine 的内部工作机制来说明这一原则。
Sub-question Query Engine 需要完成三项任务:
生成子问题——给定一个复杂问题,将其拆分成一组子问题,同时为每个子问题确定合适的数据源和检索函数。
向量检索/总结检索——针对每个子问题,在对应的数据源上使用选定的检索函数,检索相关信息。
回答聚合——将各个子问题的回答聚合成最终回答。
下面逐一详细分析这些任务。
我们的目标是把一个复杂问题拆解成一组子问题,同时为每个子问题确定合适的数据源和检索函数。例如,“哪个城市的人口最多?”这个问题会被拆成五个子问题,每个城市对应一个,形式为“{city} 的人口是多少?”。每个子问题的数据源必须是对应城市的 Wiki,检索函数则必须是向量检索。
乍看之下,这似乎是一项令人望而生畏的任务。具体来说,我们需要回答以下问题:
如何确定应该生成哪些子问题?
如何确定每个子问题应该使用哪个数据源?
如何确定每个子问题应该使用哪种检索函数?
令人惊讶的是,这三个问题的答案完全相同——一次 LLM 调用!整个子问题查询引擎由一次 LLM 调用驱动,其中使用了一个精心设计的 prompt 模板。我们把这个模板称为 Sub-question Prompt Template。
-- Sub-question Prompt Template --
"""
You are an AI assistant that specializes in breaking down complex questions into simpler, manageable sub-questions.
When presented with a complex user question, your role is to generate a list of sub-questions that, when answered, will comprehensively address the original question.
You have at your disposal a pre-defined set of functions and data sources to utilize in answering each sub-question.
If a user question is straightforward, your task is to return the original question, identifying the appropriate function and data source to use for its solution.
Please remember that you are limited to the provided functions and data sources, and that each sub-question should be a full question that can be answered using a single function and a single data source.
"""
这次 LLM 调用的上下文,是系统可以使用的数据源名称和函数。问题则是用户提出的问题。LLM 会输出一组子问题,并为每个子问题指定一个函数和一个数据源。
针对前面的三个示例问题,LLM 会返回以下输出:
对于每个子问题,我们会在对应的数据源上使用选定的检索函数,获取相关信息。例如,对于子问题“芝加哥的人口是多少?”,我们会在芝加哥数据源上执行向量检索。类似地,对于子问题“概括一下亚特兰大的优点”,我们会在亚特兰大数据源上执行总结检索。
这两种检索方法使用的是同一个 LLM prompt 模板。事实上,我们发现,LangchainHub 上广受欢迎的 RAG Prompt 无需修改,就非常适合这个步骤。
-- RAG Prompt Template --
"""
You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Question: {question}
Context: {context}
Answer:
两种检索方法的唯一区别,在于 LLM 调用所使用的上下文。对于向量检索,我们会把与子问题最相似的前 K 个数据块作为上下文。对于总结检索,我们会把整个数据源作为上下文。
这是最后一步,它会把各个子问题的回答聚合成最终回答。例如,对于问题“哪个城市的人口最多?”,各个子问题会分别检索每座城市的人口,随后由回答聚合步骤找出并返回人口最多的城市。RAG Prompt 同样非常适合这一步。
这次 LLM 调用的上下文是所有子问题的回答列表,问题是用户最初提出的问题,而 LLM 会输出最终回答。
在逐层拆解这些抽象之后,我们发现了驱动子问题查询引擎的秘密配方——4 种类型的 LLM 调用,每种调用都有不同的 prompt 模板、上下文和问题。这与我们之前总结出的通用输入模式完全吻合,也远没有最开始看到的那些复杂抽象那么神秘。总结如下:
要查看完整 Pipeline 的实际运行效果,请执行以下命令:
pip install -r requirements.txt
echo OPENAI_API_KEY='yourkey' > .env
python complex_qa.py
下面是系统回答“哪个城市的人口最多?”这一问题的示例。
现在,我们已经揭示了高级 RAG Pipeline 的内部工作机制,接下来看看它们面临的挑战。
问题敏感性——我们在这些系统中观察到的最大挑战,是它们对问题的表述非常敏感。LLM 对用户问题极其敏感,面对一些用户问题时,Pipeline 会以意想不到的方式失败。下面是我们遇到的几个失败案例:子问题错误——LLM 有时会生成错误的子问题。例如,“哪个城市拥有最多的科技公司?”会被拆解成五个“每个城市有哪些科技公司?”的问题,每个城市对应一次,而不是“多伦多有多少家科技公司?”、“芝加哥有多少家科技公司?”等。检索函数错误——对于“总结亚特兰大和多伦多的优点”,系统会使用向量检索函数,而不是总结检索方法。
子问题错误——LLM 有时会生成错误的子问题。例如,“哪个城市拥有最多的科技公司?”会被拆解成五个“每个城市有哪些科技公司?”的问题,每个城市对应一次,而不是“多伦多有多少家科技公司?”、“芝加哥有多少家科技公司?”等。
检索函数错误——对于“总结亚特兰大和多伦多的优点”,系统会使用向量检索函数,而不是总结检索方法。
为了让 Pipeline 能够正确处理每个问题,我们不得不在 prompt engineering 上投入大量精力。这是构建健壮系统时面临的一项重大挑战。
为了验证这一行为,我们使用 LlamaIndex 的 Sub-question Query Engine 实现了这个示例。与我们的观察一致,系统经常生成错误的子问题,也会为子问题选择错误的检索函数,如下所示。
成本——第二项挑战是高级 RAG Pipeline 的成本变化机制。这个问题包含两个方面:成本敏感性——一个问题的最终成本取决于生成的子问题数量、使用的检索函数,以及查询的数据源数量。由于 LLM 对 prompt 很敏感,问题的成本可能会随着问题表述和 LLM 输出而发生显著变化。例如,在上面的 LlamaIndex 基线示例中,错误选择模型(summary_tool)不仅生成了错误回答,还使成本达到了 vector_tool 的 3 倍。成本估算——RAG 框架中的高级抽象掩盖了问题的预估成本。由于问题成本取决于 LLM 输出,因此搭建成本监控系统颇具挑战。
成本敏感性——一个问题的最终成本取决于生成的子问题数量、使用的检索函数,以及查询的数据源数量。由于 LLM 对 prompt 很敏感,问题的成本可能会随着问题表述和 LLM 输出而发生显著变化。例如,在上面的 LlamaIndex 基线示例中,错误选择模型(summary_tool)不仅生成了错误回答,还使成本达到了 vector_tool 的 3 倍。
成本估算——RAG 框架中的高级抽象掩盖了问题的预估成本。由于问题成本取决于 LLM 输出,因此搭建成本监控系统颇具挑战。
由 LLM 驱动的高级 RAG Pipeline 已经彻底改变了问答系统。然而,正如我们所看到的,这些 Pipeline 并非开箱即用的完整解决方案。在底层,它们依赖精心设计的 prompt 模板,以及多次串联起来的 LLM 调用。正如这个 EvaDB 应用所展示的那样,这些 Pipeline 可能对问题表述十分敏感,系统本身也很脆弱,而且成本变化机制并不透明。理解这些复杂细节,是充分发挥它们潜力的关键,也将为未来构建更加健壮、高效的系统铺平道路。