Quick Takeaways
What you'll learn in this article
- 1
Cost exceeds $100 per hour (potential runaway loop)
- 2
p99 latency exceeds 10 seconds (user experience degraded)
- 3
Error rate exceeds 5 percent (systemic failure)
- 4
Circuit breaker trips (cost controls activating)
- 5
Cost per user up 50 percent week-over-week (trend analysis)
Keep reading for detailed implementation, code examples, and real-world results
The 95 Percent Problem
MIT researchers dropped a bombshell in July 2025: 95 percent of businesses that tried using AI found absolutely zero value in it. Not "limited value" or "mixed results" - zero. Complete failure.
If you have been following enterprise AI deployments, this statistic should not surprise you. AI agents routinely fail in production because teams deploy them without the observability infrastructure needed to understand what is actually happening. They measure nothing, track nothing, and wonder why their expensive LLM implementations provide no business value.
This tutorial solves that problem. You will build a production-ready AI agent framework with comprehensive observability, cost tracking, and performance metrics built in from day one. The complete code is available in the GitHub repository, but more importantly, you will understand the architectural decisions that separate functional AI agents from the 95 percent that fail.
What Makes AI Agents Different
Traditional software fails predictably. Exceptions throw. Tests fail. Logs capture errors. You know when something broke and approximately why it broke.
AI agents fail silently. An LLM returns plausible-sounding garbage. A reasoning loop consumes thirty dollars in API calls before timing out. A vector database query returns irrelevant context that leads to confident hallucinations. From the outside, everything looks fine - the agent responded, the logs show success, the monitoring dashboard stays green. But the business value is zero.
The difference is observable vs unobservable failure modes. You need instrumentation that exposes what LLMs are actually doing, not just whether they responded successfully.
Core Observability Requirements
Every production AI agent needs five categories of telemetry:
Cost Metrics capture every token spent. Input tokens, output tokens, reasoning tokens (for models like o1), embedding tokens, cache hits. You track cost per request, cost per user, cost per business outcome. Without this, you will wake up to a fifteen thousand dollar OpenAI bill and no idea where the money went.
Performance Metrics measure latency at every stage. Time to first token. Total response time. Time spent in vector search. Time spent in reasoning loops. Database query latency. External API calls. You need percentiles (p50, p95, p99) not just averages, because AI latency distributions have fat tails.
Quality Metrics quantify how well the agent works. Task completion rate. User satisfaction scores. Accuracy measurements. Retrieval relevance. These require human evaluation initially but can evolve toward automated quality scoring as you accumulate ground truth data.
Behavior Metrics expose what the agent is doing internally. How many reasoning steps? How many tool calls? Which tools? What was the retrieval context? What was the final prompt sent to the LLM? You need visibility into the decision-making process, not just the final output.
Error Metrics catch failure modes before users notice. Hallucination detection. Context window overflow. Timeout patterns. Rate limiting hits. Malformed tool calls. Invalid JSON parsing. Every failure mode needs a counter and alerting threshold.
Architecture Overview
The observable AI agent architecture has four layers:
Instrumentation Layer wraps every LLM call, vector search, tool invocation, and reasoning step with telemetry collection. This is not optional middleware you add later - it is the foundation. Every function that touches an LLM or vector database goes through instrumentation first.
Aggregation Layer collects metrics from instrumentation and routes them to appropriate backends. OpenTelemetry for distributed tracing. Prometheus for time-series metrics. Custom database for business metrics. Log aggregation for debugging. The key is structured data that can be queried and alerted on, not unstructured log dumps.
Analysis Layer processes telemetry to generate insights. Cost per user trending upward? Flag it. p99 latency spiking? Alert. Quality scores declining? Investigate. This layer turns raw telemetry into actionable intelligence.
Control Layer uses metrics to make runtime decisions. Circuit breakers trip when costs spike. Rate limiters engage when latency climbs. Fallback strategies activate when quality drops. The agent actively responds to its own metrics.
GitHub Repository Structure
The tutorial code is organized around this architecture:
ai-agent-observability/
โโโ src/
โ โโโ instrumentation/
โ โ โโโ llm_wrapper.py
โ โ โโโ vector_wrapper.py
โ โ โโโ tool_wrapper.py
โ โ โโโ tracing.py
โ โโโ aggregation/
โ โ โโโ metrics_collector.py
โ โ โโโ cost_tracker.py
โ โ โโโ trace_exporter.py
โ โโโ analysis/
โ โ โโโ dashboards.py
โ โ โโโ alerts.py
โ โ โโโ quality_scorer.py
โ โโโ control/
โ โ โโโ circuit_breaker.py
โ โ โโโ rate_limiter.py
โ โ โโโ fallback_manager.py
โ โโโ agent/
โ โโโ base_agent.py
โ โโโ rag_agent.py
โ โโโ reasoning_agent.py
โโโ tests/
โ โโโ test_instrumentation.py
โ โโโ test_cost_tracking.py
โ โโโ test_agent_behavior.py
โโโ docker/
โ โโโ docker-compose.yml
โ โโโ prometheus.yml
โ โโโ grafana-dashboards.json
โโโ examples/
โโโ simple_agent.py
โโโ rag_agent.py
โโโ reasoning_agent.py
The instrumentation directory contains wrappers for every external system. The aggregation directory handles metric collection and export. Analysis provides dashboards and alerting. Control implements runtime safety mechanisms. Agent contains the actual AI agent implementations that use all these layers.
LLM Instrumentation
Every LLM call goes through a wrapper that captures complete telemetry:
from opentelemetry import trace
from prometheus_client import Counter, Histogram
import time
class InstrumentedLLM:
def __init__(self, client, metrics_collector):
self.client = client
self.metrics = metrics_collector
self.tracer = trace.get_tracer(__name__)
# Prometheus metrics
self.token_counter = Counter(
'llm_tokens_total',
'Total tokens processed',
['model', 'type']
)
self.cost_counter = Counter(
'llm_cost_dollars_total',
'Total cost in dollars',
['model']
)
self.latency_histogram = Histogram(
'llm_latency_seconds',
'LLM response latency',
['model']
)
def complete(self, messages, model="gpt-4o", **kwargs):
start_time = time.time()
with self.tracer.start_as_current_span("llm_completion") as span:
span.set_attribute("llm.model", model)
span.set_attribute("llm.messages", len(messages))
try:
response = self.client.chat.completions.create(
messages=messages,
model=model,
**kwargs
)
# Extract usage
usage = response.usage
input_tokens = usage.prompt_tokens
output_tokens = usage.completion_tokens
# Calculate cost (example rates)
cost = self._calculate_cost(
model, input_tokens, output_tokens
)
# Update metrics
self.token_counter.labels(
model=model, type='input'
).inc(input_tokens)
self.token_counter.labels(
model=model, type='output'
).inc(output_tokens)
self.cost_counter.labels(model=model).inc(cost)
# Measure latency
latency = time.time() - start_time
self.latency_histogram.labels(model=model).observe(latency)
# Add to trace
span.set_attribute("llm.input_tokens", input_tokens)
span.set_attribute("llm.output_tokens", output_tokens)
span.set_attribute("llm.cost", cost)
span.set_attribute("llm.latency", latency)
# Log to custom metrics system
self.metrics.record_completion({
'model': model,
'input_tokens': input_tokens,
'output_tokens': output_tokens,
'cost': cost,
'latency': latency,
'timestamp': time.time()
})
return response
except Exception as e:
span.set_status(trace.Status(trace.StatusCode.ERROR))
span.set_attribute("error.type", type(e).__name__)
span.set_attribute("error.message", str(e))
raise
def _calculate_cost(self, model, input_tokens, output_tokens):
# Pricing as of December 2025
pricing = {
'gpt-4o': {'input': 0.0025, 'output': 0.010},
'gpt-4o-mini': {'input': 0.00015, 'output': 0.00060},
'o1': {'input': 0.015, 'output': 0.060},
'claude-sonnet-4': {'input': 0.003, 'output': 0.015},
}
if model not in pricing:
return 0.0
rates = pricing[model]
cost = (input_tokens / 1000 * rates['input'] +
output_tokens / 1000 * rates['output'])
return cost
This wrapper captures everything: token counts by type, costs, latency, model used, success or failure. The data flows to Prometheus for time-series analysis, OpenTelemetry for distributed tracing, and a custom metrics collector for business intelligence queries.
The critical insight is that you instrument at the lowest level - the actual API call - not at higher abstraction layers. If you wrap the agent but not the LLM client, you will miss internal retries, fallback models, and hidden tool usage that consumes tokens without your knowledge.
Cost Tracking Architecture
Cost is the metric most teams ignore until it explodes. You need granular cost tracking with attribution to users, sessions, and business outcomes.
The cost tracker maintains running totals with multiple dimensions:
class CostTracker:
def __init__(self, postgres_url):
self.db = self._connect(postgres_url)
self._create_tables()
def record_llm_call(self, user_id, session_id, task_id,
model, input_tokens, output_tokens, cost):
# Record to database
self.db.execute("""
INSERT INTO llm_costs
(user_id, session_id, task_id, model,
input_tokens, output_tokens, cost, timestamp)
VALUES (%s, %s, %s, %s, %s, %s, %s, NOW())
""", (user_id, session_id, task_id, model,
input_tokens, output_tokens, cost))
def get_user_cost(self, user_id, period='day'):
# Query total cost per user
return self.db.query("""
SELECT SUM(cost) as total_cost,
COUNT(*) as num_calls,
SUM(input_tokens + output_tokens) as total_tokens
FROM llm_costs
WHERE user_id = %s
AND timestamp greater than NOW() - INTERVAL %s
""", (user_id, period))
def get_task_cost(self, task_id):
# Cost for a specific task
return self.db.query("""
SELECT model,
SUM(cost) as total_cost,
SUM(input_tokens) as input_tokens,
SUM(output_tokens) as output_tokens,
COUNT(*) as num_calls
FROM llm_costs
WHERE task_id = %s
GROUP BY model
""", (task_id,))
def get_expensive_users(self, limit=10):
# Identify cost outliers
return self.db.query("""
SELECT user_id,
SUM(cost) as total_cost,
COUNT(DISTINCT session_id) as num_sessions,
AVG(cost) as avg_cost_per_call
FROM llm_costs
WHERE timestamp greater than NOW() - INTERVAL '7 days'
GROUP BY user_id
ORDER BY total_cost DESC
LIMIT %s
""", (limit,))
You track costs at three levels: user (who is spending?), session (what conversation?), task (what business outcome?). This enables queries like "what did it cost to process this support ticket?" or "which users are responsible for 80 percent of our LLM spend?"
The database schema also supports trend analysis. Is cost per user increasing over time? Are certain models becoming more or less expensive relative to value delivered? You cannot optimize what you cannot measure.
Vector Search Instrumentation
RAG agents live and die by retrieval quality. You need visibility into what chunks the vector database is returning and whether they are actually relevant.
class InstrumentedVectorStore:
def __init__(self, vector_db, metrics_collector):
self.vector_db = vector_db
self.metrics = metrics_collector
self.tracer = trace.get_tracer(__name__)
self.search_counter = Counter(
'vector_search_total',
'Total vector searches',
['collection']
)
self.search_latency = Histogram(
'vector_search_latency_seconds',
'Vector search latency',
['collection']
)
def search(self, query, collection, top_k=5):
start_time = time.time()
with self.tracer.start_as_current_span("vector_search") as span:
span.set_attribute("collection", collection)
span.set_attribute("top_k", top_k)
# Perform search
results = self.vector_db.search(
query=query,
collection=collection,
top_k=top_k
)
latency = time.time() - start_time
# Update metrics
self.search_counter.labels(collection=collection).inc()
self.search_latency.labels(collection=collection).observe(latency)
# Extract relevance scores
scores = [r.score for r in results]
# Log search metadata
span.set_attribute("results.count", len(results))
span.set_attribute("results.min_score", min(scores))
span.set_attribute("results.max_score", max(scores))
span.set_attribute("results.avg_score", sum(scores) / len(scores))
# Record to metrics system
self.metrics.record_vector_search({
'collection': collection,
'top_k': top_k,
'num_results': len(results),
'scores': scores,
'latency': latency,
'timestamp': time.time()
})
return results
The key metrics are latency, result count, and relevance scores. You want distributions, not averages - a vector search that usually returns highly relevant results but occasionally returns complete garbage is worse than one that consistently returns mediocre results, because you cannot predict when failures occur.
Advanced implementations also log the actual chunks returned so you can manually audit retrieval quality. Did the user ask about pricing but get chunks about product features? That is a retrieval failure your metrics should flag.
Reasoning Loop Instrumentation
Multi-step reasoning agents (like those using ReAct or chain-of-thought) can spiral into expensive loops if not carefully monitored.
class InstrumentedReasoningAgent:
def __init__(self, llm, tools, metrics_collector):
self.llm = llm
self.tools = tools
self.metrics = metrics_collector
self.tracer = trace.get_tracer(__name__)
def run(self, task, max_steps=10):
with self.tracer.start_as_current_span("reasoning_task") as task_span:
task_span.set_attribute("task", task)
task_span.set_attribute("max_steps", max_steps)
steps_taken = 0
total_cost = 0.0
tool_calls = []
messages = [{"role": "user", "content": task}]
for step in range(max_steps):
with self.tracer.start_as_current_span(f"step_{step}"):
# Get next action from LLM
response = self.llm.complete(
messages=messages,
tools=self.tools
)
steps_taken += 1
total_cost += response.cost
# Check if done
if response.finish_reason == 'stop':
break
# Execute tool calls
if response.tool_calls:
for tool_call in response.tool_calls:
tool_calls.append(tool_call.name)
result = self._execute_tool(tool_call)
messages.append({
"role": "tool",
"content": result
})
# Record task metrics
task_span.set_attribute("steps_taken", steps_taken)
task_span.set_attribute("total_cost", total_cost)
task_span.set_attribute("tool_calls", len(tool_calls))
self.metrics.record_reasoning_task({
'task': task,
'steps_taken': steps_taken,
'total_cost': total_cost,
'tool_calls': tool_calls,
'success': steps_taken less than max_steps,
'timestamp': time.time()
})
return response
You track steps taken, tools called, and whether the agent converged. Agents that consistently hit max_steps without completing tasks are burning money on failure cases. This metric exposes that pattern immediately.
Tool Usage Tracking
Every tool the agent can invoke needs instrumentation. Tools are where invisible failures hide - a broken API that the agent keeps retrying, a database query that times out but returns empty results, a search endpoint that rate limits silently.
class InstrumentedTool:
def __init__(self, tool_func, metrics_collector):
self.tool_func = tool_func
self.metrics = metrics_collector
self.tracer = trace.get_tracer(__name__)
self.call_counter = Counter(
'tool_calls_total',
'Total tool calls',
['tool_name', 'status']
)
self.latency_histogram = Histogram(
'tool_latency_seconds',
'Tool execution latency',
['tool_name']
)
def __call__(self, *args, **kwargs):
tool_name = self.tool_func.__name__
start_time = time.time()
with self.tracer.start_as_current_span(f"tool_{tool_name}") as span:
span.set_attribute("tool.name", tool_name)
try:
result = self.tool_func(*args, **kwargs)
latency = time.time() - start_time
self.call_counter.labels(
tool_name=tool_name,
status='success'
).inc()
self.latency_histogram.labels(
tool_name=tool_name
).observe(latency)
span.set_attribute("tool.latency", latency)
span.set_attribute("tool.result_size", len(str(result)))
self.metrics.record_tool_call({
'tool_name': tool_name,
'success': True,
'latency': latency,
'timestamp': time.time()
})
return result
except Exception as e:
self.call_counter.labels(
tool_name=tool_name,
status='error'
).inc()
span.set_status(trace.Status(trace.StatusCode.ERROR))
span.set_attribute("error.type", type(e).__name__)
self.metrics.record_tool_call({
'tool_name': tool_name,
'success': False,
'error': str(e),
'timestamp': time.time()
})
raise
Tool metrics expose patterns like "the database tool succeeds 99 percent of the time but takes 5 seconds when it does, while the API tool fails 20 percent of the time but returns instantly when it works." You need both success rates and latency distributions to make informed architectural decisions.
Quality Scoring
Quality is the hardest metric to automate but the most important to track. You cannot optimize for cost and latency if quality suffers.
Initial quality measurement requires human evaluation:
class QualityScorer:
def __init__(self, postgres_url):
self.db = self._connect(postgres_url)
self._create_tables()
def request_evaluation(self, task_id, response):
# Store response for human evaluation
self.db.execute("""
INSERT INTO evaluation_queue
(task_id, response, status)
VALUES (%s, %s, 'pending')
""", (task_id, response))
def record_evaluation(self, task_id, score, feedback):
# Human evaluator provides score 1-5
self.db.execute("""
UPDATE evaluation_queue
SET score = %s,
feedback = %s,
status = 'complete',
evaluated_at = NOW()
WHERE task_id = %s
""", (score, feedback, task_id))
def get_quality_metrics(self, period='week'):
# Aggregate quality scores
return self.db.query("""
SELECT DATE(evaluated_at) as date,
AVG(score) as avg_score,
COUNT(*) as num_evaluations,
STDDEV(score) as score_stddev
FROM evaluation_queue
WHERE status = 'complete'
AND evaluated_at greater than NOW() - INTERVAL %s
GROUP BY DATE(evaluated_at)
ORDER BY date DESC
""", (period,))
As you accumulate ground truth data, you can train a quality prediction model that estimates quality without human evaluation. But you always need human-in-the-loop validation to catch drift.
The goal is not perfect automation - it is enough signal to know when quality degrades before users complain.
Circuit Breakers and Cost Controls
Observability without control is just expensive logging. You need runtime mechanisms that respond to metrics.
Circuit breakers prevent cascading failures when costs spike:
class CostCircuitBreaker:
def __init__(self, cost_threshold, window_seconds):
self.cost_threshold = cost_threshold
self.window_seconds = window_seconds
self.recent_costs = []
self.state = 'closed' # closed, open, half_open
self.lock = threading.Lock()
def check_cost(self, estimated_cost):
with self.lock:
now = time.time()
# Remove old costs outside window
self.recent_costs = [
(t, c) for t, c in self.recent_costs
if now - t less than self.window_seconds
]
# Calculate current spend rate
total_cost = sum(c for _, c in self.recent_costs)
# Add estimated cost
projected_cost = total_cost + estimated_cost
if projected_cost greater than self.cost_threshold:
self.state = 'open'
raise CostLimitExceeded(
f"Cost limit {self.cost_threshold} exceeded: "
f"{projected_cost:.2f}"
)
# Record the cost
self.recent_costs.append((now, estimated_cost))
return True
This circuit breaker tracks rolling window costs and trips when spend rate exceeds thresholds. You wrap expensive operations to prevent runaway costs:
agent = InstrumentedReasoningAgent(llm, tools, metrics)
circuit_breaker = CostCircuitBreaker(
cost_threshold=10.0, # $10 per 5 minutes
window_seconds=300
)
try:
circuit_breaker.check_cost(estimated_cost=0.50)
result = agent.run(task)
except CostLimitExceeded as e:
logger.error(f"Circuit breaker tripped: {e}")
# Fallback to cheaper model or return cached response
result = fallback_handler.handle(task)
You can also implement per-user rate limiters, per-model budget allocations, and time-of-day cost adjustments. The key is that your agent actively manages its own resource consumption based on real-time metrics.
Deployment Architecture
The complete system runs in Docker with Prometheus for metrics, Grafana for dashboards, and Jaeger for distributed tracing:
version: '3.8'
services:
agent:
build: .
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- POSTGRES_URL=postgresql://postgres:5432/metrics
- PROMETHEUS_PUSHGATEWAY=prometheus:9091
- JAEGER_AGENT_HOST=jaeger
depends_on:
- postgres
- prometheus
- jaeger
postgres:
image: postgres:16
environment:
- POSTGRES_DB=metrics
- POSTGRES_PASSWORD=metrics
volumes:
- postgres_data:/var/lib/postgresql/data
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
ports:
- '9090:9090'
grafana:
image: grafana/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- ./grafana-dashboards.json:/etc/grafana/provisioning/dashboards/dashboards.json
- grafana_data:/var/lib/grafana
ports:
- '3000:3000'
jaeger:
image: jaegertracing/all-in-one
ports:
- '16686:16686'
- '6831:6831/udp'
volumes:
postgres_data:
prometheus_data:
grafana_data:
Prometheus scrapes metrics from the agent service. Grafana visualizes Prometheus data with pre-built dashboards. Jaeger collects distributed traces for request-level debugging. PostgreSQL stores business metrics for complex queries.
You get out-of-the-box monitoring infrastructure that requires zero code changes to integrate - everything flows through the instrumentation layer you already built.
Dashboard Design
Effective dashboards surface actionable insights, not just data dumps. You need three dashboard types:
Real-time Operations Dashboard shows current system state. Token consumption rate per second. Requests per minute. p99 latency. Current costs. Alert states. This is the dashboard you watch during incidents.
Cost Analysis Dashboard breaks down spend by dimension. Cost per user (top 20). Cost per model over time. Cost per business outcome. Projected monthly burn rate. This is the dashboard finance reviews.
Quality Dashboard tracks user-facing metrics. Task completion rate. Average quality score. User satisfaction trends. Hallucination detection rate. This is the dashboard product managers review.
The Grafana dashboards in the repository implement all three with sensible defaults. You can clone and customize based on your specific business metrics.
Alerting Strategy
Metrics without alerts are metrics nobody acts on. You need alerting rules that catch problems before they impact users.
High severity alerts fire immediately:
- Cost exceeds $100 per hour (potential runaway loop)
- p99 latency exceeds 10 seconds (user experience degraded)
- Error rate exceeds 5 percent (systemic failure)
- Circuit breaker trips (cost controls activating)
Medium severity alerts trigger investigation:
- Cost per user up 50 percent week-over-week (trend analysis)
- Quality score drops below 4.0 (degrading performance)
- Specific tool failure rate above 10 percent (dependency issue)
Low severity alerts provide context:
- New cost patterns detected (behavioral change)
- Latency trending upward (potential scaling issue)
- Token usage changing (prompt engineering impact)
Configure alerts in Prometheus Alertmanager and route to your incident management system. The key is signal-to-noise ratio - too many alerts and teams ignore them all, too few and you miss critical failures.
Testing Observable Agents
Standard unit tests verify function correctness. Observable agent tests verify that instrumentation works and metrics flow correctly.
def test_llm_instrumentation():
# Mock LLM client
mock_client = MockLLMClient()
metrics = MetricsCollector()
llm = InstrumentedLLM(mock_client, metrics)
# Make a call
response = llm.complete(
messages=[{"role": "user", "content": "Hello"}],
model="gpt-4o"
)
# Verify metrics were recorded
assert metrics.get_counter('llm_tokens_total') greater than 0
assert metrics.get_counter('llm_cost_dollars_total') greater than 0
assert metrics.get_histogram('llm_latency_seconds').count == 1
def test_cost_circuit_breaker():
breaker = CostCircuitBreaker(
cost_threshold=1.0,
window_seconds=60
)
# Should allow small costs
breaker.check_cost(0.25)
breaker.check_cost(0.25)
# Should trip on excessive cost
with pytest.raises(CostLimitExceeded):
breaker.check_cost(1.0)
def test_end_to_end_instrumentation():
# Full agent workflow
agent = create_test_agent()
metrics = MetricsCollector()
result = agent.run("Test task")
# Verify complete metric chain
assert metrics.llm_calls greater than 0
assert metrics.vector_searches greater than 0
assert metrics.tool_calls greater than 0
assert metrics.total_cost greater than 0
Integration tests verify that metrics actually flow to Prometheus, Grafana renders dashboards correctly, and Jaeger receives traces. The repository includes a test suite that validates the entire observability stack.
Common Pitfalls
Teams building observable AI agents consistently make the same mistakes:
Instrumenting too late. Adding observability after the agent is built requires refactoring every LLM call, vector search, and tool invocation. Build instrumentation first, then build the agent on top of it.
Measuring the wrong things. Vanity metrics like "number of requests" or "average tokens" tell you nothing about business value. Focus on cost per outcome, quality per dollar spent, and completion rates.
Ignoring latency distributions. Your p50 latency might be 500ms while your p99 is 30 seconds. Users see the p99, not the average. Always track percentiles.
Not tracking quality. You cannot improve what you do not measure. Even manual quality evaluation is better than none. Start with sampling 10 percent of responses and expand from there.
Treating metrics as optional. Metrics are not debugging tools you add when problems arise. They are production infrastructure as critical as the LLM itself. Every production agent needs full instrumentation.
Production Deployment Checklist
Before deploying any AI agent to production, verify these requirements:
- Every LLM call goes through instrumented wrapper
- Vector searches track latency and relevance scores
- Tool calls record success rates and failure modes
- Cost tracking includes user attribution and business context
- Circuit breakers prevent runaway costs
- Dashboards show real-time operational metrics
- Alerts trigger before users notice degradation
- Quality evaluation pipeline is running
- Traces export to Jaeger for debugging
- Metrics export to Prometheus for analysis
- Database stores business metrics for complex queries
If you cannot check all boxes, your agent is not production-ready.
Beyond Basic Observability
Advanced implementations add capabilities beyond this tutorial:
Automated Quality Scoring uses LLM-as-judge patterns to estimate quality without human evaluation. You compare agent outputs against few-shot examples and predict quality scores.
Cost Attribution tracks which features, users, or business processes drive LLM spend. This enables ROI analysis and investment prioritization.
A/B Testing Infrastructure routes traffic between agent versions and compares quality, cost, and latency metrics. You make data-driven decisions about prompt engineering and model selection.
Predictive Alerting uses historical patterns to predict when metrics will exceed thresholds. You get advance warning before circuit breakers trip.
Automated Optimization adjusts prompts, model selection, and caching strategies based on metrics. The agent optimizes itself without human intervention.
The repository includes examples of these advanced patterns. Start with basic observability, then layer on sophistication as you accumulate data and understanding.
Why This Matters
The 95 percent AI implementation failure rate is not inevitable. It happens because teams deploy agents without understanding what is actually happening inside them. LLMs are black boxes that hallucinate confidently, consume unbounded resources, and fail silently. Without observability, you cannot distinguish between agents that work and agents that burn money producing garbage.
This tutorial gives you the architecture to avoid that fate. You instrument every component, track every cost, measure every outcome. When failures occur - and they will - you have the telemetry to understand why and fix it before users notice.
The GitHub repository contains the complete implementation. Clone it, run the examples, and adapt the patterns to your specific needs. The code is production-tested and handles the edge cases that tutorials usually ignore - rate limiting, retries, timeouts, circuit breaking, cost controls.
Build observable AI agents or join the 95 percent who failed. The choice is obvious.
Repository Access
Complete code, Docker setup, Grafana dashboards, and working examples are available at:
GitHub: https://github.com/CrashBytes/ByteSizedExamples/tree/main/ai-agent-observability
Follow the README for setup instructions. The repository includes everything you need to run the complete stack locally in under five minutes.
