Agent Observability
Overview
Production agents fail differently than services: bad tool calls, hallucinated arguments, runaway loops, silent quality regression. This skill picks an observability backend, instruments node-level spans, attaches evals to traces, and sets up replay — so failures are observable, not just guessed at.
When to use
- Agent works in dev, breaks in prod with no logs that explain why
- Need to compare prompt/model changes against a baseline (eval-in-trace)
- Token/latency cost is growing and you don't know which node is the culprit
- A user reports a bad answer and you need to replay the exact session
- Multiple agents/subagents — you need a single trace tree, not interleaved logs
Platform selection
| Backend |
Pick when |
Hosting |
| LangSmith |
LangChain/LangGraph stack, want managed |
SaaS |
| Langfuse |
Open-source, self-host required, multi-framework |
SaaS or self-host |
| Arize Phoenix |
OTel-native, embed/eval drift, OSS-first |
Local or self-host |
Default: Langfuse when you need self-host, LangSmith when you're already on LangGraph, Phoenix when OTel is mandated.
Instrumentation pattern (node-level spans)
# LangGraph + Langfuse
from langfuse.decorators import observe
from langfuse.openai import openai # auto-traces tool calls
@observe(name="planner_node")
def planner(state):
return {"plan": llm.invoke(state["task"])}
@observe(name="tool_executor")
def tool_executor(state):
return {"observation": run_tool(state["action"])}
Required span attributes:
input / output (full, not truncated)
model, temperature, max_tokens
tool_name, tool_args, tool_result_status
tokens_in, tokens_out, cost_usd
session_id, user_id, trace_id
Eval-in-trace
Attach automated graders to each span so regressions surface in the same UI as latency:
from langfuse import Langfuse
langfuse = Langfuse()
langfuse.score(
trace_id=trace_id,
name="answer_correctness",
value=0.92,
comment="LLM-as-judge vs golden"
)
Common scores: correctness, tool_call_validity, groundedness, harm, latency_sla.
Replay pattern
- Log full
state at every node entry/exit (Langfuse: metadata={"state": state})
- On bug report, fetch trace by
trace_id
- Rehydrate state, re-run from any node — diff outputs
Sampling at scale
- 100% trace error/HITL paths
- 10% sample happy path
- Tail-based sampling for spans > p95 latency
- Always log: tool failures, guardrail blocks, budget caps hit
Further reading
- LangSmith docs — datasets, evals, trace replay
- Langfuse docs — self-host compose, OTel exporter
- Arize Phoenix — embed drift, OSS LLM evals
- OpenTelemetry
gen_ai semantic conventions (2026)
1---2name: agent-observability3description: Instrument LLM agents with traces, metrics, and replay. Use when an agent in production is silently failing, regressing, drifting, or burning tokens. Selects LangSmith / Langfuse / Phoenix, defines node-level spans, attaches evals to traces, and enables session replay.4---56# Agent Observability78## Overview910Production agents fail differently than services: bad tool calls, hallucinated arguments, runaway loops, silent quality regression. This skill picks an observability backend, instruments node-level spans, attaches evals to traces, and sets up replay — so failures are *observable*, not just guessed at.1112## When to use1314- Agent works in dev, breaks in prod with no logs that explain why15- Need to compare prompt/model changes against a baseline (eval-in-trace)16- Token/latency cost is growing and you don't know which node is the culprit17- A user reports a bad answer and you need to *replay* the exact session18- Multiple agents/subagents — you need a single trace tree, not interleaved logs1920## Platform selection2122| Backend | Pick when | Hosting |23|---------|-----------|---------|24| LangSmith | LangChain/LangGraph stack, want managed | SaaS |25| Langfuse | Open-source, self-host required, multi-framework | SaaS or self-host |26| Arize Phoenix | OTel-native, embed/eval drift, OSS-first | Local or self-host |2728Default: **Langfuse** when you need self-host, **LangSmith** when you're already on LangGraph, **Phoenix** when OTel is mandated.2930## Instrumentation pattern (node-level spans)3132```python33# LangGraph + Langfuse34from langfuse.decorators import observe35from langfuse.openai import openai # auto-traces tool calls3637@observe(name="planner_node")38def planner(state):39 return {"plan": llm.invoke(state["task"])}4041@observe(name="tool_executor")42def tool_executor(state):43 return {"observation": run_tool(state["action"])}44```4546Required span attributes:47- `input` / `output` (full, not truncated)48- `model`, `temperature`, `max_tokens`49- `tool_name`, `tool_args`, `tool_result_status`50- `tokens_in`, `tokens_out`, `cost_usd`51- `session_id`, `user_id`, `trace_id`5253## Eval-in-trace5455Attach automated graders to each span so regressions surface in the same UI as latency:5657```python58from langfuse import Langfuse59langfuse = Langfuse()60langfuse.score(61 trace_id=trace_id,62 name="answer_correctness",63 value=0.92,64 comment="LLM-as-judge vs golden"65)66```6768Common scores: `correctness`, `tool_call_validity`, `groundedness`, `harm`, `latency_sla`.6970## Replay pattern71721. Log full `state` at every node entry/exit (Langfuse: `metadata={"state": state}`)732. On bug report, fetch trace by `trace_id`743. Rehydrate state, re-run from any node — diff outputs7576## Sampling at scale7778- 100% trace error/HITL paths79- 10% sample happy path80- Tail-based sampling for spans > p95 latency81- Always log: tool failures, guardrail blocks, budget caps hit8283## Further reading8485- LangSmith docs — datasets, evals, trace replay86- Langfuse docs — self-host compose, OTel exporter87- Arize Phoenix — embed drift, OSS LLM evals88- OpenTelemetry `gen_ai` semantic conventions (2026)