Router executes the tool once and ends. The agent sends the tool results back to the LLM, allowing the LLM to continue reasoning and calling tools—until the task is actually completed.
A single line of code change creates a universal Agent.
Router is already powerful - it lets LLM decide whether to call a tool, and then actually execute that tool. But Router has a fundamental limitation:After the tool is executed, go directly to END。
If the user says: "Add 3 and 4, then multiply the result by 2 and divide it by 5" - the Router will only execute the first tool (the addition) and then be done with it. LLM cannot get the addition result and has no chance to do multiplication and division.
This limit comes from an edge:
# Router 中的这一行:
builder.add_edge("tools", END) # ← 工具执行完 → 结束,LLM 永远看不到工具结果
What's the solution?replace this line. Send tool results back to LLM, allowing LLM to continue inference. This is Agent.
ReAct is the Agent paradigm proposed by Google in 2022. The name comes from the combination of two words:Reasoning (reasoning) +Acting (action).
It describes how an LLM Agent should work:
The key insights from this cycle are:Tool results themselves are information. LLM sees what the tool returns so it can decide what to do next. Append tool results tomessagesList, equivalent to "let LLM observe the execution results".
In LangGraph,messagesThe list is the Agent's "working memory". See the complete conversation history for every LLM call - including all tool calls and tool results. This is how ReAct is implemented at the code level:State carries the complete observation history and drives the next round of reasoning.
From Router to Agent, the code changes are minimal. Find the difference in this line and you understand the essence of Agent:
# tools 执行完 → 结束
builder.add_edge(
"tools",
END ← 这里
)
The tool result is written to messages, but LLM never sees it - the graph is over.
# tools 执行完 → 回到 assistant
builder.add_edge(
"tools",
"assistant" ← 这里!
)
After the tool results are appended to messages, LLM is called again. It sees the result and can continue reasoning.
builder.add_edge("tools", "assistant")It is the core of the entire Agent. This line turns a linear process into a loop, and turns a single-step tool call into a multi-step reasoning.From Router to Agent, only the target node is changed: from END to "assistant".
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})
[sys_msg] + state["messages"]: Add SystemMessage in front of the dialog every time LLM is called, no matter how many times it is loopedadd_edge("tools", "assistant"), which is the physical implementation of the Agent loopOrdinary DAGs (Directed Acyclic Graphs) do not allow cycles. One of the special capabilities of LangGraph isSupport edge loops。tools → assistantThis edge changes the graph from linear to a cyclic structure that can be executed repeatedly. This is the core feature that distinguishes LangGraph from ordinary Pipeline frameworks.
User input: "Add 3 and 4. Multiply the output by 2. Divide the output by 5"
This question requires three steps of calculation: 3+4=7, 7×2=14, 14÷5=2.8. Let's track every step of the Agent, andmessagesThe evolution of lists.
| step | execution node | what happened | messages new content |
|---|---|---|---|
| initial | — | User input incoming graph | HumanMessage("Add 3 and 4...") |
| Round 1 | A assistant | LLM analyzes the task and decides to call add(3, 4) first | AIMessage(tool_calls=[add(a=3,b=4)]) |
| T tools | Execute add(3, 4) → return 7 | ToolMessage(content="7", name="add") | |
| Round 2 | A assistant | LLM sees the result 7 and decides to call multiply(7, 2) | AIMessage(tool_calls=[multiply(a=7,b=2)]) |
| T tools | Execute multiply(7, 2) → return 14 | ToolMessage(content="14", name="multiply") | |
| Round 3 | A assistant | LLM sees the result 14 and decides to call divide(14, 5) | AIMessage(tool_calls=[divide(a=14,b=5)]) |
| T tools | Executing divide(14, 5) → returns 2.8 | ToolMessage(content="2.8", name="divide") | |
| Round 4 | A assistant | LLM sees the end result, organizes natural language answers,Don't request tools again | AIMessage("The result is 2.8") ← None tool_calls |
| Finish | E END | tools_condition detected No tool_calls → route to END | After the graph is executed, the final State is returned. |
Every time the assistant node is called, it seesmessagesWhat is:
NoticemessagesThe list iscumulative growthof. Each loop adds new AIMessage and ToolMessage. LLM sees the complete "conversation history" with every invocation, which is why it can reason correctly about multi-step tasks - working memory is all here.
This is another small difference between Agent and Router:assistantWhen the node calls LLM, it will add a message to the front of the message list.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 注入在最前面
)]}
SystemMessage is defined outside the node function, each timeassistantDynamically spliced when called. This has two benefits:
messagesThe list does not accumulate as the loop repeats. Only the real conversation history is kept in State| Message type | Existence location | LLM can be seen every time | use |
|---|---|---|---|
SystemMessage |
Node function local variables | Yes (dynamic stitching per time) | Tell the LLM its roles and responsibilities |
HumanMessage |
State["messages"] | Yes (in State) | User's original request |
AIMessage |
State["messages"] | Yes (in State) | LLM's historical decisions and tool call requests |
ToolMessage |
State["messages"] | Yes (in State) | The result of tool execution is the "Observe" link of ReAct |
Is the Agent loop infinite? no. The termination condition is very clear, given bytools_conditioncontrol:
LLM decides when to stop. When it determines that all subtasks are completed, its reply no longer containstool_calls, only plain text answers.tools_conditionUpon detecting this, route toEND, the graph is executed.
In theory, if LLM requested tools forever, the graph would run indefinitely. In practice, you can passreact_graph = builder.compile()when passed inrecursion_limitParameter to limit the maximum number of iterations (default is 25):
# 限制最大递归深度(每个节点执行一次算一步)
result = react_graph.invoke(
{"messages": messages},
config={"recursion_limit": 10} # 超过则抛出 GraphRecursionError
)
Let’s compare the two graphs side by side. Everything is exactly the same except for that row of edges:
# 节点
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)
# 节点
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")
| Dimensions | Router | Agent |
|---|---|---|
| Exit of tools node | END |
"assistant"(cycle) |
| Can LLM see tool results? | cannot | Can (append to messages) |
| Support multi-step tool calling | Not supported (can only be adjusted once) | Supported (unlimited times until LLM stops) |
| graph topology | DAG (directed acyclic graph) | A cyclic graph (containing cyclic edges) |
| suitable task | Single-step tool task (check database once, check weather once) | Multi-step reasoning tasks (complex calculations, search + summary + verification) |
| SystemMessage | Optional | Recommended (reinject every round to keep the character stable) |
We demonstrated Agent using three mathematical functions, but the architecture is completely general. Replace the tool with anything and the Agent works the same way:
OpenAI’s Assistants API, Anthropic’s tool use, LangChain’s AgentExecutor – they’re all essentially the same thing: a ReAct loop. The value of LangGraph lies in using the language of graphsExplicitlyExpressing this loop allows you to inspect, modify, and extend each step of the Agent's behavior. You now have a grasp of its core structure.
StateGraph → MessagesState → ToolNode → tools_condition → add_edge("tools", "assistant"). Putting these five together is a fully functional ReAct Agent. All subsequent modules will add functions based on this foundation, rather than reinventing the wheel.