n4nAI

How to debug a LangGraph workflow with LangSmith

A practical step-by-step guide to debug LangGraph with LangSmith: configure tracing, reproduce loops, inspect spans, fix state bugs, and verify.

n4n Team3 min read638 words

Audio narration

Coming soon — every post will get a voice note here.

LangGraph makes it easy to build stateful agent loops, but when a workflow silently drops state or loops forever, you need visibility into every node and LLM call. To debug LangGraph with LangSmith, you wire tracing into your graph and read the execution tree like a distributed systems trace. This article gives you the exact steps to instrument a graph, reproduce a failure, and confirm the fix.

Step 1: Set up the LangSmith environment

LangSmith tracing is opt-in via environment variables. You don’t need to modify graph code to get basic spans—just export the keys and install the packages.

export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=ls-your-key-here
export LANGCHAIN_PROJECT=langgraph-debug-demo
pip install langgraph langchain-openai langsmith

The LANGCHAIN_PROJECT variable groups runs so you can filter them in the UI. If you skip it, traces land in the default project.

Step 2: Build a LangGraph workflow with a realistic bug

We’ll define a small agent that calls a weather tool. The tool_node below contains a deliberate mistake: it returns a plain string instead of a ToolMessage, so the agent never sees the tool result and loops.

from langgraph.graph import StateGraph, END, MessagesState
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Return weather for a city."""
    return f"Sunny in {city}"

tools = [get_weather]
model = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools)

def agent(state: MessagesState):
    response = model.invoke(state["messages"])
    return {"messages": [response]}

def tool_node(state: MessagesState):
    # Bug: returns a static string, not ToolMessages
    return {"messages": ["Tool ran"]}

def should_continue(state: MessagesState):
    last = state["messages"][-1]
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tool"
    return END

graph = StateGraph(MessagesState)
graph.add_node("agent", agent)
graph.add_node("tool", tool_node)
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tool", "agent")
app = graph.compile()

This graph compiles fine. The bug only surfaces at runtime.

Step 3: Run the graph with tracing enabled

Invoke the graph with a user question. Because the env vars are set, every node and LLM call is sent to LangSmith automatically.

result = app.invoke({"messages": [("user", "What's the weather in SF?")]})

If you’re scripting this, wrap the call in a try/except—LangGraph raises GraphRecursionError after the default 25-step limit, which is itself a signal of a loop.

Step 4: Inspect the trace in LangSmith

Open the LangSmith project you set in Step 1. Each graph invocation appears as a root run. Expand it to see child spans for:

  • The agent node (including the underlying ChatOpenAI span with prompt, completion, token counts).
  • The tool node.
  • The conditional edge logic.

When you debug LangGraph with LangSmith, the critical view is the sequence of agenttoolagent spans. In our buggy version, you’ll see the same tool_calls from the LLM repeated across iterations because the tool node never returned a valid result. The trace makes the loop obvious: identical LLM inputs on iterations 2–5, then a recursion error.

Step 5: Diagnose common failure modes

Infinite loops from broken tool returns

The fix is to emit ToolMessage objects with the correct tool_call_id. Replace tool_node:

from langchain_core.messages import ToolMessage

def tool_node(state: MessagesState):
    messages = []
    for call in state["messages"][-1].tool_calls:
        output = get_weather.invoke(call["args"])
        messages.append(ToolMessage(content=output, tool_call_id=call["id"]))
    return {"messages": messages}

After this change, the trace shows one agent call, one tool call, then END.

Silent state mutations

LangGraph merges the dict you return into the state; it does not read mutations to the input state object. If a node modifies state["messages"] in place and returns nothing, the trace shows no state change and the bug is invisible. Always return a partial update.

Tool schema mismatches

If the LLM sends arguments that don’t match the @tool function signature, LangSmith shows a failed tool span with a validation error. Inspect the tool_calls arguments in the agent span to confirm types.

Provider errors and fallbacks

If you point ChatOpenAI at an OpenAI-compatible gateway, the LLM span records the base URL and status. For example, routing through n4n.ai’s single endpoint that addresses 240+ models gives you automatic fallback when a provider is rate-limited or degraded; the LangSmith span still shows a single LLM call with standard token fields, so a backend failover doesn’t masquerade as a graph logic bug.

model = ChatOpenAI(
    model="gpt-4o-mini",
    base_url="https://api.n4n.ai/v1",
    api_key="your-n4n-key"
)

Step 6: Add local debugging aids

When the LangSmith UI is too slow for tight loops, enable verbose logging:

import langchain
langchain.debug = True

This streams node inputs, outputs, and LLM prompts to stdout. You can also attach a handler explicitly if env vars aren’t available:

from langsmith import LangSmithCallbackHandler
handler = LangSmithCallbackHandler(project_name="local-debug")
app.invoke({"messages": [("user", "Hi")]}, config={"callbacks": [handler]})

Use this to capture a single run without polluting your main project.

Step 7: Verify the fix

Re-run the original invocation. In LangSmith, confirm the trace has exactly one agent span, one tool span, and no recursion error. Then lock the behavior with a test:

def test_graph_terminates():
    res = app.invoke({"messages": [("user", "Weather in SF?")]})
    assert "Sunny" in res["messages"][-1].content
    # Ensure no repeated tool calls
    tool_msgs = [m for m in res["messages"] if m.type == "tool"]
    assert len(tool_msgs) == 1

Run pytest. Green test plus a clean LangSmith trace means you have successfully debugged LangGraph with LangSmith. For regression coverage, export the failing trace to a LangSmith dataset and add it to CI.

Tagslanggraphlangsmithdebugging

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langgraph for agent workflows posts →