在上一篇博文中,我们成功为 LangGraph 智能体赋予了"记忆",使其能够维护对话历史。然而,一个仅能"记住"过去的聊天机器人,本质上仍是一个被动的文本生成器。它无法与外部世界交互,更无法执行需要多步骤推理和外部工具协助的复杂任务,其能力边界清晰可见。
想象一下这个场景:
- 你问:"帮我查一下明天北京的天气,并根据气温推荐我穿什么衣服。"
- 你问:"请计算 (123 * 456) - 789 的结果。"
一个标准的 LLM 可能会因为无法访问实时天气 API 而"幻觉"出一个答案,或者在稍复杂的数学计算中出错。这是因为信息检索和精确计算并非其核心优势。要突破这层壁垒,我们必须让 AI 不仅能"说",更能"做"。
今天,我们将深入探讨一种赋予 AI 自主规划与执行能力 的强大模式------ReAct (Reasoning and Acting) 。通过 LangGraph,我们将构建一个能够**思考(Reasoning)并行动(Acting)**的智能体,教会它如何利用外部工具来达成复杂目标。
1. ReAct 模式的哲学:思维与行动的优雅循环
ReAct 模式的核心,在于模仿人类解决复杂问题时的思维过程:观察环境 -> 形成思路 -> 采取行动 -> 观察结果 -> 调整思路 -> 再次行动。这个持续的反馈循环,是智能的体现。
在 AI 智能体中,这个过程被具体化为:
- 推理 (Reasoning):当接收到用户请求时,LLM 首先进行"内部独白"。它会分析任务,评估现有信息,并决定下一步是直接回答,还是需要调用工具来获取额外数据。这个"思考"过程是 ReAct 的灵魂。
- 行动 (Acting):如果 LLM 决定需要外部协助,它会生成一个结构化的**工具调用(Tool Call)**请求,明确指定要使用的工具名称和所需参数。
- 观察 (Observation):外部工具被执行,并将结果(无论是数据、计算结果还是错误信息)返回给智能体。这个结果成为了新的"观察"信息。
- 循环 (Loop) :LLM 接收到工具返回的结果,将其作为新的上下文,再次进入推理阶段。它会评估新信息是否足够,并决策是基于现有信息生成最终答案,还是需要调用另一个工具,或是以不同参数再次调用同一工具。
这个"思考 -> 行动 -> 观察"的循环不断迭代,直至 LLM 判断任务已圆满完成,最终输出一个高质量、有事实依据的答案。
这就像你委托一位能干的助理一项任务。他不会盲目行动,而是先规划步骤。如果发现需要查阅资料或使用计算器,他会(行动)去执行,然后带着结果(观察)回来,继续思考如何整合信息,直到为你呈现最终的完美方案。
2. 构建 ReAct 智能体:LangGraph 中的架构与实现
在 LangGraph 中实现 ReAct 模式,需要我们精巧地设计其核心组件:状态管理、节点定义、工具集成和图的条件流控制。
2.1 增强 AgentState:构建完整的"推理轨迹"
在之前的智能体中,我们的状态仅存储了对话消息。但在 ReAct 模式下,状态需要记录的远不止于此。它必须成为一个完整的推理轨迹(Reasoning Trace),包含:
- 用户的原始请求 (
HumanMessage
) - AI 的中间思考与最终回答 (
AIMessage
) - AI 发出的工具调用请求 (包含在
AIMessage
的tool_calls
属性中) - 工具执行后返回的结果 (
ToolMessage
)
为了实现这一点,LangGraph 提供了一种强大且优雅的状态更新机制:
python
from typing import Annotated, Sequence, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
"""
AgentState 定义了 ReAct 智能体的状态结构。
Attributes:
messages: 一个可累加的消息序列,用于存储完整的交互历史。
"""
messages: Annotated[Sequence[BaseMessage], add_messages]
这里的关键在于 Annotated[Sequence[BaseMessage], add_messages]
:
BaseMessage
: 作为所有消息类型的基类,它允许messages
列表容纳HumanMessage
,AIMessage
,ToolMessage
等所有类型的消息,保证了状态的通用性。Sequence
: 表明messages
是一个序列(如列表),LangGraph 会智能地处理其更新。add_messages
(Reducer) : 这是至关重要的"魔法"。它是一个 Reducer 函数,规定了状态更新的方式。默认情况下,节点返回的新状态会覆盖 旧状态。但add_messages
指示 LangGraph 将新消息**追加(append)到messages
列表末尾,而不是覆盖。这确保了从用户输入到最终答案的每一步思考、行动和观察都被完整保留,为 LLM 的后续决策提供了完整的上下文。
2.2 定义工具:赋予 LLM 超越语言的能力
工具是智能体"行动"的物理延伸。它们是带有特定功能的 Python 函数,通过 @tool
装饰器暴露给 LLM。
python
from langchain_core.tools import tool
@tool
def multiply_numbers(a: int, b: int) -> int:
"""
计算两个整数的乘积。
当需要进行乘法运算时,请使用此工具。
Args:
a (int): 第一个整数。
b (int): 第二个整数。
"""
return a * b
@tool
def add_numbers(a: int, b: int) -> int:
"""
计算两个整数的和。
当需要进行加法运算时,请使用此工具。
Args:
a (int): 第一个整数。
b (int): 第二个整数。
"""
return a + b
# 将所有可用工具整合到一个列表中
tools = [add_numbers, multiply_numbers]
关键点:
@tool
装饰器: 它将一个普通 Python 函数转换成 LangChain 生态系统中的标准工具,使其可被 LLM 发现和调用。- 文档字符串 (Docstring) 的重要性 : Docstring 不再仅仅是给开发者看的注释。它就是给 LLM 看的 API 文档! LLM 会仔细分析 Docstring 的内容(包括函数描述、参数说明)来理解该工具的功能、适用场景以及如何构建调用参数。一个清晰、准确的 Docstring 是 LLM 能否正确使用工具的决定性因素。
2.3 LLM 绑定工具:让模型"知晓"并"掌握"工具
定义工具后,我们必须让 LLM "意识"到这些工具的存在,并赋予它调用它们的能力。这通过 .bind_tools()
方法实现。
python
from langchain_openai import ChatOpenAI
# 初始化一个支持工具调用的 LLM 模型
llm = ChatOpenAI(model="gpt-4o")
# 将工具列表绑定到 LLM,创建一个新的、具备工具调用能力的 LLM 实例
llm_with_tools = llm.bind_tools(tools)
llm.bind_tools(tools)
的作用是创建一个"武装"过的新 LLM 实例。当这个实例被调用时,它的输出将不再局限于文本,而是可能包含一个结构化的 tool_calls
字段,这正是 ReAct 模式中"行动"信号的来源。
2.4 Agent 节点(大脑):推理与决策的核心
Agent 节点是 ReAct 智能体的"大脑中枢"。它封装了 llm_with_tools
,负责接收当前完整的状态,进行推理,并决定下一步的走向。
python
def call_agent(state: AgentState) -> dict:
"""
Agent 节点,代表智能体的大脑。
它调用 LLM 进行推理,并返回 LLM 的响应。
"""
messages = state["messages"]
# 我们可以在此添加系统指令,引导 AI 的行为模式
system_prompt = SystemMessage(content="你是一个强大的AI助手,能够分析用户问题并决定是直接回答还是使用工具来解决问题。")
# 调用绑定了工具的 LLM,传入完整的历史消息
response = llm_with_tools.invoke([system_prompt] + messages)
# 返回的 response (AIMessage) 会被 add_messages 自动追加到状态中
return {"messages": [response]}
此节点完美体现了 ReAct 的推理 环节。它将迄今为止所有的交互历史(messages
)提供给 LLM,让其在最充分的上下文中做出最明智的决策:是生成最终答案,还是发出工具调用请求?
2.5 ToolNode(执行器):将指令转化为行动
当 Agent 节点决定调用工具后,流程需要一个"执行者"来实际运行这些工具。LangGraph 为此提供了一个预构建的节点:ToolNode
。
python
from langgraph.prebuilt import ToolNode
# 实例化 ToolNode,并传入我们定义的所有工具
tool_node = ToolNode(tools)
ToolNode
的工作流程非常高效:
- 它会自动检查状态中最新消息(
AIMessage
)的tool_calls
字段。 - 解析出需要调用的工具名称和参数。
- 执行对应的 Python 函数。
- 将函数的返回值包装成一个
ToolMessage
对象,并添加到状态中。
ToolNode
完美地扮演了"行动"和"观察"的角色,将 AI 的意图转化为现实世界的操作和结果。
2.6 构建 ReAct 循环:用条件边实现智能流转
ReAct 模式的精髓在于其循环结构。我们使用**条件边(Conditional Edges)**在 Agent 节点和 ToolNode 之间构建一个动态的反馈回路。
ReAct 图的基本逻辑:
(需要工具?)
START --> AgentNode ----------> ToolNode
| ^ |
| | (返回结果) |
| +-------------------+
|
+---- (无需工具/任务完成) ----> END
1. 定义决策函数:我们需要一个函数来检查 Agent 节点的输出,并决定下一步的路径。
python
def should_continue(state: AgentState) -> str:
"""
决策函数,用于路由。
检查最后一条消息是否包含工具调用。
"""
last_message = state["messages"][-1]
if last_message.tool_calls:
# 如果有工具调用,则流程继续到 tool_node
return "continue"
else:
# 如果没有工具调用(意味着 AI 认为任务已完成),则结束流程
return "end"
2. 组装计算图:
python
from langgraph.graph import StateGraph, END
# 初始化 StateGraph
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("agent", call_agent)
workflow.add_node("tools", tool_node)
# 设置入口点
workflow.set_entry_point("agent")
# 添加条件边
workflow.add_conditional_edges(
"agent", # 从 "agent" 节点出发
should_continue, # 使用此函数做决策
{
"continue": "tools", # 如果返回 "continue",则流向 "tools" 节点
"end": END # 如果返回 "end",则结束
}
)
# 添加从 ToolNode 返回 AgentNode 的普通边
workflow.add_edge("tools", "agent")
# 编译图,生成可执行应用
app = workflow.compile()
通过这几步,我们便用 LangGraph 的声明式语法构建了一个完整的 ReAct 循环。add_conditional_edges
是实现这一动态流程的核心。
3. 运行 ReAct 智能体:见证思考与行动的共舞
现在,让我们通过一个实例来直观感受 ReAct 智能体的工作流程。
python
from langchain_core.messages import HumanMessage
# 模拟用户输入一个需要多步计算的任务
query = "计算 123 乘以 456,然后将结果加上 789。"
inputs = {"messages": [HumanMessage(content=query)]}
# 使用 stream 模式运行并观察每一步的状态变化
for event in app.stream(inputs, stream_mode="values"):
event["messages"][-1].pretty_print()
print("---")
当你运行上述代码时,你将清晰地看到 ReAct 的内部流转,这个过程完美展示了 ReAct 智能体的强大之处:它能够自主地将复杂任务分解为一系列"思考-行动"的子步骤,并动态地利用工具,直到问题被彻底解决。
4. ReAct 的价值与应用前景
ReAct 模式不仅仅是一个技术技巧,它是一种架构思想,从根本上提升了 LLM 的能力维度。
- 突破固有局限:通过调用计算器、搜索引擎、各类 API,智能体得以执行精确计算、获取实时信息,弥补了 LLM 自身的短板。
- 处理复杂任务:对于需要多步骤、多工具协作的复杂工作流(如"预定一张明天从上海到北京的机票,并将其添加到我的日历"),ReAct 提供了可靠的实现框架。
- 鲁棒性与可解释性 :由于每一步的"思考"(
AIMessage
)和"观察"(ToolMessage
)都被记录下来,我们不仅能得到最终答案,还能完整地回溯智能体的决策路径,这对于调试和信任至关重要。
ReAct 是构建所有高级 AI 智能体的基石。掌握了它,你就开启了通往真正"智能"助理的大门。
总结与展望
在本篇博文中,我们深入探索了 ReAct 模式,并利用 LangGraph 将其付诸实践。你现在已经掌握了:
- ReAct (Reasoning and Acting) 模式的核心哲学:一个由"思考-行动-观察"组成的智能循环。
- 构建健壮
AgentState
:利用Annotated
和add_messages
记录完整的推理轨迹。 - 定义和绑定工具 :通过
@tool
装饰器和.bind_tools()
方法,赋予 LLM 与外部世界交互的能力。 - 构建 ReAct 循环 :利用
AgentNode
、ToolNode
和条件边实现智能体的核心逻辑。
这只是智能体之旅的开始。在 智能体模式篇(中) 中,我们将在此基础上展示LangGraph如何用于构建人机协作的工作流。
下一步建议 :动手为你的 ReAct 智能体添加一个网络搜索工具(例如使用 Tavily 或 DuckDuckGo Search API)。尝试向它提问需要实时信息的问题("今天 AI 领域有什么大新闻?"),亲身体验它从语言模型到信息助理的蜕变。
完整代码
以下是 ReAct Agent 的完整实现,复制即可运行:
python
import os
from typing import Annotated, Sequence, TypedDict
from dotenv import load_dotenv
from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage, AIMessage, SystemMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
# --- 1. 准备环境:加载环境变量 ---
load_dotenv()
# 检查 API 密钥是否已设置
if not os.getenv("OPENAI_API_KEY"):
raise ValueError("请在 .env 文件中设置您的 OpenAI API 密钥 (OPENAI_API_KEY)")
# --- 2. 定义 Agent 的状态 (AgentState) ---
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
# --- 3. 定义可用的工具 (Tools) ---
# 使用 @tool 装饰器可以轻松地将任何函数转换为 LangChain 工具。
# LLM 会利用函数的名称、文档字符串和类型注解来决定何时以及如何调用它。
# !! 重要提示:清晰的文档字符串 (docstring) 对 LLM 正确使用工具至关重要。
@tool
def add(a: int, b: int) -> int:
"""计算两个整数的和。"""
print(f"--- 工具执行: add({a}, {b}) ---")
return a + b
@tool
def subtract(a: int, b: int) -> int:
"""计算两个整数的差。"""
print(f"--- 工具执行: subtract({a}, {b}) ---")
return a - b
@tool
def multiply(a: int, b: int) -> int:
"""计算两个整数的乘积。"""
print(f"--- 工具执行: multiply({a}, {b}) ---")
return a * b
# 将所有工具放入一个列表中,以便后续使用
tools = [add, subtract, multiply]
# --- 4. 创建 LLM 和工具执行器 ---
# 4.1. 将工具绑定到 LLM
# 我们使用 .bind_tools() 方法,让 LLM "知道"我们有哪些工具可用。
# 这会返回一个新的、具备工具调用能力的 LLM 实例。
llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_tools = llm.bind_tools(tools)
# 4.2. 创建 ToolNode
# ToolNode 是一个预构建的 LangGraph 节点,它接收工具调用请求,
# 执行相应的工具,并返回结果。
tool_node = ToolNode(tools)
# --- 5. 定义图的节点 (Nodes) ---
# 5.1. 定义 Agent 节点 ("大脑")
# 这个节点负责调用 LLM,进行推理和决策。
def call_agent(state: AgentState):
"""Agent 节点,代表智能体的大脑。"""
print("--- 调用 Agent 大脑 ---")
system_prompt = SystemMessage(content=
"你是一个AI助手,请根据我的查询给出最好的回答."
)
# 调用绑定了工具的 LLM,传入完整的历史消息
# LLM 会根据上下文决定是直接回答,还是调用工具
response = llm_with_tools.invoke([system_prompt] + state["messages"])
# 返回一个包含 LLM 响应的字典,它将被 add_messages 追加到状态中
return {"messages": [response]}
# 5.2. 定义决策函数 (Conditional Edge Logic)
# 这个函数决定在 Agent 节点之后应该走向哪里。
def should_continue(state: AgentState) -> str:
"""
决策函数,用于路由。
检查最后一条消息是否包含工具调用。
"""
last_message = state["messages"][-1]
if not last_message.tool_calls:
print(">>> 决策:无需工具或任务完成,流向 'END'。")
return "end"
else:
print(">>> 决策:需要执行工具,流向 'tools' 节点。")
return "continue"
# --- 6. 构建并编译计算图 (Graph) ---
# 6.1. 初始化 StateGraph
workflow = StateGraph(AgentState)
# 6.2. 添加节点
workflow.add_node("agent", call_agent)
workflow.add_node("tools", tool_node)
# 6.3. 设置入口点
workflow.set_entry_point("agent")
# 6.4. 添加条件边
# 从 "agent" 节点出发,根据 should_continue 函数的返回值决定下一个节点。
workflow.add_conditional_edges(
"agent",
should_continue,
{
"continue": "tools", # 如果返回 "continue",则流向 "tools" 节点
"end": END # 如果返回 "end",则结束流程
}
)
# 6.5. 添加普通边
# 工具执行完毕后 ("tools" 节点),流程总是返回到 "agent" 节点,形成循环。
workflow.add_edge("tools", "agent")
# 6.6. 编译图,生成可执行应用
app = workflow.compile()
# --- 7. 运行 ReAct Agent ---
def run_agent(query: str):
"""运行 Agent 并打印每一步的详细过程。"""
# 初始状态,只包含用户的输入
inputs = {"messages": [HumanMessage(content=query)]}
# 使用 stream 模式运行并观察每一步的状态变化
for event in app.stream(inputs, stream_mode="values"):
last_message = event["messages"][-1]
last_message.pretty_print()
if __name__ == "__main__":
# 示例: 需要多步工具调用的复杂任务
run_agent("计算 (40 + 12) 然后乘以 6. 最后告诉我一个关于程序员的笑话。")