The Chain from the previous lesson only brought tool-call requests back. In this lesson, the tools actually run.
The LLM acts as the router and decides whether to answer directly or call a tool.
Recall the graph from the previous lesson, Chain: START → tool_calling_llm → END. When you ask "Multiply 2 and 3", the LLM returns an AIMessage with tool_calls, but the graph goes straight to END. The tool request simply sits in State and nobody handles it.
The Chain graph has only one node. The LLM says, "I want to call a tool," but the graph has nowhere to execute that call. The user receives a "tool-call request", not the final answer.
Router solves exactly this problem by adding two things:
The name Router comes from the network-router metaphor: choose which path a packet should take based on conditions. Here, the LLM is the router:
Router is the simplest form of an Agent: the LLM decides on its own whether to call a tool based on the user's intent. "The LLM controls the execution flow" is the definition of an Agent. Note that at this point, after the tool finishes, the graph ends immediately; it does not send the result back to the LLM. The next lesson, Agent, will implement the full "think → tool → think" loop.
ToolNode is a built-in, prebuilt LangGraph node dedicated to executing tool calls requested by the LLM. You do not need to write the execution logic yourself:
from langgraph.prebuilt import ToolNode
# Define the tool function
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
# ToolNode receives the list of tools and automatically handles execution logic
tool_node = ToolNode([multiply])
# Register it as a graph node
builder.add_node("tools", tool_node)
ToolNode takes the latest AIMessage from State, reads its tool_calls, and then:
multiplymultiply(a=2, b=3)ToolMessage and appends it to messages# After ToolNode executes, messages in State become:
# [
# HumanMessage("Multiply 2 and 3"),
# AIMessage(tool_calls=[{name:"multiply", args:{a:2,b:3}}]),
# ToolMessage(content="6", name="multiply", tool_call_id="call_xxx") ← added
# ]
If the LLM requests multiple tool calls in one response (parallel tool calling), ToolNode will execute all tools in parallel to improve efficiency.
tools_condition is LangGraph's built-in routing function specifically for checking whether the LLM requested a tool:
from langgraph.prebuilt import tools_condition
# A hand-written equivalent of tools_condition (to help you understand what it does):
def tools_condition(state: MessagesState):
last_message = state["messages"][-1] # Take the latest message
if last_message.tool_calls: # Is there a tool-call request?
return "tools" # → Go to the tools node
return END # → End
# Usage: as the routing function for conditional edges
builder.add_conditional_edges(
"tool_calling_llm", # Start from this node
tools_condition, # Use this function to decide where to go
# Optional: explicitly specify possible routing targets (improves readability)
# {"tools": "tools", END: END}
)
tools_condition is the production-grade version of the "routing function (decide_mood)" we learned in 1-1. It does exactly the same thing: read State and return the next node name.
| LLM Response Type | tools_condition Returns | Next Step |
|---|---|---|
| AIMessage (plain text) | "__end__" | End directly and return to the user |
| AIMessage (with tool_calls) | "tools" | Go to ToolNode to execute the tool |
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
# ─── 工具定义 ───
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
# ─── LLM 绑定工具 ───
llm_with_tools = llm.bind_tools([multiply])
# ─── LLM Node ───
def tool_calling_llm(state: MessagesState):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# ─── 构建图 ───
builder = StateGraph(MessagesState)
builder.add_node("tool_calling_llm", tool_calling_llm) # LLM 节点
builder.add_node("tools", ToolNode([multiply])) # 工具执行节点
builder.add_edge(START, "tool_calling_llm") # 入口
builder.add_conditional_edges( # 条件分支
"tool_calling_llm",
tools_condition, # 有工具调用→tools,没有→END
)
builder.add_edge("tools", END) # 工具执行完→结束
graph = builder.compile()
messages = [HumanMessage(content="Hello, how are you?")]
result = graph.invoke({"messages": messages})
# 执行路径:
# START → tool_calling_llm(LLM 直接回复,无 tool_calls)
# → tools_condition 返回 END
# → 结束
#
# 最终 messages:
# [HumanMessage("Hello, how are you?"), AIMessage("Hi! I'm fine...")]
messages = [HumanMessage(content="What is 2 multiplied by 3?")]
result = graph.invoke({"messages": messages})
# 执行路径:
# START → tool_calling_llm(LLM 请求工具)
# → tools_condition 返回 "tools"
# → ToolNode 执行 multiply(2, 3) = 6
# → END
#
# 最终 messages:
# [
# HumanMessage("What is 2 multiplied by 3?"),
# AIMessage(tool_calls=[{name:"multiply",args:{a:2,b:3}}]),
# ToolMessage(content="6", name="multiply") ← 工具执行结果
# ]
Router executed the tool, but putToolMessage("6")Return directly to the user,Instead of letting LLM interpret the result and answer it in natural language. "6" is just the raw output of the tool.
A truly user-friendly experience is when LLM sees the tool results and says "2 times 3 equals 6" in natural language. This is what the Agent will do in the next lesson - pass the ToolMessage back to the LLM.
| components | type | effect | Equivalent to |
|---|---|---|---|
ToolNode | Pre-built Node | Automate tool calls for LLM requests | Write a function yourself that parses tool_calls and executes it |
tools_condition | Pre-built routing functions | Determine whether there is a tool call and return the target node name | Write your own decide_mood style conditional function |
ToolNodeandtools_conditionIt is a "standard part" provided by LangGraph to help you write less boilerplate code. But you can completely eliminate them and implement equivalent functions yourself - if you understand the 1-1 graph construction pattern, you can see clearly what these two components are doing.