智能体模式篇(上)- 深入 ReAct:LangGraph构建能自主思考与行动的 AI

在上一篇博文中,我们成功为 LangGraph 智能体赋予了"记忆",使其能够维护对话历史。然而,一个仅能"记住"过去的聊天机器人,本质上仍是一个被动的文本生成器。它无法与外部世界交互,更无法执行需要多步骤推理和外部工具协助的复杂任务,其能力边界清晰可见。

想象一下这个场景:

  • 你问:"帮我查一下明天北京的天气,并根据气温推荐我穿什么衣服。"
  • 你问:"请计算 (123 * 456) - 789 的结果。"

一个标准的 LLM 可能会因为无法访问实时天气 API 而"幻觉"出一个答案,或者在稍复杂的数学计算中出错。这是因为信息检索和精确计算并非其核心优势。要突破这层壁垒,我们必须让 AI 不仅能"说",更能"做"。

今天,我们将深入探讨一种赋予 AI 自主规划与执行能力 的强大模式------ReAct (Reasoning and Acting) 。通过 LangGraph,我们将构建一个能够**思考(Reasoning)行动(Acting)**的智能体,教会它如何利用外部工具来达成复杂目标。

1. ReAct 模式的哲学:思维与行动的优雅循环

ReAct 模式的核心,在于模仿人类解决复杂问题时的思维过程:观察环境 -> 形成思路 -> 采取行动 -> 观察结果 -> 调整思路 -> 再次行动。这个持续的反馈循环,是智能的体现。

在 AI 智能体中,这个过程被具体化为:

  1. 推理 (Reasoning):当接收到用户请求时,LLM 首先进行"内部独白"。它会分析任务,评估现有信息,并决定下一步是直接回答,还是需要调用工具来获取额外数据。这个"思考"过程是 ReAct 的灵魂。
  2. 行动 (Acting):如果 LLM 决定需要外部协助,它会生成一个结构化的**工具调用(Tool Call)**请求,明确指定要使用的工具名称和所需参数。
  3. 观察 (Observation):外部工具被执行,并将结果(无论是数据、计算结果还是错误信息)返回给智能体。这个结果成为了新的"观察"信息。
  4. 循环 (Loop) :LLM 接收到工具返回的结果,将其作为新的上下文,再次进入推理阶段。它会评估新信息是否足够,并决策是基于现有信息生成最终答案,还是需要调用另一个工具,或是以不同参数再次调用同一工具。

这个"思考 -> 行动 -> 观察"的循环不断迭代,直至 LLM 判断任务已圆满完成,最终输出一个高质量、有事实依据的答案。

这就像你委托一位能干的助理一项任务。他不会盲目行动,而是先规划步骤。如果发现需要查阅资料或使用计算器,他会(行动)去执行,然后带着结果(观察)回来,继续思考如何整合信息,直到为你呈现最终的完美方案。

2. 构建 ReAct 智能体:LangGraph 中的架构与实现

在 LangGraph 中实现 ReAct 模式,需要我们精巧地设计其核心组件:状态管理、节点定义、工具集成和图的条件流控制

2.1 增强 AgentState:构建完整的"推理轨迹"

在之前的智能体中,我们的状态仅存储了对话消息。但在 ReAct 模式下,状态需要记录的远不止于此。它必须成为一个完整的推理轨迹(Reasoning Trace),包含:

  • 用户的原始请求 (HumanMessage)
  • AI 的中间思考与最终回答 (AIMessage)
  • AI 发出的工具调用请求 (包含在 AIMessagetool_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 的工作流程非常高效:

  1. 它会自动检查状态中最新消息(AIMessage)的 tool_calls 字段。
  2. 解析出需要调用的工具名称和参数。
  3. 执行对应的 Python 函数。
  4. 将函数的返回值包装成一个 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 :利用 Annotatedadd_messages 记录完整的推理轨迹。
  • 定义和绑定工具 :通过 @tool 装饰器和 .bind_tools() 方法,赋予 LLM 与外部世界交互的能力。
  • 构建 ReAct 循环 :利用 AgentNodeToolNode条件边实现智能体的核心逻辑。

这只是智能体之旅的开始。在 智能体模式篇(中) 中,我们将在此基础上展示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. 最后告诉我一个关于程序员的笑话。")
相关推荐
舒一笑1 小时前
智能体革命:企业如何构建自主决策的AI代理?
人工智能
丁先生qaq2 小时前
热成像实例分割电力设备数据集(3类,838张)
人工智能·计算机视觉·目标跟踪·数据集
Eiceblue2 小时前
Python读取PDF:文本、图片与文档属性
数据库·python·pdf
红衣小蛇妖2 小时前
神经网络-Day45
人工智能·深度学习·神经网络
weixin_527550402 小时前
初级程序员入门指南
javascript·python·算法
KKKlucifer2 小时前
当AI遇上防火墙:新一代智能安全解决方案全景解析
人工智能
程序员的世界你不懂2 小时前
Appium+python自动化(十)- 元素定位
python·appium·自动化
DisonTangor3 小时前
【小红书拥抱开源】小红书开源大规模混合专家模型——dots.llm1
人工智能·计算机视觉·开源·aigc
CryptoPP3 小时前
使用WebSocket实时获取印度股票数据源(无调用次数限制)实战
后端·python·websocket·网络协议·区块链
树叶@3 小时前
Python数据分析7
开发语言·python