Skip to main content

Module 8 — Agents: the reasoning and acting loop

Module 7 taught the assistant how to call a single tool. This module puts that call inside a loop: the model observes a state, chooses whether to call a tool, reads the result, and either calls another tool or produces the final answer. That loop is what turns a chain into an agent.

What an agent is, in one sentence

An agent is a prompt in a while loop with tools, a stopping rule and a state. That is the whole idea, and it is a decade old — the modern versions differ in how they express the loop.

Two idioms cover 95 % of the work.

The tool-calling agent ships as a helper in langgraph.prebuilt.create_react_agent and is the modern default: the model produces tool_calls, a runtime executes them, feeds observations back and re-invokes, until the model returns a message without a tool call.

The explicit graph in LangGraph gives you named nodes and edges: a "call model" node, a "call tools" node, and conditional edges deciding what runs next. You reach for it when the tool-calling agent is too opaque — branches, retries, human pauses, persistent state.

The tool-calling agent, in fifteen lines

from langgraph.prebuilt import create_react_agent
from langchain_ollama import ChatOllama

model = ChatOllama(model="qwen2.5:7b-instruct", temperature=0)
tools = [convert_currency, check_per_diem_ceiling, append_spreadsheet_row]

agent = create_react_agent(model, tools)

state = agent.invoke({"messages": [
("user",
"The Berlin dinner was 42.30 EUR on Sept 4. "
"If it clears the meal ceiling, add it to the sheet.")
]})
print(state["messages"][-1].content)

Under the hood the agent runs a small state machine. Node "agent" invokes the model with the current message list and the bound tools. Node "tools" executes any tool_calls in the reply and appends ToolMessages. A conditional edge loops back to "agent" if there were tool calls, or exits otherwise. Everything else is bookkeeping.

That loop is enough for the expense assistant: the model will check the ceiling first, then decide whether to append the row, exactly because module 7's docstrings said so.

The two failure modes that matter

Infinite loops. The model keeps calling tools without ever producing a final answer, either because a tool's return is unhelpful, because a tool errors and the model retries the same call, or because the prompt tells it to "keep checking until sure". Every agent runtime must have a hard iteration cap — 8 to 12 is generous — after which the loop exits and returns whatever the last message was. LangGraph exposes it as recursion_limit; do not run without one.

Silent tool-error swallowing. A tool raises, the runtime turns the exception into a ToolMessage("Error: ..."), and the model treats it as a normal observation and moves on. That is often exactly what you want — the model retries with different arguments. It is also what lets a broken tool produce garbage answers for weeks. Log every ToolException, alert when the error rate on a tool crosses 1 %, and never let a permanent tool failure look identical to a transient one.

Two rules against both:

GuardWhat it does
recursion_limit=10Caps the loop at ten iterations
Structured ToolExceptionDistinguishes retriable errors ("rate limited") from permanent ones ("unknown currency"), so the agent replans rather than retrying forever

LangGraph when the loop needs control

The tool-calling agent hides the graph. Once you need to inject a human approval, persist state to a database or route on a business condition, expose the graph.

from langgraph.graph import StateGraph, END
from typing_extensions import TypedDict

class AgentState(TypedDict):
messages: list
approved: bool

def call_model(state):
reply = model_with_tools.invoke(state["messages"])
return {"messages": state["messages"] + [reply]}

def call_tools(state):
... # execute reply.tool_calls, append ToolMessages
return {"messages": state["messages"]}

def needs_approval(state):
last = state["messages"][-1]
return any(tc["name"] == "append_spreadsheet_row" for tc in last.tool_calls)

graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", call_tools)
graph.add_edge("agent", "tools")
graph.add_edge("tools", "agent")
graph.add_conditional_edges("agent", lambda s: END if not s["messages"][-1].tool_calls else "tools")

The value is not the syntax — that is longer than create_react_agent — it is the explicit control flow. You can now add a human_approval node before tools that pauses the graph until an approver clicks a button in the UI, then resumes. LangGraph's checkpointer serialises the graph state to Postgres between the pause and the resume; the loop tolerates the process restart.

Human in the loop

For any tool that costs money, changes external state or cannot be undone, the right default is human approval before execution, not after. The pattern is a two-step run: the agent proposes the tool call, the runtime pauses, a human sees the proposed arguments in a UI, and either approves — the graph resumes and executes — or rejects — the observation "rejected by reviewer" is fed back and the model re-plans.

Course 30 (Deploying AI in production) returns to this pattern in depth. The reflex to install now: the more powerful the tool, the closer the approval sits to the user. A currency lookup is fully automatic; a spreadsheet write asks for confirmation on the first N calls of a new deployment; a payment tool always asks.

Draw the graph on paper before writing the code

An agent whose graph you cannot draw on a napkin is an agent you cannot debug. Nodes for "call model", "call tools", "human approval", "log outcome". Edges labelled with the condition. Ten minutes with a pen saves a week of "why is it looping".

In summary

  • An agent is a prompt in a while loop with tools, a stopping rule and a state; create_react_agent is the modern default for the simple case.
  • Every agent runtime needs a hard iteration cap (recursion_limit) and structured ToolExceptions that let the model distinguish retriable from permanent failures.
  • LangGraph exposes the loop as an explicit graph of nodes and edges, which is what you need for branches, retries, checkpoints and human-in-the-loop pauses.
  • For tools with side effects, install human approval by default and relax it only after a measured error rate — the more powerful the tool, the closer approval sits to the user.

Next module: seeing what the agent actually did — traces, evaluation and the debugging tools that turn "sometimes it hallucinates" into a measurable regression.