LangGraph Tutorial (Python): debugging agent loops for advanced developers

By Cyprian AaronsUpdated 2026-04-21
langgraphdebugging-agent-loops-for-advanced-developerspython

This tutorial shows you how to debug a LangGraph agent loop in Python by making state transitions visible, adding hard stop conditions, and instrumenting each node so you can see exactly why the graph keeps cycling. You need this when your agent is “working” but never finishes, keeps revisiting the same tool call, or returns inconsistent state after a few turns.

What You'll Need

  • Python 3.10+
  • langgraph
  • langchain-openai
  • langchain-core
  • An OpenAI API key in OPENAI_API_KEY
  • Basic familiarity with LangGraph nodes, edges, and state
  • A terminal where you can run a single Python file

Install the packages:

pip install langgraph langchain-openai langchain-core

Step-by-Step

  1. Start with a minimal graph that can loop forever if your routing logic is wrong.
    The point is not to build a perfect agent yet; it’s to create a reproducible failure mode you can inspect.
from typing import TypedDict, Annotated
from operator import add

from langgraph.graph import StateGraph, START, END


class AgentState(TypedDict):
    messages: Annotated[list[str], add]
    iterations: int


def think(state: AgentState) -> dict:
    return {
        "messages": [f"thinking at iteration {state['iterations']}"],
        "iterations": state["iterations"] + 1,
    }


def route(state: AgentState) -> str:
    return "think" if state["iterations"] < 3 else END


graph = StateGraph(AgentState)
graph.add_node("think", think)
graph.add_edge(START, "think")
graph.add_conditional_edges("think", route)

app = graph.compile()
result = app.invoke({"messages": [], "iterations": 0})
print(result)
  1. Add explicit debug output inside each node so you can see the exact state entering and leaving the node.
    In real agent loops, silent state mutation is what hides bugs; logging the shape of the state makes loop diagnosis much faster.
from typing import TypedDict, Annotated
from operator import add

from langgraph.graph import StateGraph, START, END


class AgentState(TypedDict):
    messages: Annotated[list[str], add]
    iterations: int


def think(state: AgentState) -> dict:
    print(f"[think] input={state}")
    update = {
        "messages": [f"iteration={state['iterations']}"],
        "iterations": state["iterations"] + 1,
    }
    print(f"[think] output={update}")
    return update


def route(state: AgentState) -> str:
    decision = "think" if state["iterations"] < 2 else END
    print(f"[route] iterations={state['iterations']} -> {decision}")
    return decision
  1. Instrument execution with a checkpointer so you can inspect intermediate states after each step.
    This is the most useful debugging move when the graph behaves differently on the second or third loop than it does on the first.
from typing import TypedDict, Annotated
from operator import add

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver


class AgentState(TypedDict):
    messages: Annotated[list[str], add]
    iterations: int


def think(state: AgentState) -> dict:
    return {
        "messages": [f"loop={state['iterations']}"],
        "iterations": state["iterations"] + 1,
    }


def route(state: AgentState) -> str:
    return "think" if state["iterations"] < 3 else END


graph = StateGraph(AgentState)
graph.add_node("think", think)
graph.add_edge(START, "think")
graph.add_conditional_edges("think", route)

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "debug-thread"}}
out = app.invoke({"messages": [], "iterations": 0}, config=config)
print(out)
  1. Replace your dummy node with a real LLM-backed agent step and keep the same debug pattern.
    The loop bug usually appears when the model keeps requesting tools because the termination condition is too weak or because tool results are not being written back into state correctly.
import os
from typing import TypedDict, Annotated

from operator import add
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.graph import StateGraph, START, END


class AgentState(TypedDict):
    messages: Annotated[list, add]
    iterations: int


llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)


def think(state: AgentState) -> dict:
    response = llm.invoke(state["messages"])
    print(f"[think] ai={response.content!r}")
    return {
        "messages": [response],
        "iterations": state["iterations"] + 1,
    }


def route(state: AgentState) -> str:
    last = state["messages"][-1]
    if isinstance(last, AIMessage) and "done" in last.content.lower():
        return END
    return "think"
  1. Add a hard stop guard so bad routing cannot spin forever during development.
    This is non-negotiable in production systems that call tools or external APIs; one broken branch should not burn tokens until timeout.
from typing import TypedDict, Annotated

from operator import add
from langgraph.graph import StateGraph, START, END


class AgentState(TypedDict):
    messages: Annotated[list[str], add]
    iterations: int


MAX_ITERS = 5


def think(state: AgentState) -> dict:
    return {
        "messages": [f"step-{state['iterations']}"],
        "iterations": state["iterations"] + 1,
    }


def route(state: AgentState) -> str:
    if state["iterations"] >= MAX_ITERS:
        print(f"[route] max iterations reached at {state['iterations']}")
        return END
    return "think"


graph = StateGraph(AgentState)
graph.add_node("think", think)
graph.add_edge(START, "think")
graph.add_conditional_edges("think", route)

app = graph.compile()
print(app.invoke({"messages": [], "iterations": 0}))

Testing It

Run the script and confirm three things:

  • You see node-level prints for every iteration.
  • The final output contains an incremented iterations count and accumulated messages.
  • The graph stops either because your condition returns END or because the hard stop triggers.

If you want to verify checkpointing, run with the same thread_id twice and compare how many steps are replayed versus recomputed. For loop bugs specifically, change the routing threshold from 3 to 999 and confirm your hard stop still prevents an infinite cycle.

Next Steps

  • Add interrupt_before=["tool_node"] or similar breakpoints when debugging tool-heavy graphs.
  • Use structured message/state schemas instead of raw dicts once your loop logic stabilizes.
  • Add LangSmith tracing so you can inspect branching decisions across multiple runs.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides