Skip to content

Reliability, Evaluation and Safety

Observability and tracing

Seeing inside the agent

When an agent misbehaves it rarely throws a stack trace. It just quietly does the wrong thing across ten steps. You cannot debug, cost, or improve what you cannot see. This lesson makes an agent run legible: the trace, the span, the OpenTelemetry standard that names them, and how to read a waterfall to spot a bug in seconds.

Why agents need their own observability

Traditional monitoring (APM) assumes deterministic code paths and error codes: a request either returns 200 or throws. Agents break both assumptions. They are non-deterministic, so the same input can produce a different sequence of tool calls. They run a multi-step, stateful loop of reason, act, observe, reason again. And their worst failures are silent: goal drift, context loss, and quality degradation emit no error code at all.[9]

5xx alerts are necessary but nowhere near sufficient

Alerting on exceptions catches crashes. It does not catch an agent that confidently returns a plausible-but-wrong answer, loops on a redundant search, or slowly blows its token budget. Those only surface when you read what the agent actually did, step by step, over real runs.

So the unit of observability is not the log line, it is the . Hamel Husain's single most contrarian piece of advice: get data flowing first, "it doesn't matter whether you use an observability tool or just log to files," because you cannot do error analysis or write evals on runs you never recorded.[1]

Trace and span: the two words that matter

A trace is one whole agent run. It is a tree of , each one a unit of work with a start time, an end time, and a bag of attributes. For agents, three span types cover almost everything: the agent turn, an LLM call, and a tool call. Every span records the same six load-bearing facts, and reading a trace is mostly a matter of scanning them.

Fact on every spanWhat it tells you
input tokensThe prompt this call saw. It grows every step as history accumulates, so it is your context and cost meter.
output tokensWhat the model produced this call.
finish_reasonThe model's intent: tool_calls (wants to act), stop (done), length (truncated mid-thought).
latencyStart to end of this span.
modelWhich model served the call, for example claude-sonnet-4.
costTokens times price, rolled up over the run.
Anatomy of one span. Roll these six facts up over a run and you have latency budgets, a cost meter, and a debuggable history.

Two attributes do outsized work. finish_reason tells you the model's intent: tool_calls means "I want to act" (an execute_tool span should follow); stop means "I'm done"; length means it was truncated mid-thought. And the per-span input token count is your context and cost meter: it grows every step as history accumulates, so a sudden jump flags a bloated payload.

The standard: OpenTelemetry GenAI conventions

You do not want a bespoke logging format per vendor. (OTel) formed a GenAI Special Interest Group in April 2024 and now defines semantic conventions for agent telemetry: a shared vocabulary of span names and attributes.[2] This is the vendor-neutral wire format that Langfuse, Arize Phoenix, Datadog, Honeycomb, and New Relic all consume: instrument once, view anywhere.

The canonical agent trace is a three-level tree. Learn this shape; it is the mental model for every tool in this lesson.

Tap a node to see what it does.

The OTel GenAI span tree. A root invoke_agent span nests alternating chat and execute_tool children, literally the reason-act loop, recorded.

The attribute gen_ai.operation.name is the discriminator that names each span. The rest are the facts above, with exact standardized keys:[3]

Attribute / metricMeaning
gen_ai.request.modelModel requested (for example gpt-4o, claude-sonnet-4)
gen_ai.usage.input_tokensPrompt / input token count for that call
gen_ai.usage.output_tokensCompletion / output token count
gen_ai.response.finish_reasonsWhy generation stopped: stop, tool_calls, length
gen_ai.input.messages / output.messagesThe actual content, opt-in only (may hold PII or secrets)
gen_ai.client.token.usageMetric: token histogram, split by input / output
gen_ai.client.operation.durationMetric: latency histogram, filterable by model
The gen_ai.* keys every OTel-speaking backend understands. Structure is always safe; content is gated.

Structural telemetry is safe; content is gated

The convention deliberately separates structure (token counts, latency, finish reasons, always safe to record) from content (the raw messages and tool arguments). Capturing gen_ai.input.messages can log customer data, API keys, and PII straight into your observability backend, so content capture is opt-in and should be redacted.[2] You often want it in dev, gated in prod.

A parallel spec, Arize's OpenInference, powers Phoenix and predates the OTel effort. The two are converging but not identical, worth knowing both names exist when you compare tools.[8]

# structural attributes (safe) - the OTel GenAI convention
{
  "gen_ai.operation.name":        "chat",
  "gen_ai.request.model":         "claude-sonnet-4",
  "gen_ai.usage.input_tokens":     2400,
  "gen_ai.usage.output_tokens":    88,
  "gen_ai.response.finish_reasons": ["tool_calls"],
  "start_time": 2.1, "end_time": 3.0   # -> 0.9s latency for this call
}
one_chat_span.json - how your backend stores a single chat span (structural attributes only)

What every agent span should capture

A span is only as useful as the attributes you attach to it. For agent work the high-value fields are consistent across the ecosystem, and capturing them is the difference between a trace you can debug and a pretty timeline you cannot.

  1. Inputs and outputs. The full prompt sent to the model (system, history, and retrieved context) and the raw completion it returned. This is the single most important thing to record and the one most often truncated away.
  2. Tool calls. The tool name, the exact arguments the model chose, and the raw result or error that came back. Most agent bugs are a wrong argument or a tool result the model then misreads.
  3. Model metadata. Model id, temperature and sampling settings, and the stop reason, so a run is reproducible and you can tell a truncation from a real stop.
  4. Tokens and cost. Prompt and completion token counts per call, which drive both the bill and the latency and let you find the step that is quietly blowing the budget.
  5. Status and latency. Success or error, the error type on failure, and wall-clock duration, so you can rank spans by what is slow or breaking.

Reading a trace waterfall

Every backend renders a trace the same way: a waterfall, spans laid out on a horizontal time axis, indented to show nesting. Width is duration; the tree is the agent's control flow. Here is one real-shaped run of a research agent answering "Compare the battery life of the top 3 flagship phones." Read top to bottom, and watch the input token count climb.

SpanIn -> Out tokensfinish_reasonDuration
invoke_agent research-agent (root)14.2k tok total, $0.06-7.8s
chat #11.15k -> 95tool_calls0.9s
execute_tool web_search "phone battery 2026"--1.2s
chat #22.4k -> 88tool_calls0.9s
execute_tool web_search "phone battery 2026" (duplicate args)--1.3s
chat #33.65k -> 120tool_calls0.9s
execute_tool fetch_page "gsmarena..."--1.2s
chat #4 final4.9k -> 210stop1.4s
A trace waterfall for one agent run. Input tokens climb every step (1.15k, 2.4k, 3.65k, 4.9k) as history accumulates: that curve is your live cost meter. The duplicate web_search is a bug.

Spotting the bug: a redundant loop

The trace makes the defect obvious. execute_tool #1 already ran web_search(q="phone battery 2026") and got results. Yet chat #2 read those results and re-proposed the identical search, so execute_tool #2 ran the same query again. That is a redundant loop: no step errored, but the model duplicated work it had already done.[9]

What the redundant call cost

+1.3s latency, pure dead time on the critical path. It bloats every later step: the duplicate results re-enter context, pushing chat #3's input to 3.65k and inflating cost on all downstream calls. Left unchecked at scale, this is exactly how agents hit 4x the tokens of chat, and token budget alone explained roughly 80% of performance variance in Anthropic's research system.[4]

What the trace tells you to fix

Cache or dedup tool calls: hash (tool_name, args) and return the prior result on a repeat. Loop detection: abort or warn when the same call recurs; pair it with a . Surface the result better: the model re-searched because the first result read as low-signal, a context-engineering and tool-interface fix, not a model swap.

This is the whole game. Anthropic found the same class of bug in their multi-agent system, subagents "duplicating work" and one query spawning "50 subagents," and the fix was the same: make the waste visible in traces, then constrain it.[4] A note of caution for framework users: agent frameworks "create extra layers of abstraction that can obscure the underlying prompts and responses, making them harder to debug,"[5] so keep the raw prompt and response visible in your trace (that is what content capture is for).

A reading checklist

Open any trace and run this scan. It turns a wall of spans into a diagnosis in under a minute.

  1. Start at the root. Total latency, total tokens, total cost, final finish_reason. Is the end-state actually correct?
  2. Scan the finish reasons. Every tool_calls should be followed by an execute_tool; a stray length means a truncated call that likely corrupted what came after.
  3. Follow the token curve. Input tokens should grow gently. A sudden jump is a bloated tool payload dumped into context (context rot).[6]
  4. Hunt for repeats. Identical (tool, args) spans are a redundant loop; the same error string recurring is an error cascade where one bad step silently corrupts every later step.
  5. Inspect the widest bars. The longest span owns your latency; the largest chat input owns your cost. Optimize those, ignore the rest.
  6. Read the messages on the suspect span. Only now open the (redacted) content to confirm the root cause.

The tooling landscape

Hamel's advice stands: do not build custom logging infra, observability is not your product, so adopt a platform.[1] Because they mostly speak OTel/OTLP now, the choice is about workflow fit, not lock-in. The five you will hear most, segmented by job-to-be-done:[8]

ToolOpen sourceBest fit
LangSmithNo (self-host = Enterprise)LangChain / LangGraph teams; Studio lets you set breakpoints, edit state mid-run, resume from a checkpoint
LangfuseYes (MIT)Framework-agnostic via OTel; prompt-centric workflows, evals, and cost analytics
Arize PhoenixYesOTel / OpenInference-native; strong drift and embedding analysis
W&B WeavePartlyTeams already living in Weights & Biases for experiment tracking
BraintrustNoEval-centric workflows where scoring is the main loop
Five platforms, one wire format. Because they mostly speak OTel, the choice is workflow fit, not lock-in.

Traces feed evals, in that order

Observability is not the goal, it is the substrate. Hamel's priority order is deliberate: get traces flowing, then do hands-on error analysis on 50+ real traces, then automate the top failure clusters with an LLM-as-judge. Error analysis is "the step most people skip and the most important," and it reveals an 80/20: most failures cluster in a few places.[1] Building an elaborate judge before reading traces optimizes the wrong thing.

Instrument from day one

Design observability in from the start: the goals are to minimize MTTD (mean time to detection) and MTTR (mean time to response); bolting it on after an incident is too late.[7] Emit invoke_agent / chat / execute_tool spans with token and finish-reason attributes on run #1, and treat cost per task as a first-class SLO: a runaway loop is also a runaway bill.[4] The next lesson turns these signals into guardrails.

Check yourself

Match each OTel GenAI span to what it records.

drop here

the root span for one whole agent turn

drop here

one LLM call, request and response

drop here

one tool invocation, args and result

In the OpenTelemetry GenAI conventions, which span represents a single LLM call?

Why do plain 5xx / exception alerts miss most real agent failures?

Recall the three-level OTel GenAI span tree, and one key attribute each level carries.

What is the discriminator, and what facts does each level record? Try to state it, then check.

Lock it in

  • The unit of agent observability is the trace, one run as a tree of spans, not the log line. Agents fail silently, so error alerts alone are blind.
  • Every span records six load-bearing facts: inputs, outputs, model, tokens, latency, cost, plus a finish_reason that reveals the model's intent.
  • The OpenTelemetry GenAI conventions standardize this: invoke_agent, chat, execute_tool with gen_ai.* attributes, so instrument once and view in any backend. Content capture is opt-in.
  • Read a waterfall by scanning finish reasons, the token curve, repeated (tool, args) spans, and the widest bars: a redundant loop or cascade jumps out in seconds.
  • Get traces flowing first, then error-analyze roughly 50 of them, then write evals. Do not build your own logging; adopt Langfuse, Phoenix, LangSmith, Weave, or Braintrust.

Primary source

OpenTelemetry, Inside the LLM Call: GenAI Observability with OpenTelemetry

The clearest walkthrough of the invoke_agent / chat / execute_tool span tree, the exact gen_ai.* attributes and metrics, and why content capture is gated. Pair it with Hamel Husain's "Observability in LLM Applications" for the traces-first mindset.

Sources

  1. 1.Hamel Husain, Observability in LLM Applications
  2. 2.OpenTelemetry, Inside the LLM Call: GenAI Observability with OpenTelemetry
  3. 3.OpenTelemetry, Semantic conventions for GenAI spans
  4. 4.Anthropic, Building a multi-agent research system
  5. 5.Anthropic, Building Effective Agents
  6. 6.Chroma, Context Rot: how increasing input tokens impacts LLM performance
  7. 7.Chip Huyen, Building A Generative AI Platform
  8. 8.Laminar, Top agent observability platforms
  9. 9.Latitude, AI agent failure detection guide