TechByteByByte

What create_agent Is Actually Doing

Go one level deeper than Module 14's simplified loop — watch the real loop unfold step by step, see how tool failures are actually handled, and understand exactly what happens when an agent hits its limit.

#LangChain#Agents#LangGraph#Debugging

Module 14 gave you a real, working mental model of an agent’s loop — a model, tools, and a while loop. That model is genuinely correct, but it’s also the simplified version. create_agent’s real, production loop handles a few messy, important edge cases your hand-built version never had to face. This module goes one level deeper, so that when an agent misbehaves in a real project, you know exactly where to look.

The loop, drawn properly

flowchart TD
    A[User sends a message] --> B[Model reads messages and available tools]
    B --> C{Does the model request a tool?}
    C -->|No| H[Loop ends this is the final answer]
    C -->|Yes| D[Tool is executed]
    D --> E{Did the tool raise an error?}
    E -->|Yes| F[Error is caught and turned into a ToolMessage]
    E -->|No| G[Result is turned into a ToolMessage]
    F --> B
    G --> B

This is genuinely the same loop from Module 14, with one real addition worth focusing on: the {Did the tool raise an error?} branch. Your hand-built version never had this — if a tool in run_agent raised an unhandled exception, the entire Python program would crash. create_agent’s real loop doesn’t let that happen.

Example 1: watching the loop unfold, step by step

Recall astream_events from Module 10 — your way of seeing what’s happening inside a running chain, rather than only its final output. It works on agents too, and this is genuinely the clearest way to actually see the loop diagram above happening in real time.

import asyncio
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.agents import create_agent

@tool
def get_capital(country: str) -> str:
    """Get the capital city of a given country."""
    capitals = {"Japan": "Tokyo", "France": "Paris"}
    return capitals.get(country, f"Unknown country: {country}")

@tool
def get_weather(city: str) -> str:
    """Get the current weather for a given city."""
    return f"It's 19°C and partly cloudy in {city}."

agent = create_agent(model=init_chat_model("openai:gpt-4o-mini"), tools=[get_capital, get_weather])

async def main():
    inputs = {"messages": [{"role": "user", "content": "What's the weather in the capital of Japan?"}]}
    async for event in agent.astream_events(inputs, version="v2"):
        if event["event"] == "on_chat_model_start":
            print("→ Model is thinking...")
        elif event["event"] == "on_tool_start":
            print(f"→ Calling tool: {event['name']} with {event['data'].get('input')}")
        elif event["event"] == "on_tool_end":
            print(f"→ Tool result: {event['data'].get('output')}")

asyncio.run(main())

Run this, and you’ll see the loop announce itself, live: the model thinking, then calling get_capital, seeing its result, thinking again, calling get_weather, seeing that result, and finally producing an answer — the exact same round-trip pattern from Module 14’s diagram, now visible as it actually happens, rather than something you have to imagine.

Example 2: what genuinely happens when a tool crashes

Let’s write a tool that deliberately raises an unhandled exception — not the well-behaved “return an error string” pattern from Module 12, but a genuine, uncaught crash — and see how create_agent’s real loop handles it differently than your Module 14 version would have.

from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.agents import create_agent

@tool
def get_stock_price(ticker: str) -> str:
    """Get the current stock price for a given ticker symbol."""
    prices = {"AAPL": 190.50}
    return f"${prices[ticker]}"  # deliberately unhandled — raises KeyError for unknown tickers

agent = create_agent(model=init_chat_model("openai:gpt-4o-mini"), tools=[get_stock_price])

result = agent.invoke({"messages": [{"role": "user", "content": "What's the stock price for XYZ?"}]})
print(result["messages"][-1].content)

Run this with a ticker that isn’t in the dictionary, and notice something genuinely important: your program doesn’t crash. create_agent’s loop catches the tool’s raised exception automatically, converts it into a ToolMessage describing the failure, and hands that back to the model — giving it a real chance to explain the problem to the user gracefully, rather than the whole application collapsing. This is a genuine, real safety net your hand-built Module 14 loop didn’t have, and it’s one of the concrete, practical reasons create_agent earns its place over the manual version in real, production code.

It’s still worth being honest about the trade-off here: this safety net is convenient, but it doesn’t replace the discipline from Module 12 of handling likely failures inside your own tool and returning a genuinely useful message. A caught, generic exception message is rarely as helpful to the model as a deliberately written one explaining exactly what went wrong and why.

Example 3: what actually happens when the recursion limit is hit

Recall recursion_limit from Module 15 — a genuine, necessary safety cap. Let’s actually hit it and see what happens.

from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.agents import create_agent
from langgraph.errors import GraphRecursionError

@tool
def unhelpful_tool(query: str) -> str:
    """A tool that never quite answers the question, forcing repeated calls."""
    return "Hmm, not quite what you need. Try asking again with more detail."

agent = create_agent(model=init_chat_model("openai:gpt-4o-mini"), tools=[unhelpful_tool])

try:
    result = agent.invoke(
        {"messages": [{"role": "user", "content": "Use the tool to find out what day it is."}]},
        config={"recursion_limit": 3},
    )
    print(result["messages"][-1].content)
except GraphRecursionError:
    print("The agent hit its recursion limit without reaching a final answer.")

Because unhelpful_tool is deliberately designed to never satisfy the model, it keeps getting called, round after round, until the loop hits its cap. Notice this raises a real, catchable GraphRecursionError — a genuine, named exception from LangGraph, the engine running underneath, referenced back in Module 2. Wrapping agent calls in a try/except for this specific error is a real, worthwhile production habit: it lets your application respond gracefully — “I wasn’t able to fully resolve this” — rather than crashing or hanging when an agent genuinely can’t converge on an answer.

Common mistakes worth avoiding

Assuming a caught tool error means the agent will automatically recover gracefully. Recall Example 2’s honest caveat — the loop catches the crash and keeps running, but the model only gets a generic error description, not the deliberately useful message a well-written tool, following Module 12’s own advice, would have provided. Automatic error catching is a safety net, not a substitute for writing your own tools defensively.

Never testing what happens when recursion_limit is actually hit. It’s easy to set a limit, as Module 15 taught, and never actually verify your application handles the resulting failure gracefully. Recall Example 3 — without a try/except around GraphRecursionError, your application will crash with an unhandled exception the moment a real agent genuinely can’t converge, which is exactly the scenario a limit was meant to protect against in the first place.

Debugging a misbehaving agent by only looking at the final answer. Recall astream_events from Example 1 — when an agent’s final answer looks wrong, the actual cause is almost always visible somewhere in the intermediate steps: a tool called with the wrong arguments, or a result the model misread. Looking only at result["messages"][-1] throws away exactly the information you need to diagnose what really happened.

What you should take away from this module

  • create_agent’s real loop matches Module 14’s mental model, with one genuine addition: it automatically catches exceptions raised inside tools and converts them into a ToolMessage, rather than crashing.
  • astream_events lets you watch the loop’s actual, real-time progress — which tool got called, with what arguments, and what it returned — the single most useful technique for debugging an agent that isn’t behaving as expected.
  • Hitting recursion_limit raises a real, catchable GraphRecursionError from LangGraph — worth explicitly handling in any agent you’d actually deploy.
  • Automatic tool-error handling is a genuine safety net, not a replacement for writing tools that handle their own likely failures thoughtfully, as taught back in Module 12.

Where this goes next

The next module addresses a question this entire sequence has been quietly building toward: how does an agent actually remember anything — across a single request’s several rounds, and across entirely separate conversations? Agent State and Memory clears up terminology that’s easy to blur together, and shows exactly which mechanism to reach for, for which real need.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed