首页 › G LangGraph Agent 编排 › G1 核心心智模型 › 03

G1-03 · 手写 StateGraph 与条件边:ReAct 循环自己搭

入门阶段约 180 分钟阶段:G1 核心心智模型

阶段:G1-核心心智模型 | 预计耗时:90 分钟(本周压轴)| 前置:G1-02 State 与 Reducer 代码:要你自己敲成 langgraph-learn/examples/g1_handmade_react.py(不用 create_agent) 目标:G1-01 说过"create_agent 内部是循环图"——这次亲手把它写出来

学习目标

  1. 掌握 StateGraph 五个动词:add_node / add_edge / add_conditional_edges / compile / invoke
  2. 亲手搭 agent ⇄ tools 循环,理解条件边 router 是 Agent 的"大脑开关"
  3. 知道循环上限 recursion_limit 防什么

核心知识

1. 最小 StateGraph 全景(五个动词一张图)

from langgraph.graph import StateGraph, START, END

builder = StateGraph(State)                       # 1. 拿一张空图
builder.add_node("agent", agent_node)             # 2. 摆节点
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")                  # 3. 定入口
builder.add_conditional_edges("agent", router, ["tools", END])   # 4. 条件边
builder.add_edge("tools", "agent")                #    工具完回来接着想
graph = builder.compile()                         # 5. 编译成可执行图
graph.invoke({"messages": [...]})                 #    开跑

2. 条件边 = Agent 的"继续还是停"开关

import json
def router(state: State) -> str:
    last = state["messages"][-1]
    if getattr(last, "tool_calls", None):         # 模型要求调工具?
        return "tools"
    return "__end__"                              # 说人话了 → 结束

Agent 的本质就这一段:每轮问"上一条 AI 消息里有没有 tool_calls"——有就执行工具再回炉,没有就交作业。

3. 工具执行节点:按模型点的菜做菜

模型在 tool_calls 里说"我要 add(3,5)",tools 节点负责执行并包成 ToolMessage 塞回 messages——模型下一轮才看得到结果。(骨架用 llm.bind_tools(tools) 绑定;执行循环可先用最朴素的 if/elif 手工分发,G4 再上 ToolNode。)

实验任务

任务 1:敲出 handmade ReAct(50 分钟,卡住 20 分钟才准看文档)

# g1_handmade_react.py —— 不用 create_agent 的 ReAct 循环
import os
from typing import Annotated, TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import AnyMessage, ToolMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

@tool
def add(a: int, b: int) -> int:
    """两数相加"""
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """两数相乘"""
    return a * b
TOOLS = {t.name: t for t in [add, multiply]}

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

llm = ChatOpenAI(model=os.environ.get("LG_MODEL", "Qwen/Qwen3.5-4B"),
                 api_key=os.environ["SILICONFLOW_API_KEY"],
                 base_url="https://api.siliconflow.cn/v1", temperature=0).bind_tools(list(TOOLS.values()))

def agent_node(state: State) -> dict:
    return {"messages": [llm.invoke(state["messages"])]}

def tool_node(state: State) -> dict:
    last = state["messages"][-1]
    out = []
    for call in last.tool_calls:                          # 可能点多个菜
        result = TOOLS[call["name"]].invoke(call["args"])
        out.append(ToolMessage(content=str(result), tool_call_id=call["id"]))
    return {"messages": out}

def router(state: State) -> str:
    return "tools" if state["messages"][-1].tool_calls else END

builder = StateGraph(State)
builder.add_node("agent", agent_node); builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", router)
builder.add_edge("tools", "agent")

graph = builder.compile()
result = graph.invoke({"messages": [HumanMessage(content="请计算 (3+5)×12")]})
for m in result["messages"]:
    print(f"[{m.__class__.__name__}] {m.content or '(工具调用)'}")

任务 2:与 create_agent 对账(20 分钟)

同一个问题分别跑 01_tool_agent.py 和本脚本,对比消息轨迹条数与内容——不一致的地方就是你手写版和框架版的差异点,逐条写进笔记。

任务 3:破坏实验(15 分钟)

把 router 永远返回 "tools"(不返回 END)再跑——观察 GraphRecursionError。这就是 recursion_limit(默认 25)防的事:Agent 死循环烧钱烧到天亮。

验收标准

G1 毕业检查点(对应路线图检查点 1):能说清"为什么 Agent 是图不是链";能解释 Reducer 为什么必须存在。

衔接下一阶段

循环会搭了 → G2-01 Command 与动态跳转:比条件边更灵活的跳转方式。

小测验(3 题)

答完立即判分,错题给解析;得分存本地。实验与实操题不上网测:回对话找老师验收。