《深入理解 ReAct Agent:用 LangGraph 实现思维循环的实战指南》

系列文章第三篇,通过一个完整的数学计算 Demo,深入理解 ReAct Agent 的工作原理和 LangGraph 的图构建方法

项目地址langchain_react_agent_demo —— 一个典型的 ReAct Agent 实现,建议边读文章边对照代码!


1. 引言 - 什么是 ReAct Agent?

在上一篇文章《从零理解 LangChain》中,我们了解了 Agent = LLM + Tools + 规划能力。但那个例子相对简单——用户问一个问题,Agent 调用一次工具就得到答案。

现实情况往往更复杂:

1
2
3
4
用户: "Calculate (3 + 5) * 2."
Agent: 需要先计算 3 + 5 = 8
再计算 8 * 2 = 16
最后返回答案

这种情况下,单次工具调用是不够的——Agent 需要:

  1. 调用加法工具计算 3 + 5
  2. 获取结果后,再调用乘法工具
  3. 最终生成答案

ReAct(Reason + Act) 就是解决这个问题的模式:

让 Agent 能够循环调用工具,直到得到最终答案

一个形象的比喻:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
传统 Agent(非循环)
┌─────────────────────────────────────┐
│ 用户问题 ──────► 工具调用 ──────► 答案 │
└─────────────────────────────────────┘

ReAct Agent(循环)
┌─────────────────────────────────────┐
│ │
│ 问题 ──► LLM决策 ──► 工具调用 │
│ │ │
│ ▼ │
│ 结果 ──► LLM决策 │
│ │ │
│ ▼ │
│ 答案 ◄────────────┘
│ │
│ └──── 循环直到完成
└─────────────────────────────────────┘

本文通过一个数学计算 Agent Demo,带你深入理解 ReAct 模式的实现原理。

2. 项目概述

2.1 项目特性

这个 Demo 实现了一个典型的 ReAct Agent:

特性 说明
工具调用循环 LLM 根据用户输入决定是否调用工具,形成循环
内置工具 提供加(add)、乘(multiply)、除(divide)三个数学工具
条件边 使用 add_conditional_edges 实现循环逻辑
多种调用方式 支持同步、异步、流式三种调用方式
兼容多模型 支持 DeepSeek、OpenAI 等多种 LLM

2.2 架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                        ┌─────────────────────┐
│ tool_node │
│ (执行工具,返回结果) │
└──────────┬──────────┘


┌──────┐ ┌───────────┐ ┌──────────────┐ ┌────────────┐
│START │───▶│ llm_call │───▶│should_continue│───▶│ END │
└──────┘ └───────────┘ └──────┬───────┘ └────────────┘

┌────────────┴────────────┐
│ │
有 tool_calls 无 tool_calls
│ │
▼ │
┌───────────┐ │
│tool_node │──────────────────┘
└───────────┘

工作流程解读:

  1. STARTllm_call:LLM 分析用户问题,决定是否调用工具
  2. llm_callshould_continue:判断是否需要继续
  3. should_continue 分支:
    • 有 tool_calls → tool_node(执行工具)
    • 无 tool_calls → END(结束)
  4. tool_nodellm_call:工具执行完后,将结果发回 LLM 继续处理

3. 核心概念

3.1 状态(State)

在 LangGraph 中,状态是在整个工作流中传递的数据容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AgentState(TypedDict):
"""
Agent 状态定义

属性:
messages: 对话历史列表
llm_calls: LLM 调用的次数
"""
# 对话消息列表,使用 operator.add 累加器
# 每次有新的消息时,会追加到列表末尾
messages: Annotated[list[AnyMessage], operator.add]

# LLM 调用的总次数
llm_calls: int

关键点:

  • Annotated[list, operator.add]:这是一个”累加器”语法
  • 每次节点返回消息时,会追加到列表中,而不是替换整个列表
  • 这样可以保留完整的对话历史

3.2 工具定义

使用 @tool 装饰器定义工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from langchain_core.tools import tool

@tool
def add(a: int, b: int) -> int:
"""Adds `a` and `b`."""
return a + b

@tool
def multiply(a: int, b: int) -> int:
"""Multiplies `a` and `b`."""
return a * b

@tool
def divide(a: int, b: int) -> float:
"""Divides `a` by `b`."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b

# 工具列表
tools = [add, multiply, divide]

@tool 装饰器的魔法:

  • 自动解析函数签名生成 JSON Schema
  • 提取函数文档字符串发送给 LLM
  • 让 LLM 理解”这个工具是做什么的”

3.3 模型绑定工具

1
2
3
4
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="deepseek-chat")
model_with_tools = model.bind_tools(tools)

绑定后的模型行为:

用户问题 模型输出
“你好” 普通文本回复
“3 + 4 等于多少” tool_calls: [{name: "add", args: {a: 3, b: 4}}]

3.4 条件边

这是实现循环的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def should_continue(state):
if state["messages"][-1].tool_calls:
return "tool_node" # 有工具调用 → 继续循环
return "end" # 没有工具调用 → 结束

# 添加条件边
builder.add_conditional_edges(
"llm_call", # 从 llm_call 节点出发
should_continue, # 条件函数
{
"tool_node": "tool_node", # 返回 "tool_node" → 跳转到 tool_node
"end": END # 返回 "end" → 结束
}
)

4. 节点详解

4.1 llm_call 节点 - LLM 决策

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def llm_call(state: AgentState):
"""LLM 决策节点:让 LLM 根据用户输入决定如何回答"""

# 构建对话上下文
response = model_with_tools.invoke(
[
SystemMessage(
content="你是一个擅长数学计算的助手。当用户提出计算问题时,你应该使用提供的工具(加法、乘法、除法)来计算结果。"
)
]
+ state["messages"] # 追加之前的对话历史
)

return {
"messages": [response],
"llm_calls": state.get("llm_calls", 0) + 1
}

工作流程:

  1. 发送系统提示 + 对话历史给 LLM
  2. LLM 可能返回:
    • 普通文本(不需要工具)
    • 包含 tool_calls 的回复(需要调用工具)
  3. 返回更新后的状态

4.2 tool_node 节点 - 工具执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def tool_node(state: AgentState):
"""工具执行节点:执行 LLM 指定的工具"""

result = []
last_message = state["messages"][-1]

# 遍历每个工具调用
for tool_call in last_message.tool_calls:
tool_name = tool_call["name"] # 如 "add"
tool_args = tool_call["args"] # 如 {"a": 3, "b": 4}
tool_call_id = tool_call["id"] # 唯一标识符

# 查找并执行工具
tool_func = tools_by_name[tool_name]
observation = tool_func.invoke(tool_args)

# 包装成 ToolMessage
result.append(
ToolMessage(
content=str(observation),
tool_call_id=tool_call_id
)
)

return {"messages": result}

关键点:

  • 解析 LLM 返回的 tool_calls
  • 执行对应的工具函数
  • 将结果转换为 ToolMessage 格式
  • 返回给 LLM 继续处理

4.3 should_continue 节点 - 循环控制

1
2
3
4
5
6
7
8
9
10
def should_continue(state: AgentState) -> Literal["tool_node", "end"]:
"""判断是否继续循环"""

messages = state["messages"]
last_message = messages[-1]

if last_message.tool_calls:
return "tool_node" # 继续执行工具
else:
return "end" # 流程结束

5. 图构建

5.1 创建 StateGraph

1
2
3
from langgraph.graph import StateGraph, START, END

agent_builder = StateGraph(AgentState)

5.2 添加节点

1
2
3
4
5
# 添加 LLM 决策节点
agent_builder.add_node("llm_call", llm_call)

# 添加工具执行节点
agent_builder.add_node("tool_node", tool_node)

5.3 添加边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 开始边:从 START 到 llm_call
agent_builder.add_edge(START, "llm_call")

# 2. 条件边:根据是否有 tool_calls 决定走向
agent_builder.add_conditional_edges(
"llm_call",
should_continue,
{
"tool_node": "tool_node",
"end": END
}
)

# 3. 循环边:从 tool_node 回到 llm_call
agent_builder.add_edge("tool_node", "llm_call")

5.4 编译 Agent

1
agent = agent_builder.compile()

编译后的工作流:

1
2
3
4
5
6
7
8
9
10
llm_call → should_continue

┌────────┴────────┐
│ │
有 tool_calls 无 tool_calls
│ │
▼ │
tool_node ────────────┘

└────→ llm_call (循环)

6. 三种调用方式

6.1 同步调用(invoke)

1
2
3
4
5
6
7
8
9
10
11
def run_demo():
"""同步调用演示"""

messages = [HumanMessage(content="Add 3 and 4.")]

# 等待完整结果
result = agent.invoke({"messages": messages})

# 打印对话详情
for msg in result["messages"]:
print(f"[{msg.type}]: {msg.content}")

特点:

  • 最简单直接
  • 等待整个工作流执行完成
  • 返回完整结果

6.2 流式调用(stream)

1
2
3
4
5
6
7
8
def run_stream():
"""流式调用演示"""

messages = [HumanMessage(content="Add 10 and 20.")]

# 逐步获取结果
for chunk in agent.stream({"messages": messages}):
print(chunk, end="|", flush=True)

输出示例:

1
{'llm_call': ...}|{'tool_node': ...}|{'llm_call': ...}|

特点:

  • 实时显示每一步的输出
  • 适合聊天机器人场景
  • 可以看到 Agent 的”思考过程”

6.3 异步调用(ainvoke)

1
2
3
4
5
6
7
async def run_async():
"""异步调用演示"""

messages = [HumanMessage(content="Multiply 7 and 8.")]

# 异步等待结果
result = await agent.ainvoke({"messages": messages})

特点:

  • 不会阻塞当前线程
  • 适合 Web 服务器(如 FastAPI)
  • 支持高并发

7. 实战案例

7.1 简单计算

输入: “Add 3 and 4.”

执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
Step 1: llm_call
→ LLM 决定调用 add(3, 4)

Step 2: tool_node
→ 执行 add(3, 4) = 7

Step 3: llm_call
→ LLM 收到工具结果,生成最终回复 "3 + 4 = 7"

Step 4: should_continue
→ 没有新的 tool_calls,返回 "end"

最终结果:3 + 4 = 7

7.2 复合计算

输入: “Calculate (3 + 5) * 2.”

执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Step 1: llm_call
→ LLM 决定调用 add(3, 5)

Step 2: tool_node
→ 执行 add(3, 5) = 8

Step 3: llm_call
→ LLM 收到结果 8,决定调用 multiply(8, 2)

Step 4: tool_node
→ 执行 multiply(8, 2) = 16

Step 5: llm_call
→ LLM 收到结果 16,生成最终回复

Step 6: should_continue
→ 没有新的 tool_calls,返回 "end"

最终结果:(3 + 5) * 2 = 16

7.3 无需工具

输入: “What is 2 + 3? Just give me the answer.”

执行流程:

1
2
3
4
5
6
7
Step 1: llm_call
→ LLM 判断可以直接回答,不需要工具

Step 2: should_continue
→ 没有 tool_calls,返回 "end"

最终结果:2 + 3 = 5

8. 代码结构

1
2
3
4
5
6
7
8
9
langchain_react_agent_demo/
├── main.py # 主入口,三种调用方式演示
├── src/
│ └── agent/
│ ├── state.py # Agent 状态定义
│ ├── model.py # 模型初始化和工具绑定
│ ├── tools.py # 工具定义(add, multiply, divide)
│ ├── nodes.py # 节点定义(llm_call, tool_node, should_continue)
│ └── graph.py # LangGraph 图构建
文件 功能
state.py 定义 AgentState,使用 Annotated + operator.add 实现消息累加
model.py 创建 ChatOpenAI 模型,绑定工具
tools.py 定义三个数学工具:add, multiply, divide
nodes.py 定义三个节点:llm_call, tool_node, should_continue
graph.py 构建 StateGraph,添加节点和边,编译 Agent
main.py 演示同步、流式、异步三种调用方式

9. 运行指南

9.1 环境准备

1
2
3
4
5
6
7
8
9
10
# 1. 克隆项目
git clone https://github.com/liupx/langchain_react_agent_demo
cd langchain_react_agent_demo

# 2. 安装依赖
uv sync

# 3. 配置环境变量
cp .env.example .env
# 编辑 .env,填入 DEEPSEEK_API_KEY

9.2 运行演示

1
uv run python main.py

9.3 环境变量

变量名 说明 默认值
DEEPSEEK_API_KEY DeepSeek API Key -
DEEPSEEK_BASE_URL API 基础 URL https://api.deepseek.com/v1
DEEPSEEK_MODEL 模型名称 deepseek-chat

10. 总结

核心概念回顾

概念 说明
ReAct Reason + Act,循环调用工具直到得到答案
StateGraph LangGraph 的状态图组件
条件边 根据条件决定下一步走向,实现循环
TypedDict + Annotated 定义状态,使用 operator.add 实现消息累加
ToolMessage 工具执行结果的消息类型

ReAct Agent 的核心价值

  1. 处理复杂任务

    • 单次工具调用不够,需要多次调用
    • 自动循环,直到得到最终答案
  2. 灵活的流程控制

    • 条件边实现动态路由
    • 可以随时根据 LLM 的决策调整流程
  3. 可观测性强

    • 每个步骤都有清晰的日志
    • 可以追踪 LLM 调用次数

与前两篇文章的关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
┌─────────────────────────────────────────────────────────┐
│ 系列文章知识体系 │
├─────────────────────────────────────────────────────────┤
│ │
│ 第一篇:LangChain Agent 原理 │
│ ├── Agent = LLM + Tools + 规划 │
│ ├── 工具调用(Function Calling) │
│ ├── 对话记忆(Memory) │
│ └── 结构化输出(Structured Output) │
│ │ │
│ ▼ │
│ 第二篇:LangSmith 可观测性 │
│ ├── 为什么需要可观测性 │
│ ├── LangSmith 工作原理 │
│ └── 追踪数据解读 │
│ │ │
│ ▼ │
│ 第三篇:ReAct Agent(本文) │
│ ├── ReAct = Reason + Act │
│ ├── 工具调用循环 │
│ ├── LangGraph StateGraph │
│ ├── 条件边实现循环逻辑 │
│ └── 三种调用方式(同步/流式/异步) │
│ │ │
│ ▼ │
│ 下一阶段 │
│ ├── 多 Agent 协作 │
│ ├── RAG(检索增强生成) │
│ └── 生产环境部署 │
│ │
└─────────────────────────────────────────────────────────┘

11. 参考资料

相关文档

本项目文档

源码索引

文件 功能
main.py 主入口,三种调用演示
src/agent/state.py Agent 状态定义
src/agent/model.py 模型初始化与工具绑定
src/agent/tools.py 工具定义
src/agent/nodes.py 节点定义
src/agent/graph.py LangGraph 图构建

关于作者:本文基于 langchain_react_agent_demo 项目编写,延续系列文章的风格,专注于 ReAct Agent 原理与 LangGraph 实战的深度解析。欢迎 Star 和 Fork!