LangGraph Tutorial (Python): debugging agent loops for advanced developers
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
- •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)
- •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
- •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)
- •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"
- •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
iterationscount and accumulated messages. - •The graph stops either because your condition returns
ENDor 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
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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