A traditional web service fails loudly - a 500 error, a stack trace, a spike on a dashboard. An AI agent can fail silently: it returns a confident, well-formatted, completely wrong answer, and nothing in a standard uptime monitor notices anything went wrong. The request succeeded. The response was fast. The content was garbage. Standard observability tooling was never built to catch that class of failure, which is why agentic systems need a different monitoring layer on top of the usual one.
Why Standard APM Doesn't Catch Agent Failures
Application performance monitoring tools are built around latency, error rate, and throughput - metrics that describe whether a request completed, not whether the response was correct. An agent that hallucinates a wrong answer, drops a tool call it should have made, or gets stuck in a retry loop that eventually succeeds will often show up as a perfectly healthy request in Datadog or New Relic. The failure lives inside the reasoning, not in the HTTP layer, and that is invisible to infrastructure-level monitoring.
The Three Layers of Agent Observability
| Layer | What It Answers | Primary Use |
|---|---|---|
| Tracing | What happened, step by step - every LLM call, tool call, and retrieval | Debugging a specific bad response |
| Evaluation | Was the output actually good, scored against a rubric or reference | Catching quality regressions before deploy |
| Alerting | Automated signals when quality or behavior drifts in production | Finding out about a problem before a client does |
Layer 1: Tracing Every Step of the Agent's Reasoning
The foundation of agent observability is capturing every step the agent took - not just the final output. When a user reports a bad answer, you need to see the full chain: what the retriever returned, which tool calls were made and with what arguments, what each intermediate LLM call produced, and where in that chain things went wrong.
import time
from contextlib import contextmanager
class AgentTrace:
def __init__(self, trace_id: str, user_id: str):
self.trace_id = trace_id
self.user_id = user_id
self.spans = []
@contextmanager
def span(self, name: str, **metadata):
start = time.time()
span_data = {"name": name, "metadata": metadata, "start": start}
try:
yield span_data
span_data["status"] = "success"
except Exception as e:
span_data["status"] = "error"
span_data["error"] = str(e)
raise
finally:
span_data["duration_ms"] = (time.time() - start) * 1000
self.spans.append(span_data)
def export(self):
return {
"trace_id": self.trace_id,
"user_id": self.user_id,
"spans": self.spans,
}
# Usage inside an agent run
trace = AgentTrace(trace_id=str(uuid4()), user_id=user_id)
with trace.span("retrieval", query=query) as span:
docs = retriever.get_relevant_documents(query)
span["result_count"] = len(docs)
with trace.span("llm_call", model="claude-opus-4-5") as span:
response = llm.invoke(prompt)
span["tokens"] = response.usage.total_tokens
send_to_observability_platform(trace.export())The metadata attached to each span matters more than the span itself. Recording token counts, retrieved document IDs, and tool call arguments turns a trace from a vague timeline into something you can actually use to diagnose why a specific response went wrong.
Layer 2: Automated Evaluation, Not Just Logging
Traces tell you what happened. They do not tell you whether it was good. That requires a separate evaluation pass - typically an LLM-as-judge scoring recent production traces against a rubric, run on a sample of traffic rather than every single request to keep costs manageable.
eval_prompt = """Score this AI agent response on a 1-5 scale for each dimension.
Return JSON only.
User query: {query}
Agent response: {response}
Retrieved context: {context}
Dimensions:
- faithfulness: does the response only use information from the context?
- relevance: does the response actually address the user's query?
- completeness: does it cover what the user needed, not just part of it?
"""
def evaluate_trace(trace: dict, llm) -> dict:
result = llm.invoke(eval_prompt.format(
query=trace["query"],
response=trace["response"],
context=trace["retrieved_context"],
))
scores = json.loads(result.content)
scores["trace_id"] = trace["trace_id"]
return scores
# Sample 10% of production traffic for continuous evaluation
if random.random() < 0.1:
scores = evaluate_trace(trace, judge_llm)
store_eval_result(scores)Running this continuously, not just at deploy time, is what catches quality drift - a retrieval index that slowly degrades, an upstream API that starts returning stale data, or a prompt that stops working well after a model provider quietly updates the underlying model.
Layer 3: Alerting on What Actually Predicts Client Complaints
The goal of alerting is catching a problem before a client emails you about it. That means alerting on leading indicators, not just aggregate averages that hide the failures a single unhappy client actually experienced.
- -Faithfulness score drop. A sustained drop in the evaluation faithfulness metric usually means retrieval quality degraded before anyone files a complaint about hallucinated answers.
- -Tool call failure rate. A spike in failed tool calls - a downstream API timing out, a schema mismatch - is often the earliest signal of an integration breaking.
- -Response length anomalies. A sudden shift in typical response length, in either direction, frequently correlates with a prompt regression or a model behaving unexpectedly after a provider-side update.
- -Fallback and retry rate. If your agent has fallback logic, a rising fallback rate means the primary path is failing more often than the top-line error rate shows.
Building a Minimal Observability Stack Without Overengineering
For most teams, this does not require building custom infrastructure. Dedicated LLM observability platforms handle tracing and evaluation storage out of the box and integrate with LangChain, direct API calls, and most agent frameworks with a few lines of instrumentation. The engineering effort that actually matters is deciding what to evaluate and what to alert on - the tooling is largely solved, the judgment calls are not.
The practical starting point on a new client project: trace every request from day one, run automated evaluation on a 5 to 10% sample, and set alerts on faithfulness score and tool failure rate before launch. That combination catches the large majority of production issues without requiring a dedicated observability engineer.
Frequently Asked Questions
How much does running continuous evaluation cost?
Evaluating a 10% sample of traffic with a capable judge model typically adds 3 to 8% to total LLM spend, since the judge call is usually smaller than the original generation call. That cost is consistently smaller than the cost of a client losing trust in a system after repeated unnoticed bad answers.
Can I use the same model as both the agent and the judge?
It works, but a slightly stronger model as judge than the one generating responses tends to catch more subtle failures - a model is not always well positioned to catch its own systematic blind spots. If budget allows, use a stronger or differently-trained model for evaluation than for generation.
What's the single most valuable metric to track first?
Faithfulness - whether the response only uses information actually present in the retrieved context or provided tools. It is the metric most directly tied to hallucination, and hallucinated answers are the failure mode most likely to damage a client's trust in the system, more than a slow response or a minor formatting issue ever would.
Do I need this level of observability for a small client project?
A lightweight version - full tracing plus a small evaluation sample - is worth setting up even on small projects, because it is what lets you say with confidence that a system is working well rather than assuming it is. It also becomes the evidence you show a client when they ask how you know the system is reliable, which is a stronger sales position than "it seemed fine in testing."
