虽然检索增强生成(RAG)在 2023 年占据主导地位,但代理工作流正在推动 2024 年的巨大进展。智能体的使用为构建更强大、稳健且多功能的 大型语言模型(LLM)应用程序开辟了新的可能性。其中一种可能性是在 RAG 流水线中引入智能体,形成代理式 RAG 流水线。
本文将介绍 Agentic RAG 的概念、其实现方式,以及其优势和局限性。
Agentic RAG 基础知识
Agentic RAG 描述了基于 AI 代理的 RAG 实现。在进一步讨论之前,让我们快速回顾一下 RAG 和 AI 代理的基本概念。
什么是检索增强生成(RAG)?
检索增强生成(Retrieval-Augmented Generation,RAG)是一种构建基于大型语言模型(LLM)应用的技术。它利用外部知识源为 LLM 提供相关上下文,以减少幻觉(hallucination)。
一个基础的 RAG 流水线由两个核心组件组成:
- 检索组件 ------ 通常包括嵌入模型(embedding model)和向量数据库(vector database)。
- 生成组件 ------ 基于 LLM 生成回答。
在推理过程中,用户查询会用于对索引文档执行相似性搜索,检索与查询最匹配的文档,并将其提供给 LLM 作为额外的上下文,以提高回答的准确性和相关性。
典型的 RAG 应用有两个相当大的局限性:
- 简单的 RAG 管道仅考虑一个外部知识源。但是,某些解决方案可能需要两个外部知识源,而某些解决方案可能需要外部工具和 API,例如网络搜索。
- 它们是一次性解决方案,这意味着上下文只检索一次。没有对检索到的上下文的质量进行推理或验证。
AI 系统中的代理(智能体)是什么
随着 LLM 的普及,出现了新的智能体(AI agent)和多智能系统范式。智能体是具有角色和任务的 LLM,可以访问内存和外部工具。LLM 的推理能力可帮助代理规划所需的步骤并采取行动完成手头的任务。
因此,智能体的核心组件是:
- LLM(具有角色和任务)
- 内存(短期和长期)
- 规划(例如反思、自我批评、查询路由等)
- 工具(例如计算器、网络搜索等)
一个流行的框架是 ReAct 框架。ReAct 代理可以通过将路由、查询规划和工具使用组合成一个实体来处理顺序多部分查询,同时保持状态(在内存中)。
ini
ReAct = Reason + Act(使用 LLM)
该过程涉及以下步骤:
- 思考:收到用户查询后,代理会推理下一步要采取的操作
- 操作:代理决定操作并执行该操作(例如,使用工具)
- 观察:代理观察操作的反馈
- 此过程不断迭代,直到代理完成任务并响应用户。
什么是 Agentic RAG?
Agentic RAG 指的是一种基于 AI 代理(AI agents)的 RAG 实现方式。它在 RAG 流水线中引入 AI 代理,不仅用于协调各个组件,还能够执行超越简单信息检索和生成的额外任务,以弥补传统非代理式 RAG 流水线的局限性。
Agentic RAG 描述了基于 AI 代理的 RAG 实现。
Agentic RAG 如何工作?
尽管 AI 代理可以嵌入 RAG 流水线的不同阶段,但 Agentic RAG 最常见的做法是增强检索(retrieval)组件,使其具备智能决策能力。
具体来说,检索组件通过 检索代理(retrieval agents) 变得更加智能化,这些代理可以访问多种检索工具,例如:
- 向量搜索引擎(又称查询引擎),用于在向量索引上执行向量搜索(类似于传统 RAG 流水线中的检索方式)
- Web 搜索,用于实时查找最新信息
- 计算器,支持数值计算
- API 调用,可用于访问软件程序,例如电子邮件或聊天应用
- 其他定制化工具,适用于特定业务需求
RAG 代理(RAG agent)可以在检索过程中进行推理和决策,例如:
- 决定是否需要检索信息,避免不必要的查询
- 选择合适的检索工具,确保获取最相关的信息
- 自动构造查询,优化搜索结果
- 评估检索到的上下文,判断是否需要重新检索,以提高结果质量
通过这些智能化特性,Agentic RAG 能够优化信息检索流程,提高 LLM 生成内容的准确性和相关性。
Agentic RAG 架构
与顺序执行的传统 RAG 架构 不同,Agentic RAG 的核心是 AI 代理(Agent) 。这种架构可以根据复杂度的不同进行扩展。在最简单的形式下,单代理 RAG(Single-Agent RAG) 充当路由器(Router) 的角色,而在更高级的场景中,可以使用多个代理构建 多代理 RAG(Multi-Agent RAG)。本节介绍这两种基础架构。
单代理 RAG(Single-Agent RAG,Router)
在最简单的形式下,Agentic RAG 充当 路由器(Router)。这意味着:
- 你的系统至少包含 两个或以上的外部知识源
- AI 代理决定从哪个知识源获取额外上下文
然而,外部知识源不仅限于 向量数据库(Vector Database) ,它还可以是各种工具和服务,例如:
- 向量搜索引擎:从向量数据库(如 Elasticsearch、FAISS)检索相关文档
- Web 搜索:进行实时查询,获取最新的外部信息
- API 调用:访问业务系统,如 Slack 频道、电子邮件、CRM 等
例如,代理可以基于用户查询的需求,决定:
- 是否需要检索信息?(有些问题可能不需要额外查询)
- 应该从哪里检索?(数据库 vs. Web vs. API)
- 如何优化查询?(调整关键词,提高检索准确度)
通过引入代理,RAG 系统能够更智能地选择最优的数据来源,提升查询效率和 LLM 生成内容的准确性。
多代理 RAG 系统(Multi-Agent RAG Systems)
尽管单代理 RAG 能够在不同知识源之间进行选择,但它仍然存在局限性:
- 单一代理 需要同时执行 推理(Reasoning)、检索(Retrieval)和答案生成(Answer Generation),这可能导致性能瓶颈
- 不同类型的检索任务 可能需要不同的专长,而单一代理可能无法高效处理所有类型的查询
因此,将多个代理 串联(Chain) 起来,形成 多代理 RAG(Multi-Agent RAG),可以更高效地完成复杂的信息检索任务。
多代理 RAG 体系结构
在 Multi-Agent RAG 体系中,通常会设计一个 主代理(Master Agent) ,负责协调多个专门的检索代理(Specialized Retrieval Agents)。
📌 示例架构:
- 主代理(Master Agent) :接收用户查询,决定如何拆分任务,并分配给适当的子代理
- 专门检索代理(Specialized Retrieval Agents) :每个子代理负责特定的数据源,例如:
- 🔹 企业内部数据检索代理:从专有数据库或文档存储(如 Confluence、SharePoint)中获取信息
- 🔹 个人数据检索代理 :从用户的 电子邮件、Slack、企业 IM 等个人账户中检索数据
- 🔹 公共信息检索代理 :从 Web 搜索 获取最新的公开信息
多代理 RAG 的优势
- ✅ 分工明确,提高效率:不同代理处理不同任务,避免单一代理超载
- ✅ 灵活适配不同数据源:能够组合内部、个人和公开数据,提升查询质量
- ✅ 更强的推理能力:主代理可以优化查询策略,甚至跨数据源整合信息
通过多代理架构 ,RAG 系统可以更智能、更高效地进行信息检索,从而提升 LLM 的回答质量和实时性。
超越检索代理(Beyond Retrieval Agents)
前面的示例主要展示了不同类型的检索代理(Retrieval Agents)的作用。然而,在 RAG 系统中,代理(Agents)不仅限于检索任务,它们的应用场景非常广泛,可以大幅增强 RAG 系统的能力。
实施 Agentic RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是一种提升 LLM(大型语言模型)准确性和可靠性的方法,它通过连接 LLM 到某种外部数据源来生成响应。Agentic RAG 则是在这个过程中引入某种 AI 代理(Agent)。
简单来说,AI 代理是一个系统,它的大脑是 LLM,同时它还具备记忆能力,并且可以访问一组工具,利用这些工具自主决策并执行特定任务。LLM 本身其实可以被视为一个基本形式的 AI 代理。例如,ChatGPT 具有 LLM 作为大脑,并且能够在一个会话中记住聊天历史,这就是一种记忆能力。然而,ChatGPT 并不具备工具调用能力,所以它只是最基础的 AI 代理。
普通的 RAG(即 "Vanilla RAG")的工作流程是:用户提出问题,系统进行检索(可能还会进行重新排序),然后将检索到的信息传递给 LLM 来生成响应。从这个角度来看,RAG 也可以被视为一种 AI 代理,因为它具备 LLM、记忆(聊天历史)和一个工具(检索能力)。
而我们今天要实现的是一个更复杂的 AI 代理。当用户提出问题后,我们会先进行检索,然后检查检索到的内容是否足以回答问题。如果足够,我们直接将检索内容传递给 LLM 生成答案;否则,我们会调用在线搜索工具,获取更多信息,然后再生成答案。
在上面的这张图中,你可以看到 Agentic RAG 的基本架构:
- 用户提出问题。
- 系统判断是否可以直接使用检索到的内容回答问题。
- 如果可以,就传递给 LLM 生成答案。
- 如果不行,就调用 AI 代理,使用工具(例如在线搜索)来查找额外的信息,然后再生成答案。
除了在线搜索,还有其他工具可用,例如:
- 数学计算工具(用于计算任务)。
- 摘要工具(用于文本总结)。
- 翻译工具(用于语言转换)。
这些工具本质上就是一组函数,AI 代理可以根据需要调用它们。
你可能会问:"OpenAI 的 LLM 不是已经具备很强的推理能力吗?为什么还需要 AI 代理?"。确实,当你向 ChatGPT 提出一般性问题时,它可以直接进行推理并给出答案。但当涉及到外部信息的获取(例如在线搜索或数据库检索),LLM 本身是无法完成的。通过显式地设计我们的 AI 代理,我们可以完全掌控推理步骤,选择要使用的工具,并定义它们的工作模式。LLM 的推理能力是一种通用能力,而 AI 代理是一种可以执行特定任务的系统,例如执行在线搜索或文本处理。
安装
我们首先需要安装 Elasticsearch 及 Kibana。我们可以参考如下的文章来进行安装:
当我们安装的时候,选择 Elastic Stack 8.x 的文章来进行安装。在本展示中,我们将使用 Elastic Stack 8.17.2 来进行展示。当我们首次运行 Elasticsearch 时,我们可以看到如下的界面:
请记下上面的 elastic 超级用户的密码,以备在下面进行使用。
下载代码
为了展示方便,我们在地址 github.com/liu-xiao-gu... 下载最新的代码。
拷贝证书
为了能够使得我们的代码能够正常工作,我们必须拷贝 Elasticsearch 的证书:
bash
1. $ pwd
2. /Users/liuxg/python/twosetai
3. $ cp ~/elastic/elasticsearch-8.17.2/config/certs/http_ca.crt .
4. $ ls 13_agentic_rag_es.ipynb
5. 13_agentic_rag_es.ipynb
6. $ ls http_ca.crt
7. http_ca.crt
创建一个 .env 文件
我们还需要创建一个叫做 .env 的本地文件,并置于项目的根目录下:
.env
ini
1. ES_USER="elastic"
2. ES_PASSWORD="zhHdJmd5oBEVwEfoH2Cr"
3. ES_ENDPOINT="localhost"
5. MODEL_NAME=text-embedding-ada-002
6. AZURE_EMBEDDING_ENDPOINT=https://YourAzureEmbeddingName.openai.azure.com/
7. AZURE_EMBEDDING_API_KEY=YourEmbeddingKey
8. AZURE_EMBEDDING_API_VERSION=2023-05-15
10. AZURE_API_KEY=YourCompletionKey
11. AZURE_EDNPOINT="https://YourAzureChatCompletionName.openai.azure.com"
12. AZURE_API_VERSION="YourVersion"
13. AZURE_DEPLOYMENT_ID="YourDeploymentID"
如上所示,我们将使用 AzureOpenAI 来完成我们的展示。你需要按照文章 "Elasticsearch 开放 inference API 增加了对 Azure OpenAI 嵌入的支持" 来创建相应的资源。如果你的 Elasticsearch 有不同的地址及相应的账号,你需要针对上面的配置做响应的改动。
安装 Python 依赖项
我们使用如下的命令来安装 Python 包:
pip3 install openai elasticsearch python-dotenv PyPDF2 langchain langchain-community litellm langchain-openai langchain-elasticsearch duckduckgo_search
我们可以通过如下的命令来查看 Python 及 Elasticsearch 包的版本:
markdown
1. $ pip3 list | grep elasticsearch
2. elasticsearch 8.17.1
3. langchain-elasticsearch 0.3.2
4. $ python --version
5. Python 3.11.8
搜索的页面
在本示例中,我们将从一个页面下载其内容,并写入到 Elasticsearch 向量库中,然后针对它进行搜索。这个页面的地址是:What is Meta's Llama 3? Everything you Need to Know | Enterprise Tech News EM360Tech
当然,我们也可以使用 Elastic Stack 自带的 Crawler 来对它进行摄入。详细教程请参考文章 "Elastic:开发者上手指南"。
代码展示及运行
读入变量,创建 ES 连接及创建 AzureOpenAI 使用的变量
ini
1. import os
2. import PyPDF2
3. from tqdm.notebook import tqdm
4. import re
5. import json
7. from dotenv import load_dotenv
8. from elasticsearch import Elasticsearch
9. from langchain_openai import AzureOpenAIEmbeddings
10. # from langchain.chat_models import AzureChatOpenAI
11. from openai import AzureOpenAI
13. load_dotenv()
15. ES_USER= os.getenv("ES_USER")
16. ES_PASSWORD = os.getenv("ES_PASSWORD")
17. ES_ENDPOINT = os.getenv("ES_ENDPOINT")
19. MODEL_NAME = os.getenv("MODEL_NAME")
20. AZURE_EMBEDDING_ENDPOINT = os.getenv("AZURE_EMBEDDING_ENDPOINT")
21. AZURE_EMBEDDING_API_KEY = os.getenv("AZURE_EMBEDDING_API_KEY")
22. AZURE_EMBEDDING_API_VERSION = os.getenv("AZURE_EMBEDDING_API_VERSION")
24. AZURE_API_KEY = os.getenv("AZURE_API_KEY")
25. AZURE_EDNPOINT = os.getenv("AZURE_EDNPOINT")
26. AZURE_API_VERSION = os.getenv("AZURE_API_VERSION")
27. AZURE_DEPLOYMENT_ID = os.getenv("AZURE_DEPLOYMENT_ID")
29. url = f"https://{ES_USER}:{ES_PASSWORD}@{ES_ENDPOINT}:9200"
30. es = Elasticsearch(url, ca_certs = "./http_ca.crt", verify_certs = True)
32. print(es.info())
34. elastic_index_name = "agent_rag_index"
36. embeddings = AzureOpenAIEmbeddings(
37. model=MODEL_NAME,
38. azure_endpoint=AZURE_EMBEDDING_ENDPOINT,
39. api_key= AZURE_EMBEDDING_API_KEY,
40. openai_api_version=AZURE_EMBEDDING_API_VERSION
41. )
43. chat = AzureOpenAI(
44. api_key = AZURE_API_KEY,
45. api_version = AZURE_API_VERSION,
46. azure_endpoint = AZURE_EDNPOINT
47. )
50. def read_pdfs_from_folder(folder_path):
51. pdf_list = []
53. # Loop through all files in the specified folder
54. for filename in tqdm(os.listdir(folder_path)):
55. if filename.endswith('.pdf'):
56. file_path = os.path.join(folder_path, filename)
58. # Open each PDF file
59. with open(file_path, 'rb') as file:
60. reader = PyPDF2.PdfReader(file)
61. content = ""
63. # Read each page's content and append it to a string
64. for page_num in range(len(reader.pages)):
65. page = reader.pages[page_num]
66. content += page.extract_text()
68. # Add the PDF content to the list
69. pdf_list.append({"content": content, "filename": filename})
71. return pdf_list
73. folder_path = "./rag_data"
上面的代码输出为:
它显示我们的 ES 客户端连接是成功的。
读取 Web URL 里的内容
python
1. from typing import Optional
2. import requests
4. def fetch_url_content(url: str) -> Optional[str]:
5. """
6. Fetches content from a URL by performing an HTTP GET request.
8. Parameters:
9. url (str): The endpoint or URL to fetch content from.
11. Returns:
12. Optional[str]: The content retrieved from the URL as a string,
13. or None if the request fails.
14. """
15. prefix_url: str = "https://r.jina.ai/"
16. full_url: str = prefix_url + url # Concatenate the prefix URL with the provided URL
18. try:
19. response = requests.get(full_url) # Perform a GET request
20. if response.status_code == 200:
21. return response.content.decode('utf-8') # Return the content of the response as a string
22. else:
23. print(f"Error: HTTP GET request failed with status code {response.status_code}")
24. return None
25. except requests.RequestException as e:
26. print(f"Error: Failed to fetch URL {full_url}. Exception: {e}")
27. return None
python
1. # Replace this with the specific endpoint or URL you want to fetch
2. url: str = "https://em360tech.com/tech-article/what-is-llama-3"
3. content: Optional[str] = fetch_url_content(url)
6. if content is not None:
7. print("Content retrieved successfully:")
8. else:
9. print("Failed to retrieve content from the specified URL.")
我们读取过后的 content 里的内容如下:
把文本分成块
ini
1. from langchain_text_splitters import MarkdownHeaderTextSplitter
2. from langchain_text_splitters import RecursiveCharacterTextSplitter
3. from litellm import completion
5. token_size = 150
6. text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
7. model_,
8. chunk_size=token_size,
9. chunk_overlap=0,
10. )
13. text_chunks = text_splitter.split_text(content)
14. print(f"Total chunks: {len(text_chunks)}")
分成块后,我们可以通过如下方法查看第一个块:
写入数据到 Elasticsearch 中
我们可以通过如下的方法把数据写入到 Elasticsearch 中:
ini
1. from langchain_elasticsearch import ElasticsearchStore
3. def ingest_data_into_es(texts):
4. if not es.indices.exists(index=elastic_index_name):
5. print("The index does not exist, going to generate embeddings")
6. docsearch = ElasticsearchStore.from_texts(
7. texts,
8. embedding = embeddings,
9. es_url = url,
10. es_connection = es,
11. index_name = elastic_index_name,
12. es_user = ES_USER,
13. es_password = ES_PASSWORD
14. )
15. else:
16. print("The index already existed")
18. docsearch = ElasticsearchStore(
19. es_connection=es,
20. embedding=embeddings,
21. es_url = url,
22. index_name = elastic_index_name,
23. es_user = ES_USER,
24. es_password = ES_PASSWORD
25. )
27. return docsearch
29. docsearch = ingest_data_into_es(text_chunks)
我们可以在 Kibana 里进行查看:
上面显示共有 74 个文档。
搜索文档
python
1. def search(str):
2. docs = docsearch.similarity_search(str)
3. return docs
5. question = "what is openai o1 model?"
6. docs = search(question)
7. print("Found docs: ", len(docs))
8. print(docs)
判定搜索的结果是否相关
ini
1. def azure_openai_completion(question, context, is_system_prompt=False):
2. prompt = system_prompt if is_system_prompt else decision_system_prompt
3. summary = chat.chat.completions.create(
4. model = AZURE_DEPLOYMENT_ID,
5. messages=[
6. {"role": "system", "content": prompt.format(context=context) },
7. {"role": "user", "content": user_prompt.format(question=question)},
8. ]
9. )
11. print(summary)
12. return summary
ini
1. question = "what is openai o1 model"
2. results = search(question)
3. context = format_docs(results)
4. response = azure_openai_completion(question, context)
6. has_answer = response.choices[0].message.content
7. has_answer
从返回的结果中,我们可以看出来 has_answer 为 0,也即该文章不含有该问题的答案。即便使用 AzureOpenAI 的 completion 端点返回的结果也不是我们所需要的。
我们再来看看另外一个问题:
ini
1. question = "what is Llama 3?"
2. results = search(question)
3. context = format_docs(results)
4. response = azure_openai_completion(question, context)
6. has_answer = response.choices[0].message.content
7. has_answer
has_answer 这次的值是 1,表明这篇文章含有这个问题的答案。
检查检索到的上下文是否可以回答问题
scss
1. from IPython.display import Markdown, display
2. from duckduckgo_search import DDGS
4. def format_search_results(results):
5. return "\n\n".join(doc["body"] for doc in results)
8. print(f"Question: {question}")
9. if has_answer == '1':
10. print("Context can answer the question")
11. # response = completion(
12. # model="gpt-4o-mini",
13. # messages=[{"content": system_prompt.format(context=context),"role": "system"}, {"content": user_prompt.format(question=question),"role": "user"}],
14. # max_tokens=500
15. # )
16. response = azure_openai_completion(question, context, True)
17. print("Answer:")
18. display(Markdown(response.choices[0].message.content))
19. else:
20. print("Context is NOT relevant. Searching online...")
21. results = DDGS().text(question, max_results=5)
22. context = format_search_results(results)
23. print("Found online sources. Generating the response...")
24. # response = completion(
25. # model="gpt-4o-mini",
26. # messages=[{"content": system_prompt.format(context=context),"role": "system"}, {"content": user_prompt.format(question=question),"role": "user"}],
27. # max_tokens=500
28. # )
29. response = azure_openai_completion(question, context, True)
30. print("Answer:")
31. display(Markdown(response.choices[0].message.content))
在上面,我们依赖于用户所提的问题。如果 has_answer 已经得到答案,那么我们就直接展示答案。相反,如果 has_answer 的值为 0,那么我们就到网上去搜索答案,然后再去让 completion 接口去推理答案。