Technical guide

Debugging LangChain agents: a field guide

An agent that misbehaves rarely fails on one line you can put a breakpoint on. It fails across a loop of model calls and tool calls, often only with real data, sometimes only on the fortieth turn of a thread. This is the toolbox we use on the LangChain stack that runs every Olano agent, in the order we reach for it, with the symptom each tool is best at.

Start by deciding which layer is wrong

A LangChain agent has three places a bug can live, and the first useful question is which one. The model layer (LangChain chat models) turns messages into a reply or tool calls: wrong answers, malformed tool arguments and provider errors live here. The runtime (LangGraph) decides which step runs next and saves state: loops, lost memory, steps running twice and resumes that start from the wrong place live here. The harness (Deep Agents, or your own middleware) adds planning, files, subagents and summarisation: an agent that forgets the start of a session, or a subagent that never reports back, lives here.

Each tool below answers a different one of those questions. Using the right one first saves an afternoon.

1. Turn on debug output

The cheapest look inside is LangChain's global debug flag. It prints every run's inputs and outputs - each model call with the exact messages sent, each tool call with its arguments and result - as they happen:

from langchain_core.globals import set_debug, set_verbose

set_debug(True)     # every run, full inputs and outputs
# set_verbose(True) # a quieter summary

It is noisy, and that is the point: it shows the messages the model actually received, which is frequently not what you thought you sent. A system prompt that was replaced rather than appended, a tool result that was truncated, a message from an earlier turn that should not be there - all visible in the first screen. Switch it off before production; it logs everything, including whatever personal data is in the conversation.

2. Read a trace

For anything beyond a single run, a trace tree beats a log. LangSmith, LangChain's hosted tracing service, records every run as a nested tree you can expand: the graph step, the model call inside it, the tool call it produced, the model call after that. It is switched on with environment variables, no code change:

LANGSMITH_TRACING=true
LANGSMITH_API_KEY=...
LANGSMITH_PROJECT=my-agent

A trace answers questions logs cannot: how many model calls one user message really caused, which step was slow, what the token count was on the call that hit the context limit. Two cautions. Traces contain the full conversation, so a production tenant's data goes to wherever the traces are stored; decide that deliberately. And a trace shows what happened, not why the model decided it - for that you still need to read the messages.

If you would rather keep data in-house, a callback handler gives you the same events locally. Subclass BaseCallbackHandler, implement the on_chat_model_start, on_llm_end, on_tool_start and on_tool_end hooks you care about, and pass it in the run's config["callbacks"].

3. Stream in the right mode

LangGraph can stream a run in several modes, and each one is a different debugger. updates yields what each node changed, step by step - the clearest view of the loop. values yields the whole state after every step, which shows when a field went wrong. messages yields model tokens as they are generated, plus every other message type the graph produces. debug yields everything the runtime knows: task starts, results and checkpoints.

for mode, chunk in graph.stream(inputs, config, stream_mode=["updates", "debug"]):
    print(mode, chunk)

For a loop that will not end, updates is usually enough: you watch the same node fire with the same tool call until you see why the model keeps choosing it. For a step that runs twice, debug shows whether the runtime scheduled it twice or a resume replayed it.

Symptom to first tool

Where we look first, by what the agent did.

SymptomLikely layerLook first at
Wrong or odd answerModelDebug output: the exact messages sent, especially the system prompt and the last tool result.
Malformed tool argumentsModelThe tool's schema as the model saw it, and the raw tool call in the trace.
Calls the same tool foreverRuntime and modelupdates stream mode, then the tool result the model keeps reacting to.
Forgets earlier turnsRuntime or harnessSaved state for the thread, then whether summarisation has fired.
Text in the reply that should not be thereRuntimeThe streaming guide: a nested model call leaking into the stream.
Breaks after changing modelModelThe provider-switching guide: old content blocks the new provider rejects.
SlowAnyThe trace's timings per step, and the import cost of provider packages at start-up.

4. Read the saved state, and travel back in it

If the graph has a checkpointer, the state after every step is on disk, and you can inspect it after the fact - including for a run that happened in production yesterday. graph.get_state(config) returns the latest state for a thread; graph.get_state_history(config) walks back through every checkpoint, newest first, with the step that produced each.

config = {"configurable": {"thread_id": "customer-4711"}}
for snap in graph.get_state_history(config):
    print(snap.config["configurable"]["checkpoint_id"], snap.next,
          len(snap.values.get("messages", [])))

The message count per step is a fast way to find where memory went: if it drops sharply, something summarised or trimmed the conversation there. To replay from a point, pass that checkpoint's checkpoint_id in the config and invoke again; LangGraph forks a new branch from it instead of overwriting history. update_state lets you edit the state at a checkpoint first - useful for testing whether a different tool result would have changed the model's next move. The checkpointer guide covers what goes wrong with the storage itself.

5. Know where the loop limit really is

A raw LangGraph graph stops after 25 steps by default and raises GraphRecursionError, with a message telling you to raise the recursion_limit config key. That default catches runaway loops early. But the prebuilt agents set it far higher: both LangChain's create_agent and Deep Agents' create_deep_agent configure a limit of 9,999, because a genuine multi-step task can take hundreds of steps. So on an agent built with either, a model stuck calling the same tool will run - and bill - for a very long time before anything stops it.

Do not lower the recursion limit to fix that; it cuts off legitimate long jobs too. Use the purpose-built middleware instead: ModelCallLimitMiddleware and ToolCallLimitMiddleware cap calls per run or per thread, and can end the run gracefully with a message rather than an exception. In Olano, a loop breaker watches for the same failing call repeated and stops the turn with an explanation, and background runs carry a step budget and a timeout that move together, since raising one without the other just turns one kind of stop into the other.

6. Reproduce it offline with a fake model

A bug you can only trigger against a paid API, with a model that answers differently each time, is a bug you cannot fix with confidence. LangChain ships fake chat models for exactly this. GenericFakeChatModel returns the messages you give it, in order, tool calls included, and streams them in chunks like a real model:

from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage

model = GenericFakeChatModel(messages=iter([
    AIMessage(content="", tool_calls=[{"name": "search", "args": {"q": "x"}, "id": "1"}]),
    AIMessage(content="Found it."),
]))

Script the exact sequence that triggered the bug - the tool call, the odd result, the reply - and the runtime, middleware and streaming code all run for real against it, deterministically and free. We keep an offline fake model in Olano for the same reason: it makes agent-loop performance measurable and lets end-to-end tests run with no provider key at all.

A habit worth more than any tool

Write the reproduction down before the fix. A failing test with a fake model and a scripted sequence is the difference between "we fixed it" and "it has not happened again yet". Many of the bugs in the rest of this series came back once after being fixed by hand; none came back after being pinned by a test.

FAQ

How do I see what a LangChain agent is sending to the model?

Call set_debug(True) from langchain_core.globals before running the agent. It prints every run's inputs and outputs, including the exact list of messages each model call received. For a structured view across many runs, enable LangSmith tracing with the LANGSMITH_TRACING and LANGSMITH_API_KEY environment variables, or attach your own callback handler.

Why does my LangGraph agent raise GraphRecursionError?

The graph took more steps than its recursion_limit allows: 25 by default for a raw LangGraph graph. Either the task genuinely needs more steps, in which case raise the limit in the run's config, or the agent is looping, in which case stream in updates mode to see which node repeats and why the model keeps choosing it.

Why does my agent loop forever without hitting the recursion limit?

Agents built with LangChain's create_agent or Deep Agents' create_deep_agent set the recursion limit to 9,999, so a stuck loop can run for a long time. Add ModelCallLimitMiddleware or ToolCallLimitMiddleware to cap calls per run or per thread instead of lowering the recursion limit.

How can I test a LangChain agent without calling a real model?

Use GenericFakeChatModel from langchain_core.language_models.fake_chat_models. Give it the sequence of AI messages you want, including tool calls, and the rest of the agent - the graph, middleware, tools and streaming - runs for real against it, deterministically and offline.

Can I inspect what an agent did in a past conversation?

Yes, if the graph has a checkpointer. get_state returns a thread's latest state, and get_state_history walks every saved checkpoint for it. Passing an older checkpoint_id in the config replays from that point as a new branch, leaving the original history intact.

Related reading

The rest of the LangChain series, and where to go next.

Checked 13 October 2026 against LangChain 1.4, langchain-core 1.6, LangGraph 1.2 and Deep Agents 0.7. Olano is an independent company that builds on LangChain's open-source libraries; it is not affiliated with or endorsed by LangChain, Inc. Library behaviour changes between releases, and the LangChain documentation is the primary source. If we have described something wrongly, write to support@olano.ai and we will correct this page.

Stuck on an agent that will not behave?

Tell us what it does, what it should do and which stack it runs on. We will say plainly whether it is a model, runtime or harness problem, and whether running it on Olano would make it go away.

Discuss a project