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.

Four pieces: model, tools, loop, limits.
Figure 3.1 — Model, tools, loop and limits.

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.