系列文章第三篇,通过一个完整的数学计算 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 需要:
- 调用加法工具计算 3 + 5
- 获取结果后,再调用乘法工具
- 最终生成答案
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 │──────────────────┘ └───────────┘
|
工作流程解读:
- START → llm_call:LLM 分析用户问题,决定是否调用工具
- llm_call → should_continue:判断是否需要继续
- should_continue 分支:
- 有 tool_calls → tool_node(执行工具)
- 无 tool_calls → END(结束)
- tool_node → llm_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 调用的次数 """ messages: Annotated[list[AnyMessage], operator.add]
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", should_continue, { "tool_node": "tool_node", "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 }
|
工作流程:
- 发送系统提示 + 对话历史给 LLM
- LLM 可能返回:
- 普通文本(不需要工具)
- 包含
tool_calls 的回复(需要调用工具)
- 返回更新后的状态
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"] tool_args = tool_call["args"] tool_call_id = tool_call["id"]
tool_func = tools_by_name[tool_name] observation = tool_func.invoke(tool_args)
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
| 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
| agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges( "llm_call", should_continue, { "tool_node": "tool_node", "end": END } )
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
| git clone https://github.com/liupx/langchain_react_agent_demo cd langchain_react_agent_demo
uv sync
cp .env.example .env
|
9.2 运行演示
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 的核心价值
处理复杂任务
- 单次工具调用不够,需要多次调用
- 自动循环,直到得到最终答案
灵活的流程控制
- 条件边实现动态路由
- 可以随时根据 LLM 的决策调整流程
可观测性强
- 每个步骤都有清晰的日志
- 可以追踪 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. 参考资料
相关文档
本项目文档
源码索引
关于作者:本文基于 langchain_react_agent_demo 项目编写,延续系列文章的风格,专注于 ReAct Agent 原理与 LangGraph 实战的深度解析。欢迎 Star 和 Fork!