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
agentnode (including the underlying ChatOpenAI span with prompt, completion, token counts). - The
toolnode. - The conditional edge logic.
When you debug LangGraph with LangSmith, the critical view is the sequence of agent → tool → agent 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.