langchain从入门到精通(四十一)——基于ReACT架构的Agent智能体设计与实现
1. Agent 概念和运行流程
在 LLM 应用中,如果我们知道用户输入所需的工具使用特定顺序时,使用 LCEL 表达式构建链应用非常有用,但是对于某一些特例,我们使用工具的次数与顺序取决于输入,在这种情况下,我们希望让 LLM 本身决定使用工具的次数和顺序,而 Agent智能体 能做到这一点。
在 LangChain 中,Agent 是一个核心概念,它代表了一种能够利用语言模型(LLM)和其他工具来执行复杂任务的系统,Agent 设计的目的是为了处理那些语言模型可能无法直接解决的问题,尤其是当这些任务涉及到多个步骤或者需要外部数据源的情况。
无论一个 Agent 设计得多么复杂,使用什么架构,最基础的工作流程其实都非常简单,只有 5 个步骤:
- 输入理解:Agent 首先解析用户输入,理解其意图和需求。
- 计划定制:基于对输入的理解,Agent 会定制一个执行计划,决定使用哪些工具和执行的顺序。
- 工具调用:Agent 按照计划调用相应的工具,执行必要的操作。
- 结果整合:收集所有工具返回的结果,进行整合和解析,形成最终的输出。
- 反馈循环:如果任务没有完成或者需要进一步的消息,Agent 可以迭代上述过程直到满足条件为止。
运行流程图如下:
对比前面函数调用,其实 Agent 的运行流程非常接近,多了一步 执行工具
和 观察结果
的步骤对于一个 Agent 来说,其组成模块包括 3 个部分:
- Tools:Agent可以访问的工具集,每个工具通常执行一个特定的功能。
- Executor:执行 Agent计划的逻辑。
- Prompt Templates:指导 Agent 如何理解和处理输入的模板,可以定制化以适应不同任务。
在 LangChain v0.2.0 版本中,有两种实现 Agent 的技巧,一种使用的是 传统Agent组件,一种使用 LangGraph,传统Agent组件 特别适合入门的开发者,所以在这一章中我们会使用该方式,下一章在考虑使用 LangGraph 创建更加复杂、灵活性和控制性更强的 Agent 应用。
针对 传统Agent组件,LangChain 团队封装了共计 8 种 Agent,不同的 Agent 适用于不同的聊天模型,表格如下:
• Agent 类型文档链接:https://python.langchain.com/v0.1/docs/modules/agents/agent_types/
2. ReACT 智能体运行流程与实现
2.1 ReACT介绍
ReACT
是 LangChain 最早支持的 Agent 架构,ReACT = Reason + Action,即 推理与行动,目前绝大部分 Agent 架构都是在 ReACT 架构上进行衍生的,该架构最早是 Yao 等人在 2022 年发布的论文中首次被提出,论文参考地址:https://arxiv.org/pdf/2210.03629
。
在 LangChain 中,要想创建基于 ReACT 架构的智能体,其实也非常简单,导入 AgentExecutor、create_react_agent,在实例化的时候,传递对应的 工具 + prompt 即可,其中 ReACT 架构的智能体 prompt 是有要求的。
• ReACT prompt 文档:https://smith.langchain.com/hub/hwchase17/react
例如实现一个带有 谷歌实时搜索 的 ReACT 架构的 Agent,其完整示例代码如下:
import dotenv
from langchain import hub
from langchain.agents import create_react_agent, AgentExecutor
from langchain_community.tools import GoogleSerperRun
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.tools import render_text_description_and_args
from langchain_openai import ChatOpenAIdotenv.load_dotenv()class GoogleSerperArgsSchema(BaseModel):query: str = Field(description="执行谷歌搜索的查询语句")# 1.定义工具与工具列表
google_serper = GoogleSerperRun(name="google_serper",description=("一个低成本的谷歌搜索API。""当你需要回答有关时事的问题时,可以调用该工具。""该工具的输入是搜索查询语句。"),args_schema=GoogleSerperArgsSchema,api_wrapper=GoogleSerperAPIWrapper(),
)
tools = [google_serper]# 2.定义智能体提示模板
prompt = hub.pull("hwchase17/react")# 3.创建大语言模型
llm = ChatOpenAI(model="gpt-3.5-turbo-16k")
agent = create_react_agent(llm=llm,tools=tools,prompt=prompt,tools_renderer=render_text_description_and_args,
)agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)print(agent_executor.invoke({"input": "马拉松的最新世界记录是多少?"}))
当提问 马拉松的最新世界记录是多少? 时,该智能体的输出内容如下:
Entering new AgentExecutor chain…
I should use the google_serper tool to search for the latest world record in marathon.
Action: google_serper
Action Input: {‘query’: ‘latest world record in marathon’}Kenyan athlete Kelvin Kiptum set a men’s world record time of 2:00:35 on October 8, 2023, at the 2023 Chicago Marathon. Ethiopian athlete Tigst Assefa broke the … Missing: query | Show results with:query. Here’s a complete list of the marathon world-record holders and all time top 26.2-mile runners. Missing: query | Show results with:query. In the 2023 Chicago marathon Kelvin Kiptum from Kenya, set a new marathon world record with a time of two hours and 35 seconds, taking 34 seconds off the … Missing: query | Show results with:query. Exactly four years ago, Kipchoge made history with his 1:59:40 unofficial time in Vienna, the fastest any runner had ever covered the marathon. Missing: query | Show results with:query. Kenyan Kelvin Kiptum broke the marathon world record to win the Chicago Marathon in 2 hours and 35 seconds, nearly breaking the two-hour … Kelvin Kiptum set a new world record as he ran the Chicago marathon in 2hr 0min 35sec. Subscribe to Guardian Sport ▻ http://bit.ly/GDNSport … Missing: query | Show results with:query. The current marathon world records are held by Kelvin Kiptum (2:00:35) and Tigist Assefa (2:11:53). Here are the top 20 fastest marathons of all … Missing: query | Show results with:query. CHICAGO (AP) — Kelvin Kiptum set a world record in the Chicago Marathon on Sunday, finishing in 2 hours, 35 seconds to shatter fellow Kenyan …The latest world record in marathon is 2:00:35, set by Kenyan athlete Kelvin Kiptum at the 2023 Chicago Marathon.
Final Answer: The latest world record in marathon is 2:00:35.
Finished chain.
{‘input’: ‘马拉松的最新世界记录是多少?’, ‘output’: ‘The latest world record in marathon is 2:00:35.’}
2.2 ReACT 智能体的缺陷
在 ReACT 架构中,通过对 LLM 的输出内容进行信息提取用于执行不同的逻辑,如果向 ReACT智能体 提问 马拉松的世界记录是多少?,它底层 推理-行动 的完整 Prompt 如下:
Answer the following questions as best you can. You have access to the following tools:google_serper - 一个低成本的谷歌搜索API。当你需要回答有关时事的问题时,可以调用该工具。该工具的输入是搜索查询语句。, args: {'query': {'title': 'Query', 'description': '执行谷歌搜索的查询语句', 'type': 'string'}}Use the following format:Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [google_serper]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input questionBegin!Question: 马拉松的最新世界记录是多少?
Thought:I should use the google_serper tool to search for the latest world record in marathon.
Action: google_serper
Action Input: {'query': 'latest world record in marathon'}
Observation: Kenyan athlete Kelvin Kiptum set a men's world record time of 2:00:35 on October 8, 2023, at the 2023 Chicago Marathon. Ethiopian athlete Tigst Assefa broke the ... Missing: query | Show results with:query. Here's a complete list of the marathon world-record holders and all time top 26.2-mile runners. Missing: query | Show results with:query. In the 2023 Chicago marathon Kelvin Kiptum from Kenya, set a new marathon world record with a time of two hours and 35 seconds, taking 34 seconds off the ... Missing: query | Show results with:query. Exactly four years ago, Kipchoge made history with his 1:59:40 unofficial time in Vienna, the fastest any runner had ever covered the marathon. Missing: query | Show results with:query. Kenyan Kelvin Kiptum broke the marathon world record to win the Chicago Marathon in 2 hours and 35 seconds, nearly breaking the two-hour ... Kelvin Kiptum set a new world record as he ran the Chicago marathon in 2hr 0min 35sec. Subscribe to Guardian Sport ▻ http://bit.ly/GDNSport ... Missing: query | Show results with:query. The current marathon world records are held by Kelvin Kiptum (2:00:35) and Tigist Assefa (2:11:53). Here are the top 20 fastest marathons of all ... Missing: query | Show results with:query. CHICAGO (AP) — Kelvin Kiptum set a world record in the Chicago Marathon on Sunday, finishing in 2 hours, 35 seconds to shatter fellow Kenyan ...
Thought: The latest world record in marathon is 2:00:35, set by Kenyan athlete Kelvin Kiptum at the 2023 Chicago Marathon.
Final Answer: The latest world record in marathon is 2:00:35.
可以完整的观察到 Question(原始问题)
、Thought(推理)
、Action(执行动作)
、Action input(动作输入)
、Observation(观察工具输出)
、Thought(推理)
、Final Answer(最终答案)
一系列完整的运行流程,输出格式非常规范,所以程序不会抛出错误。
但是我们都知道 LLM 的输出是不稳定的,假设我们提问 你好,你是?
时,按推理来说,这段原始提问是不需要调用工具,所以可以直接输出答案,对于 ReACT 来说,需要大语言模型输出如下格式的数据:
I now know the final answer
Final Answer: 我是一个人工智能助手,旨在帮助回答问题和提供信息。请问有什么我可以帮忙的吗?
和上一次生成内容组装成完整的对话语句如下:
Question: 你好,你是?
Thought: I now know the final answer
Final Answer: 我是一个人工智能助手,旨在帮助回答问题和提供信息。请问有什么我可以帮忙的吗?
这样 ReACT智能体 底层检测到 Final Answer
这个关键词,就可以提取出最终答案,但是 LLM 的输出是及其不稳定的,在实际测试中,哪怕是 GPT-4o 模型,它会输出如下的数据:
我是一个人工智能助手,旨在帮助回答问题和提供信息。请问有什么我可以帮忙的吗?
乍得一看好像很合理,但是 ReACT智能体 是根据不同的输出结构来提取出后续要执行的步骤是什么,如果这个时候组装上之前的提问,就变成了
Question: 你好,你是?
Thought: 我是一个人工智能助手,旨在帮助回答问题和提供信息。请问有什么我可以帮忙的吗?
- ReACT智能体 检测到只有 Thought 即观察,并没有后续的步骤了,肯定抛出错误,因为在 Thought 后一般会携带 Action 或者 Final Answer,程序识别不了后续的步骤是什么,结果肯定出错。
- ReACT智能体 在使用时,如果需要修改 Prompt 为中文,一般还需要同步修改 输出解析器,使用代价非常大。
这是因为 ReACT智能体 底层是通过解析响应内容特定关键词
来提醒后续步骤的,如果 Prompt 改成中文,不调整 输出解析器
,在代码底层还是会提取 Thought 的数据,程序 100% 会抛出错误。
3. 基于工具调用的智能体实现
3.1 工具调用智能体
基于 ReACT 架构的智能体会将 tools(工具描述)
、agent_scratchpad(智能体草稿)
、工具结果
、推理
等内容全部放到同一个 prompt 中,并通过提取 LLM的规范输出 来决定下一步的操作,这种模式会随着 LLM 输出的随机性,不同 LLM 性能的差异让程序变得异常脆弱。
而且 ReACT 架构早期设计之初是针对 LLM(文本补全模型) 进行设计的,即传入一段话,让 LLM 补全其后续的文本,随着 LLM 的发展,消息设计更友好、结构化输出更稳定的函数调用、性能更强大的 ChatModel 发布了,可以考虑将 ReACT 迁移到基于 聊天消息 + 工具调用
的架构上,思想不变,但是使用更稳定的 消息列表 + 工具调用
。
迁移流程图如下:
在上述的 工具调用智能体Prompt 中,输出规范 会通过检测 LLM 是输出 文本内容 还是 工具调用参数 来判断下一步是什么,这样性能更加稳定,而且对于绝大部分 LLM 来说,工具调用 支持一次性调用生成多个工具的参数,性能会更强。
在 LangChain 中,其实也为 基于工具调用的Agent 封装了一个快速创建的方法 create_tool_calling_agent() 和 预设Prompt。
• LangChain hub 工具调用 Prompt 链接:
https://smith.langchain.com/hub/hwchase17/openai-tools-agent
• 基于工具调用的智能体文档:https://python.langchain.com/v0.1/docs/modules/agents/agent_types/tool_calling/
3.2 实现示例
在 LangChain 中,要实现 工具调用Agent 其实也非常简单,步骤其实和 ReACT-Agent 一模一样,创建好工具列表、Prompt、LLM(支持工具调用),然后使用 create_tool_calling_agent() 创建智能体,接下来创建智能体执行者完成包装即可。
例如实现一个可以根据用户输入实现自主选择 联网搜索 + 文生图 的智能体,示例代码如下:
import dotenv
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_community.tools import GoogleSerperRun
from langchain_community.tools.openai_dalle_image_generation import OpenAIDALLEImageGenerationTool
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_community.utilities.dalle_image_generator import DallEAPIWrapper
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAIdotenv.load_dotenv()class GoogleSerperArgsSchema(BaseModel):query: str = Field(description="执行谷歌搜索的查询语句")class DallEArgsSchema(BaseModel):query: str = Field(description="输入应该是生成图像的文本提示(prompt)")# 1.定义工具与工具列表
google_serper = GoogleSerperRun(name="google_serper",description=("一个低成本的谷歌搜索API。""当你需要回答有关时事的问题时,可以调用该工具。""该工具的输入是搜索查询语句。"),args_schema=GoogleSerperArgsSchema,api_wrapper=GoogleSerperAPIWrapper(),
)
dalle = OpenAIDALLEImageGenerationTool(name="openai_dalle",api_wrapper=DallEAPIWrapper(model="dall-e-3"),args_schema=DallEArgsSchema,
)
tools = [google_serper, dalle]# 2.定义工具调用agent提示词模板
prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"),("placeholder", "{chat_history}"),("human", "{input}"),("placeholder", "{agent_scratchpad}"),
])# 2.创建大语言模型
llm = ChatOpenAI(model="gpt-4o-mini")# 3.创建agent与agent执行者
agent = create_tool_calling_agent(prompt=prompt,llm=llm,tools=tools,
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)print(agent_executor.invoke({"input": "请帮我绘制一张老爷爷爬山的图片"}))
输出内容:
> Entering new AgentExecutor chain... Invoking: `openai_dalle` with `{'query': 'An elderly man climbing a mountain, with a determined expression, wearing typical hiking gear. The background shows a scenic mountain landscape with trees and a clear sky. The scene conveys a sense of adventure and resilience.'}` https://dalleproduse.blob.core.windows.net/private/images/6fa42821-1331-4b18-90dc-12364b6fb390/generated_00.png?se=2024-08-19T13%3A32%3A20Z&sig=vro%2B%2FrpJbsEbrMxeQnV8rbkE5GKU5tTqlCEvc908V2E%3D&ske=2024-08-23T22%3A55%3A33Z&skoid=09ba021e-c417-441c-b203-c81e5dcd7b7f&sks=b&skt=2024-08-16T22%3A55%3A33Z&sktid=33e01921-4d64-4f8c-a055-5bdaffd5e33d&skv=2020-10-02&sp=r&spr=https&sr=b&sv=2020-10-02这是我为您绘制的老爷爷爬山的图片:  希望您喜欢这幅作品! > Finished chain.
{'input': '请帮我绘制一张老爷爷爬山的图片', 'output': '这是我为您绘制的老爷爷爬山的图片:\n\n\n\n希望您喜欢这幅作品!'}
修改成提问 马拉松的世界记录是多少?
,该智能体的回复如下:
> Entering new AgentExecutor chain... Invoking: `google_serper` with `{'query': '马拉松世界纪录 2023'}` 2023年4月的伦敦马拉松赛,他以2小时1分25秒夺冠,这个成绩仅比当时的世界纪录慢了16秒。 2023年10月的芝加哥马拉松赛,以2小时00分35秒打破世界纪录。 仅有的三次马拉松经历,各个都是世界前六的好成绩。截至2023年10月,马拉松的世界纪录是由选手在芝加哥马拉松上创造的,时间为2小时00分35秒。 > Finished chain.
{'input': '马拉松的世界记录是多少?', 'output': '截至2023年10月,马拉松的世界纪录是由选手在芝加哥马拉松上创造的,时间为2小时00分35秒。'}
4. 内置的其他 Agent 介绍
在 LangChain v 0.2.0 版本之前封装了大量基于 传统Agent组件 的 Agent 智能体创建方法,这些组件的设计思路其实都是以 推理-行动-观察 为思想,不同类型的 Agent 会进行一些额外的扩展,例如 记忆、外挂知识库、多角色、反思 等。
而 LangChain 中封装的 Agent 也是基于 推理-行动-观察 思想,并为不同类型的 LLM/ChatModel 设计了不同的 运行流程 与 prompt,例如有些大语言模型擅长解读和回复 XML 类型的数据(Authropic),而有些模型擅长解读和回复 JSON 数据,而有些模型又擅长结构化输出,所以对于不同类型的模型,可以使用不同 Agent创建方法 来创建。
• LangChain 不同类型 Agent 文档:https://python.langchain.com/v0.1/docs/modules/agents/agent_types/xml_agent/
以 XMLAgent 为例,创建和使用的技巧也非常简单,只需修改 prompt 与创建 Agent 的方法即可,其他的无需任何调整:
# 创建XMLAgent提示模板
prompt = ChatPromptTemplate.from_messages([("human", """You are a helpful assistant. Help the user answer any questions.You have access to the following tools:{tools}In order to use a tool, you can use <tool></tool> and <tool_input></tool_input> tags. You will then get back a response in the form <observation></observation>
For example, if you have a tool called 'search' that could run a google search, in order to search for the weather in SF you would respond:<tool>search</tool><tool_input>weather in SF</tool_input>
<observation>64 degrees</observation>When you are done, respond with a final answer between <final_answer></final_answer>. For example:<final_answer>The weather in SF is 64 degrees</final_answer>Begin!Previous Conversation:
{chat_history}Question: {input}
{agent_scratchpad}"""),
])# 创建大语言模型
llm = ChatOpenAI(model="gpt-4o-mini")# 创建agent与agent执行者
agent = create_xml_agent(prompt=prompt,llm=llm,tools=tools,
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)print(agent_executor.invoke({"input": "马拉松的世界记录是多少?", "chat_history": ""}))
输出内容:
> Entering new AgentExecutor chain...
<tool>google_serper</tool><tool_input>马拉松 世界记录 2023今年4月23日的伦敦马拉松,基普图姆以2小时01分25秒夺冠,大幅打破基普乔格保持的赛道纪录,与世界纪录只差16秒。 这一表现,让他被评为2023年男子场外赛事世界年度最佳运动员。<final_answer>截至2023年,马拉松的世界纪录是2小时01分09秒,由埃利乌德·基普乔格于2018年创造。</final_answer>> Finished chain.
{'input': '马拉松的世界记录是多少?', 'chat_history': '', 'output': '截至2023年,马拉松的世界纪录是2小时01分09秒,由埃利乌德·基普乔格于2018年创造。'}
切换到 JSONAgent 同样只需要更改 prompt 与 create_xml_agent() 方法即可,修正的 prompt 如下
from langchain_core.prompts import ChatPromptTemplateprompt = ChatPromptTemplate.from_messages([("system", """Assistant is a large language model trained by OpenAI.Assistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.Assistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.Overall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist."""),("placeholder", "{chat_history}"),("human", """TOOLS
------
Assistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:{tools}RESPONSE FORMAT INSTRUCTIONS
----------------------------When responding to me, please output a response in one of two formats:**Option 1:**
Use this if you want the human to use a tool.
Markdown code snippet formatted in the following schema:/```json
{{"action": string, \ The action to take. Must be one of {tool_names}"action_input": string \ The input to the action
}}
/```**Option #2:**
Use this if you want to respond directly to the human. Markdown code snippet formatted in the following schema:```json
{{"action": "Final Answer","action_input": string \ You should put what you want to return to use here
}}
/```USER'S INPUT
--------------------
Here is the user's input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):{input}"""),("placeholder", "{agent_scratchpad}"),
])
其他类型的 Agent 也是一模一样的操作,修改 prompt 并切换到不同的 Agent创建方法 即可。
4.1 内置 Agent 的异同点
ReACTAgent、工具调用Agent、XMLAgent 示例演示,其实可以很容易发现这些 Agent 的异同点,首先是相同点:
- 所有 Agent 都拥有 input、agent_scratchpad 两个输入变量,表示原始问题和智能体草稿。
- 所有 Agent 都是单 Agent 自我执行,无法与其他 Agent 进行相互协作。
- 所有 Agent 都可以通过切换 prompt 与 create_xxx_agent() 方法快速切换 Agent 而无需修改大量代码。
- 所有 Agent 设计思想都是基于 推理-行动-观察,只是不同的 Prompt 有所差异。
- 所有 Agent 都是使用同一个 LLM 进行 推理 与 答案生成,并不支持 多LLM 分工。
- 除了 基于工具调用的Agent,其他类型的智能体要修改 Prompt 适配特定语言一般都需要同步修改 输出解析器。
有差异的地方也非常明显: - 不同 Agent 的提示词风格有所差异,有的 Agent 会将 tools 也填写到 prompt 中,有的使用文本提示,有的使用消息提示。
- 不同 Agent 的 输出解析器 不一致,绝大部分取决于 prompt 的差异,有的支持 多工具,有的不支持。
- 不同 Agent 的 输入编码方式 不一致,绝大部分取决于 prompt 和 LLM 的差异,有的支持 历史记忆输入,有的不支持。