Lesson 10 / 29
The Tool-Calling Agent as a Graph
See the model-and-tools cycle as two nodes and a conditional edge.
The ReAct-style loop
The classic agent is a small graph: a model node looks at the messages and either answers or requests a tool; a conditional edge checks whether a tool was requested; a tools node executes it and appends the result; an edge goes back to the model. The loop ends when the model answers without requesting a tool, or a limit is reached. In real use the model node calls a chat model bound to tool schemas, and the messages are AIMessage objects with tool_calls and ToolMessage results. LangGraph and LangChain ship prebuilt helpers (such as ToolNode and ready-made agent constructors) so you rarely write this by hand, but building it once makes the framework's behaviour clear.
Model, tools, loop
A tool-calling agent is a two-node cycle: the model decides, the tool node acts, and the result returns to the model.
An agent loop from scratch, run
I ran this offline with langgraph 1.2.12 and langchain-core 1.6.6 in a Python virtual environment. No API key or model is needed because plain Python functions stand in for the model, so the output is repeatable. A scripted function plays the model: it asks for add, then mul, then answers. The tools node validates the tool name against an allow-list before executing. The final message list shows every step of the loop.
import operator
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
TOOLS = {"add": lambda a, b: a + b, "mul": lambda a, b: a * b}
class State(TypedDict):
messages: Annotated[list, operator.add]
script = iter([ # stands in for a model that asks for tools, then answers
{"tool": "add", "args": {"a": 2, "b": 3}},
{"tool": "mul", "args": {"a": 5, "b": 4}},
{"final": "2+3 is 5 and 5*4 is 20."},
])
def model(state):
return {"messages": [next(script)]}
def run_tool(state):
call = state["messages"][-1]
allowed = call["tool"] in TOOLS # validate before executing
result = TOOLS[call["tool"]](**call["args"]) if allowed else "error: unknown tool"
return {"messages": [{"tool_result": result}]}
def after_model(state):
return END if "final" in state["messages"][-1] else "tools"
g = StateGraph(State)
g.add_node("model", model); g.add_node("tools", run_tool)
g.add_edge(START, "model")
g.add_conditional_edges("model", after_model, {END: END, "tools": "tools"})
g.add_edge("tools", "model")
out = g.compile().invoke({"messages": [{"user": "what is 2+3, then 5*4?"}]}, {"recursion_limit": 10})
for m in out["messages"]:
print(m)
Output:
{'user': 'what is 2+3, then 5*4?'}
{'tool': 'add', 'args': {'a': 2, 'b': 3}}
{'tool_result': 5}
{'tool': 'mul', 'args': {'a': 5, 'b': 4}}
{'tool_result': 20}
{'final': '2+3 is 5 and 5*4 is 20.'}Quick check: When does the agent loop end?
- When the model answers without requesting a tool, or a limit is hit
- After exactly one tool call
- Never
- When the GPU is idle
Answer
When the model answers without requesting a tool, or a limit is hit — The conditional edge routes to END once no tool is requested.