LangGraph
LangGraph is a low-level orchestration framework for building stateful, graph-based agent workflows. You compose agents from StateGraph nodes and edges, with full control over routing, state, and tool execution.
The deepeval integration traces LangGraph runs through LangChain's CallbackHandler, which you pass into your graph's runtime config. Every graph run, node, model call, tool call, and nested step becomes a span you can inspect, without rewriting your LangGraph app.
deepeval's LangGraph integration enables you to:
- Trace any LangGraph run β pass a
CallbackHandlerthrough the graph's callbacks config per call. - Evaluate the complete ordered agent trajectory β score the sequence across all applicable graph, agent, LLM, and tool spans.
- Evaluate traces or model / agent components with
deepevalmetrics. - Run evals from scripts or CI/CD β same callback, different surfaces.
- Customize trace and span data through callback kwargs and
next_*_spanstaging.
Getting Started
Installation
pip install -U deepeval langgraph langchain-openaiLangGraph uses LangChain's callback system, so the deepeval integration is per-call. You decide which graph runs are traced by passing CallbackHandler(...) into the graph config.
Instrument and evaluate
Wire your StateGraph (LangGraph's core abstraction), then pass the callback handler to the invocation you want to evaluate.
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from deepeval.integrations.langchain import CallbackHandler
from deepeval.dataset import EvaluationDataset, Golden
from deepeval.metrics import TaskCompletionMetric
def get_weather(city: str) -> str:
"""Return the weather in a city."""
return f"It's always sunny in {city}!"
llm = init_chat_model("openai:gpt-4o-mini").bind_tools([get_weather])
def chatbot(state: MessagesState):
return {"messages": [llm.invoke(state["messages"])]}
graph = (
StateGraph(MessagesState)
.add_node(chatbot)
.add_node("tools", ToolNode([get_weather]))
.add_edge(START, "chatbot")
.add_conditional_edges("chatbot", tools_condition)
.add_edge("tools", "chatbot")
.compile()
)
# Goldens are the inputs you want to evaluate.
dataset = EvaluationDataset(goldens=[Golden(input="What is the weather in Paris?")])
# The `TaskCompletionMetric` is passed into the `evals_iterator`.
for golden in dataset.evals_iterator(metrics=[TaskCompletionMetric()]):
graph.invoke(
{"messages": [{"role": "user", "content": golden.input}]},
config={"callbacks": [CallbackHandler()]},
)Done β
. You've run your first eval with full traceability into LangGraph via deepeval.
What gets traced
Each LangGraph run that receives a CallbackHandler produces a trace β the end-to-end unit your user observes. Inside that trace are component spans for each callback LangGraph emits through LangChain:
- Agent span β the compiled
StateGraphinvoke(...)call (the root of the tree). Nested graph nodes stay custom spans. - LLM spans β chat model and completion calls inside a node.
- Tool spans β tool calls executed by
ToolNode(or your own). - Retriever spans β retriever calls, when your graph uses retrieval.
Trace β what the user observes
βββ Agent: weather_graph β one graph invoke(...) call
βββ Node: chatbot β model picks a tool
β βββ LLM: gpt-4o-mini
βββ Node: tools β ToolNode runs the tool
β βββ Tool: get_weather
βββ Node: chatbot β model writes the final answer
βββ LLM: gpt-4o-miniThe trace and its component spans are independently evaluable.
Running evals
There are two surfaces for running evals against a LangGraph app. Pick by where you want results to surface β your terminal during development, or your CI pipeline as a pass/fail gate.
In CI/CD
Use the deepeval pytest integration. Each parametrized test invocation becomes one LangGraph run; failing metrics fail the test, which fails the build.
import pytest
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from deepeval import assert_test
from deepeval.integrations.langchain import CallbackHandler
from deepeval.dataset import EvaluationDataset, Golden
from deepeval.metrics import TaskCompletionMetric
def get_weather(city: str) -> str:
"""Return the weather in a city."""
return f"It's always sunny in {city}!"
llm = init_chat_model("openai:gpt-4o-mini").bind_tools([get_weather])
def chatbot(state: MessagesState):
return {"messages": [llm.invoke(state["messages"])]}
graph = (
StateGraph(MessagesState)
.add_node(chatbot)
.add_node("tools", ToolNode([get_weather]))
.add_edge(START, "chatbot")
.add_conditional_edges("chatbot", tools_condition)
.add_edge("tools", "chatbot")
.compile()
)
dataset = EvaluationDataset(goldens=[
Golden(input="What is the weather in Paris?"),
Golden(input="What is the weather in London?"),
])
@pytest.mark.parametrize("golden", dataset.goldens)
def test_langgraph_agent(golden: Golden):
graph.invoke(
{"messages": [{"role": "user", "content": golden.input}]},
config={"callbacks": [CallbackHandler()]},
)
assert_test(golden=golden, metrics=[TaskCompletionMetric()])Run it with:
deepeval test run test_langgraph_agent.pyIn a script
Use EvaluationDataset + evals_iterator. Each Golden becomes one LangGraph run, and metrics passed to the iterator score the resulting trace end-to-end.
dataset = EvaluationDataset(goldens=[
Golden(input="What is the weather in Paris?"),
Golden(input="What is the weather in London?"),
])
for golden in dataset.evals_iterator(metrics=[TaskCompletionMetric()]):
graph.invoke(
{"messages": [{"role": "user", "content": golden.input}]},
config={"callbacks": [CallbackHandler()]},
)Evaluate agent trajectories
Trajectory evaluation scores the complete ordered trace: the graph's routing decisions, model calls, tool use, and other applicable spans in the sequence they occurred. This is distinct from evaluating only the final output or attaching a metric to an individual component span. Custom graph and node spans remain part of that ordered trajectory, so graph-specific orchestration is preserved in the evaluation scope.
Pass trajectory metrics to evals_iterator so each instrumented LangGraph run is evaluated at trace scope:
from deepeval.metrics import TaskCompletionMetric, StepEfficiencyMetric, PlanAdherenceMetric
from deepeval.integrations.langchain import CallbackHandler
from deepeval.dataset import EvaluationDataset, Golden
...
dataset = EvaluationDataset(goldens=[Golden(input="What is the weather in Paris?")])
metrics = [TaskCompletionMetric(), StepEfficiencyMetric(), PlanAdherenceMetric()]
for golden in dataset.evals_iterator(metrics=metrics):
graph.invoke(
{"messages": [{"role": "user", "content": golden.input}]},
config={"callbacks": [CallbackHandler()]},
)See the trajectory evaluation guide for metric selection and interpretation. To score one step instead of the ordered run, apply metrics to components next.
Applying metrics to components
Passing metrics to evals_iterator evaluates the overall LangGraph run. To evaluate a component instead, stage metrics onto the next span the callback opens.
Agent spans
Wrap the graph invocation in next_agent_span. The callback drains the staged metric onto the root agent span opened by invoke(...) β useful when you want a span-level score on the graph run itself rather than the whole trace.
from deepeval.integrations.langchain import CallbackHandler
from deepeval.metrics import TaskCompletionMetric
from deepeval.tracing import next_agent_span
...
for golden in dataset.evals_iterator():
with next_agent_span(metrics=[TaskCompletionMetric()]):
graph.invoke(
{"messages": [{"role": "user", "content": golden.input}]},
config={"callbacks": [CallbackHandler()]},
)The same one-shot semantic as the LLM helper applies: only the root agent span picks up the staged metric.
LLM calls
Wrap the graph invocation in next_llm_span. The callback drains the staged metric onto the first LLM span the graph emits; later LLM calls on subsequent loop turns get nothing.
from deepeval.integrations.langchain import CallbackHandler
from deepeval.metrics import AnswerRelevancyMetric
from deepeval.tracing import next_llm_span
...
for golden in dataset.evals_iterator():
with next_llm_span(metrics=[AnswerRelevancyMetric()]):
graph.invoke(
{"messages": [{"role": "user", "content": golden.input}]},
config={"callbacks": [CallbackHandler()]},
)For deterministic tool calls, use tool spans for traceability, inputs, outputs, and metadata. Avoid attaching metrics directly to tool spans.
Customizing trace and span data
LangGraph is instrumented per-call through LangChain callbacks, so customization happens at the callback or span-staging boundary.
- Use
CallbackHandlerkwargs for trace-level defaults likename,tags,metadata,thread_id, anduser_id. - Use
next_agent_span/next_llm_span/next_retriever_span/next_tool_spanto stage component-level fields onto the next span the callback opens. - Use tool spans for deterministic traceability, inputs, outputs, and metadata.
callback = CallbackHandler(
name="weather-graph",
tags=["langgraph", "weather"],
metadata={"team": "support"},
user_id="user-123",
)
graph.invoke(
{"messages": [{"role": "user", "content": "What is the weather in Paris?"}]},
config={"callbacks": [callback]},
)Advanced patterns
The primitives above β the callback handler and next_*_span β compose around one boundary: LangGraph owns the graph execution lifecycle, and your code chooses where to stage component config for the next span the callback opens.
Evaluate a node instead of the whole graph
Staging a metric applies it to the next matching span the callback opens during the graph run. Use this when you want to evaluate a subagent node or model step instead of the full graph. Pick the helper that matches the span you want to score.
from deepeval.integrations.langchain import CallbackHandler
from deepeval.metrics import TaskCompletionMetric
from deepeval.tracing import next_agent_span
...
def run_graph(prompt: str):
with next_agent_span(metrics=[TaskCompletionMetric()]):
return graph.invoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"callbacks": [CallbackHandler()]},
)No trace-level metrics required
Trace-level metrics are end-to-end metrics: they score the whole trace. They are not strictly necessary here because the TaskCompletionMetric is staged for the agent span, so CI/CD and scripts only need to run the graph inside the staging block.
This is how you'd run it:
import pytest
from deepeval import assert_test
...
@pytest.mark.parametrize("golden", dataset.goldens)
def test_component_metrics(golden: Golden):
run_graph(golden.input)
assert_test(golden=golden)deepeval test run test_langgraph_agent.py...
for golden in dataset.evals_iterator():
run_graph(golden.input)Wrap a LangGraph run in an observed function
When the LangGraph call is part of a larger operation, observe the outer function. LangGraph spans nest under your observed span when the callback runs inside it.
from deepeval.tracing import observe
...
@observe(name="respond_to_user")
def respond_to_user(prompt: str):
return graph.invoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"callbacks": [CallbackHandler()]},
)Tracing a LangGraph server deployment
When you deploy with the LangGraph server, the server invokes your graph. So instead you can attach the handler to the compiled graph and export that. The callback then applies to every run the server executes.
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from deepeval.integrations.langchain import CallbackHandler
...
graph = (
StateGraph(MessagesState)
.add_node(chatbot)
.add_node("tools", ToolNode([get_weather]))
.add_edge(START, "chatbot")
.add_conditional_edges("chatbot", tools_condition)
.add_edge("tools", "chatbot")
.compile()
.with_config(callbacks=[CallbackHandler()]) # baked into every server run
)Point langgraph.json at the exported graph:
{
"dependencies": ["."],
"graphs": { "agent": "./langgraph_agent.py:graph" },
"env": ".env"
}Then start the server β every request it runs against the graph is traced:
pip install -U "langgraph-cli[inmem]"
langgraph devAPI reference
CallbackHandler accepts the following trace-level kwargs. Each one is a default for runs that use that callback.
| Kwarg | Type | Description |
|---|---|---|
name | str | Default trace name. |
tags | list[str] | Tags applied to traces produced by this callback. |
metadata | dict | Trace metadata applied when the callback starts a trace. |
thread_id | str | Groups related runs into a single trace thread. |
user_id | str | Actor identifier for the trace. |
metrics | list | Trace-level metrics for the run. Prefer evals_iterator in eval scripts. |
metric_collection | str | Trace-level metric collection, for online evals on live traffic. |
test_case_id | str | Optional test case identifier. |
turn_id | str | Optional turn identifier for conversational traces. |
For native tracing helpers (@observe, update_current_trace, update_current_span) see the tracing reference.
FAQs
Can I evaluate a sub-agent node inside my graph?
graph.invoke(...) in next_agent_span and the callback drains the metric onto the agent span that sub-agent node emits β scoring it on its own. It's one-shot per run, so for every loop turn either drive the loop yourself or score end-to-end with trace-level metrics.Can I fail CI when a LangGraph metric regresses?
CallbackHandler() into the graph config inside a parametrized pytest test and assert with assert_test(...) under deepeval test run.Where do my LangGraph traces show up beyond the console?
deepeval login and Confident AI visualizes the full graph trace β every node, model call, and tool call as nested spans β with their scores in a shared cloud UI. It's optional.How do I trace a LangGraph server deployment?
invoke call of your own to pass the callback to. Attach it to the compiled graph with with_config(callbacks=[...]) and point langgraph.json at that exported graph β every server run is then traced.Can I keep evaluating a deployed LangGraph app in production?
thread_id; logged into Confident AI those live traces power online evals on real traffic.