LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 1 · 基础入门
1 1-1 Simple Graph
2 1-2 Chain
3 1-3 Router
4 1-4 Agent
5 1-5 Agent Memory
6 1-6 Deployment
SUM Module Summary
HomeLangGraphModule 1 · 基础入门1-4 Agent
📓 Notebook 📖 Explained
LANGGRAPH MODULE 1 · LESSON 4

Agent:ReAct 架构与无限工具循环
act → observe → reason → 循环

Router 执行一次工具就结束。Agent 把工具结果送回 LLM,让 LLM 继续推理、继续调用工具——直到任务真正完成。
一行代码的改变,成就了通用 Agent。

1 上一讲的局限:Router 只能单步

Router 已经很强大了——它可以让 LLM 决定是否调用工具,然后真正执行那个工具。但 Router 有一个根本限制:工具执行完毕后,直接走向 END

Router 的瓶颈

如果用户说:"把 3 和 4 相加,然后把结果乘以 2,再除以 5"——Router 只会执行第一个工具(加法),然后就结束了。LLM 拿不到加法结果,根本没机会去做乘法和除法。

这个限制来自一条边:

# Router 中的这一行:
builder.add_edge("tools", END)  # ← 工具执行完 → 结束,LLM 永远看不到工具结果

解决方案是什么?把这一行替换掉。把工具结果送回给 LLM,让 LLM 继续推理。这就是 Agent。

2 ReAct 是什么:Reasoning + Acting 循环

ReAct 是 2022 年 Google 提出的 Agent 范式,名字来自两个词的组合:Reasoning(推理)+ Acting(行动)。

它描述了一个 LLM Agent 应该如何工作:

ReAct 循环的本质

🧠 Reason LLM 分析问题
决定下一步
⚙️ Act 调用工具
执行操作
👁️ Observe 获取工具结果
追加到 messages
循环直到 LLM 判断任务完成,不再请求工具为止

这个循环的关键洞察是:工具结果本身也是信息。LLM 看到工具返回了什么,才能决定下一步怎么做。把工具结果追加到 messages 列表,等价于"让 LLM 观察到了执行结果"。

LangGraph 中的 ReAct

在 LangGraph 里,messages 列表就是 Agent 的"工作记忆"。每次 LLM 调用都能看到完整的对话历史——包括所有工具调用和工具结果。这就是 ReAct 在代码层面的实现:状态(State)携带着完整的观察历史,驱动下一轮推理。

3 那一行改变一切的代码

从 Router 到 Agent,代码改动极小。找到这一行的差异,你就理解了 Agent 的本质:

Router(上一讲)

# tools 执行完 → 结束
builder.add_edge(
  "tools",
  END  ← 这里
)

工具结果被写入 messages,但 LLM 永远看不到它——图已经结束了。

Agent(这一讲)

# tools 执行完 → 回到 assistant
builder.add_edge(
  "tools",
  "assistant"  ← 这里!
)

工具结果追加到 messages 后,LLM 再次被调用。它看到了结果,可以继续推理。

核心要点

builder.add_edge("tools", "assistant") 是整个 Agent 的核心。这一行把线性流程变成了循环,把单步工具调用变成了多步推理。从 Router 到 Agent,只改了目标节点:从 END 改为 "assistant"。

4 完整 Agent 图:代码全解析

from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import tools_condition, ToolNode

# ─── 1. 定义工具 ───────────────────────────────────────
def add(a: int, b: int) -> int:
    """Adds a and b."""
    return a + b

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

def divide(a: int, b: int) -> float:
    """Divides a by b."""
    return a / b

tools = [add, multiply, divide]

# ─── 2. LLM 绑定工具 ───────────────────────────────────
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools(tools)

# ─── 3. 系统提示(在每次 LLM 调用时注入)─────────────
sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic.")

# ─── 4. assistant 节点:把 sys_msg 前置注入 ────────────
def assistant(state: MessagesState):
    return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}

# ─── 5. 构建图 ────────────────────────────────────────
builder = StateGraph(MessagesState)

builder.add_node("assistant", assistant)         # LLM 节点
builder.add_node("tools", ToolNode(tools))       # 工具执行节点

builder.add_edge(START, "assistant")             # 入口
builder.add_conditional_edges("assistant", tools_condition)  # 条件分支
builder.add_edge("tools", "assistant")           # ← 关键!循环回去

react_graph = builder.compile()

# ─── 6. 运行 ─────────────────────────────────────────
messages = [HumanMessage(content="Add 3 and 4. Multiply the output by 2. Divide the output by 5")]
result = react_graph.invoke({"messages": messages})

代码要点解析

5 Agent 循环图示:双向箭头的秘密

Agent 图的完整结构(对比 Router)

Router(上一讲)
__start__
tool_calling_llm
tools_condition
有工具调用
tools
END
无工具调用
END
Agent(这一讲)
__start__
assistant
tools_condition
有工具调用
tools
无工具调用
END
循环回去!
唯一区别:tools 节点的出口,从 END 变成了 "assistant"
为什么叫"循环图"

普通的 DAG(有向无环图)不允许循环。LangGraph 的特殊能力之一就是支持循环边tools → assistant 这条边让图从线性变成可以反复执行的循环结构,这是 LangGraph 区别于普通 Pipeline 框架的核心特性。

6 执行步骤追踪:(3+4)×2÷5 完整过程

用户输入:"Add 3 and 4. Multiply the output by 2. Divide the output by 5"

这道题需要三步计算:3+4=7,7×2=14,14÷5=2.8。让我们追踪 Agent 的每一步,以及 messages 列表的演变。

步骤 执行节点 发生了什么 messages 新增内容
初始 用户输入传入图 HumanMessage("Add 3 and 4...")
第 1 轮 A assistant LLM 分析任务,决定先调用 add(3, 4) AIMessage(tool_calls=[add(a=3,b=4)])
T tools 执行 add(3, 4) → 返回 7 ToolMessage(content="7", name="add")
第 2 轮 A assistant LLM 看到结果 7,决定调用 multiply(7, 2) AIMessage(tool_calls=[multiply(a=7,b=2)])
T tools 执行 multiply(7, 2) → 返回 14 ToolMessage(content="14", name="multiply")
第 3 轮 A assistant LLM 看到结果 14,决定调用 divide(14, 5) AIMessage(tool_calls=[divide(a=14,b=5)])
T tools 执行 divide(14, 5) → 返回 2.8 ToolMessage(content="2.8", name="divide")
第 4 轮 A assistant LLM 看到最终结果,组织自然语言答案,不再请求工具 AIMessage("The result is 2.8") ← 无 tool_calls
结束 E END tools_condition 检测到无 tool_calls → 路由到 END 图执行完毕,返回最终 State

messages 列表的完整演变

每次 assistant 节点被调用时,它看到的 messages 是什么:

第 1 次调用 assistant 时,LLM 看到:
HumanMessage("Add 3 and 4. Multiply the output by 2. Divide the output by 5")
第 2 次调用 assistant 时,LLM 看到:
HumanMessage("Add 3 and 4...")
AIMessage(tool_calls=[add(3,4)])
ToolMessage("7", name="add") ← 新增,LLM 现在知道 3+4=7
第 3 次调用 assistant 时,LLM 看到:
HumanMessage("Add 3 and 4...")
AIMessage(tool_calls=[add(3,4)])
ToolMessage("7", name="add")
AIMessage(tool_calls=[multiply(7,2)])
ToolMessage("14", name="multiply") ← 新增,LLM 现在知道 7×2=14
第 4 次调用 assistant 时,LLM 看到:
HumanMessage("Add 3 and 4...")
AIMessage(tool_calls=[add(3,4)])
ToolMessage("7", name="add")
AIMessage(tool_calls=[multiply(7,2)])
ToolMessage("14", name="multiply")
AIMessage(tool_calls=[divide(14,5)])
ToolMessage("2.8", name="divide") ← 新增,LLM 现在知道 14÷5=2.8
→ LLM 判断所有计算完成,返回自然语言答案,不再请求工具
messages 列表是 Agent 的大脑

注意 messages 列表是累积增长的。每次循环都把新的 AIMessage 和 ToolMessage 追加进去。LLM 每次调用都能看到完整的"对话历史",这就是它能正确推理多步任务的原因——工作记忆全在这里。

7 SystemMessage 的作用

这是 Agent 和 Router 的另一个小区别:assistant 节点在调用 LLM 时,会在消息列表最前面加一条 SystemMessage

sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic.")

def assistant(state: MessagesState):
    return {"messages": [llm_with_tools.invoke(
        [sys_msg] + state["messages"]   # ← sys_msg 注入在最前面
    )]}
为什么每次都注入,而不是放到 State 里?

SystemMessage 被定义在节点函数外部,每次 assistant 被调用时动态拼接。这样做有两个好处:

消息类型存在位置LLM 每次能看到用途
SystemMessage 节点函数局部变量 是(每次动态拼接) 告诉 LLM 它的角色和职责
HumanMessage State["messages"] 是(在 State 里) 用户的原始请求
AIMessage State["messages"] 是(在 State 里) LLM 的历史决策和工具调用请求
ToolMessage State["messages"] 是(在 State 里) 工具执行的结果,是 ReAct 的"Observe"环节

8 循环何时终止

Agent 循环是无限的吗?不是。终止条件非常明确,由 tools_condition 控制:

tools_condition 的决策逻辑

assistant 节点执行完毕
tools_condition 检查
最新 AIMessage 有 tool_calls 吗?
有 tool_calls → "tools"
tools(执行工具)
↩ 回到 assistant
没有 tool_calls → END
__end__
循环终止,返回答案

LLM 自己决定什么时候停止。当它判断所有子任务都完成了,它的回复就不再包含 tool_calls,只有纯文字答案。tools_condition 检测到这一点,路由到 END,图执行完毕。

注意:无限循环的风险

理论上,如果 LLM 永远请求工具,图会无限运行。实践中,你可以通过 react_graph = builder.compile() 时传入 recursion_limit 参数来限制最大迭代次数(默认是 25):

# 限制最大递归深度(每个节点执行一次算一步)
result = react_graph.invoke(
    {"messages": messages},
    config={"recursion_limit": 10}  # 超过则抛出 GraphRecursionError
)

9 Router vs Agent:唯一的区别

让我们把两个图并排比较。除了那一行边,其他完全相同:

Router 完整代码(相关部分)

# 节点
builder.add_node("tool_calling_llm", tool_calling_llm)
builder.add_node("tools", ToolNode([multiply]))

# 边
builder.add_edge(START, "tool_calling_llm")
builder.add_conditional_edges(
    "tool_calling_llm", tools_condition
)
# ↓ 工具结果 → 结束
builder.add_edge("tools", END)

Agent 完整代码(相关部分)

# 节点
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))

# 边
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
    "assistant", tools_condition
)
# ↓ 工具结果 → 回到 LLM(关键!)
builder.add_edge("tools", "assistant")
维度RouterAgent
tools 节点的出口 END "assistant"(循环)
LLM 能看到工具结果吗 不能 能(追加到 messages)
支持多步工具调用 不支持(只能调一次) 支持(无限次,直到 LLM 停止)
图的拓扑结构 DAG(有向无环图) 有环图(包含循环边)
适合的任务 单步工具任务(查一次数据库、查一次天气) 多步推理任务(复杂计算、搜索+总结+验证)
SystemMessage 可选 推荐(每轮重新注入,保持角色稳定)

10 这就是通用 Agent 架构

我们用三个数学函数演示了 Agent,但这个架构完全通用。把工具换成任何东西,Agent 的工作方式不变:

可以接入的工具类型

  • 网络搜索(Tavily、Bing)
  • 数据库查询(SQL、向量数据库)
  • 代码执行(Python REPL)
  • API 调用(天气、地图、支付)
  • 文件读写(读 CSV、写报告)
  • 子 Agent 调用(Agent 嵌套)

可以完成的任务类型

  • 研究助手:搜索→总结→验证→再搜索
  • 数据分析:查数据→计算→画图→解释
  • 代码助手:写代码→运行→看报错→修改
  • 客服机器人:查订单→查库存→处理退款
  • 任何需要"边做边思考"的任务
ReAct Agent 是 LLM 应用的标准范式

OpenAI 的 Assistants API、Anthropic 的 tool use、LangChain 的 AgentExecutor——本质上都是同一件事:ReAct 循环。LangGraph 的价值在于用图的语言显式地表达这个循环,让你能够检查、修改、扩展 Agent 的每一步行为。你现在已经掌握了它最核心的结构。

下一步扩展方向

你已经掌握的核心概念

StateGraph → MessagesState → ToolNode → tools_condition → add_edge("tools", "assistant")。把这五个拼在一起,就是一个功能完整的 ReAct Agent。接下来的所有模块,都是在这个基础上加功能,而不是推倒重来。