AI EngineeringZero to ProductionHome·About·What’s new·Contact
MLOps & LLMOps · Chapter O4

Monitoring, Governance & Responsible AI in Production

A deployed LLM app that no one is watching is a liability with a URL. This chapter is the observability pillar (O1) plus the governance and responsible-AI practices that keep a live system trustworthy — the four signals to watch, how to trace an agent, and how to run an incident when the model does something it shouldn't.

⏱️ ~55 min📊 Hands-on🎯 Intermediate→Advanced

Learning objectives

  • Instrument the four core signals: quality, cost, latency, and safety.
  • Trace a multi-step agent so you can debug what actually happened.
  • Detect drift and quality regressions in production, not just offline.
  • Apply governance: audit logs, access control, data handling, human oversight.
  • Run responsible-AI practice — bias, transparency, and an AI incident response.
📋 Before you startThis page builds on: MLOps & LLMOps foundations · Evaluation & testing. Jumping in? Skim those first — or open the recap.
30-second recap
  • MLOps & LLMOps foundations: Treat models, prompts and data as versioned artifacts with tests, deploys and monitoring — the same discipline as software, plus evaluation.
  • Evaluation & testing: A golden set of real questions plus cheap deterministic checks and an LLM judge, run as a regression gate before every change ships.
Where this sitsThis closes the LLMOps loop from O1: observability is how "monitor" feeds the next iteration. It builds on the evals of Chapter 5 (now run live), the guardrails of Chapter 6, and the security of T1 — turned into an operational, always-on practice.

The four signals advanced

Start with the failure that nobody sees coming. Your app is green. Latency is flat, the error rate is a rounding error, the pager is quiet — and yet, for the last three days, one in ten answers has been confidently, fluently wrong. The provider silently rolled a new model checkpoint; your retrieval corpus went stale; a prompt tweak shipped Tuesday. Every one of those requests returned HTTP 200 in 900ms. Your dashboard is telling the truth and missing the disaster at the same time, because it was built to watch a normal web service and an LLM is not a normal web service.

Here's the analogy. Classic web monitoring is a car dashboard with two gauges: speed (latency) and the check-engine light (errors). That's enough for a car, because a car that's moving and not on fire is basically fine. But an LLM app is more like a restaurant kitchen: the food can come out fast and the kitchen can be spotless, and the dish can still be inedible — or cost you triple what you charged for it. "Fast" and "didn't crash" simply do not tell you whether the thing you shipped is any good, or whether it's quietly draining your budget one token at a time.

So an LLM app needs two more axes bolted onto the classic two. Cost is unique because inference is metered — you pay per token in and per token out, so a longer prompt, a chattier answer, or an agent that loops one extra time all cost real money, and none of that shows up as an error. Quality is unique because a request can succeed at every layer of the stack — valid JSON, 200 status, no exception — and still hand the user a hallucination. Correctness lives above the HTTP layer, so the transport can't measure it for you; you have to sample and judge the actual content.

The mistake almost everyone makes first is to treat quality and cost as things you'll "add later, once we have time." You won't — because there's no error to remind you, the gap stays invisible until a customer or a finance report finds it for you. The fix is a discipline, not a tool: decide that every single request emits all four signals — quality, cost, latency, safety — from day one, even if "quality" starts as nothing more than a thumbs-up button. A signal you don't emit per request is a signal you can never aggregate, alert on, or trend later.

Classic monitoring watches latency and errors. An LLM app needs two more axes — cost (it's metered) and quality (correct HTTP 200s can still be wrong). Watch all four or you're flying blind on the ones that matter most.

Qualityis it right? Cost$/req, tokens Latencyp50/p95, TTFT Safetyrefusals/flags the two on the left are what classic monitoring misses every request should emit all four Four axes, always on. Quality: is the output correct/grounded? Cost: tokens and dollars per request and per feature. Latency: p50/p95 and time-to-first-token. Safety: refusals, guardrail hits, flagged content. A green latency dashboard hides a quality collapse — you need all four.
🗺️ How to read this diagram

This diagram names the four things you must watch for a live LLM app. Normal web apps watch only two of them; the picture points out the two extra ones that catch people out.

  • Read the four boxes left to right. Each is one signal — a number you collect on every request. Under each name is the plain-English question it answers.
  • Quality — "is it right?" A reply can be a successful HTTP 200 and still be wrong or made-up, so correctness is its own signal, not something the server status tells you.
  • Cost — "$/req, tokens." Every call is metered: you pay per token in and per token out. This is unique to LLM apps and it can quietly balloon.
  • Latency — "p50/p95, TTFT." How long a reply takes. p50 is the typical request, p95 is the slow tail (95% are faster), and TTFT is time-to-first-token — how fast streamed text starts appearing.
  • Safety — "refusals, flags." How often the model refuses, trips a guardrail, or emits something flagged. The top caption calls out that Quality and Cost (the left two) are exactly what classic monitoring misses.

In short: The bottom caption is the rule: every request should emit all four. A green latency dashboard can hide a quality collapse — watch all four axes or you're flying blind on the ones that matter most.

SignalWhat to trackHow
QualityGroundedness, correctness, user thumbs, task successLive LLM-judge sampling, user feedback, golden-set replay (Ch 5)
CostInput/output tokens, $/request, spend per feature & per userusage on every response, aggregated in the gateway (C2, O2)
Latencyp50/p95/p99, time-to-first-token, per-step in agentsTiming spans; TTFT matters for streamed UX (A3)
SafetyRefusals, guardrail triggers, injection attempts, PII leaksstop_reason, guardrail logs, red-team alerts (T1)

Lab O4.1 · Tracing an agent advanced

Picture the bug report you'll actually get: "the agent gave a wrong answer to this question." That's all. Now — where do you even look? The agent didn't do one thing; it did eight. It planned, searched your docs, read the results, called a tool, got an error, retried, reasoned again, and wrote a reply. A single log line saying "request failed" or even "answer: …" tells you the outcome and nothing about the eight steps that produced it. You're left re-running it by hand and hoping it misbehaves the same way twice.

The fix is a trace, and the cleanest analogy is a medical bill versus an itemized receipt. A one-line log is the total at the bottom: "$47, something went wrong." A trace is the itemized receipt — every line item, what it cost, how long it took, and a red flag next to the one that failed. You stop guessing which organ hurts and just read where the pain is. Concretely, a trace is the nested, timed tree of every step in one request: each model call, each tool call, each retrieval, with its duration, its token count, and any error, all stitched together under one shared request id.

The common mistake here is logging at the wrong altitude. Teams instrument the outer request (start, end, total time) because that's what their web framework already does — and it's useless for an agent, because the interesting failures happen inside the loop. Retrieval returned nothing, or a tool timed out on the third of five calls, or the model reasoned fine but off stale context. Whole-request logging can't distinguish any of those; they all look like "slow request, bad answer." The discipline is to log at the step level and thread one trace_id through every step, so the individual lines reassemble into the tree. That single id is the whole trick — get it flowing and everything else is just fields on a record.

For a single call, a log line is enough. For an agent — a loop of model calls, tool calls, retrievals (L4/M4) — you need a trace: the full tree of steps for one request, so when it goes wrong you can see where.

request · 4.2s · $0.03 · 3 tool calls └ model call · plan · 1.1s └ tool: search_docs · 0.4s └ model call · use results · 1.8s └ tool: run_query · 0.6s · ERROR (retried) └ model call · final answer · 0.3s A trace is the request's story. One row per step — model calls, tool calls, retrievals — nested and timed, with tokens/cost and errors attached. When an agent gives a wrong answer, the trace shows whether retrieval missed, a tool errored, or the model reasoned badly. Without it, you're guessing.
🗺️ How to read this diagram

This is a trace: the full story of a single agent request, broken into the steps it took. An agent isn't one call — it's a loop of model calls and tool calls — so you need to see the whole tree to know where things went right or wrong.

  • The top bar is the whole request, with its totals: 4.2s elapsed, $0.03 spent, and 3 tool calls. Everything below is a step inside it.
  • Read top to bottom = time order, and indentation = nesting. The └ marks show each row is a child step of the request above it.
  • The steps alternate: a model call ("plan"), then a tool call (search_docs), another model call that uses the results, another tool call, and a final model call that writes the answer. Each row carries its own duration.
  • Look at run_query · 0.6s · ERROR (retried). The trace records that this tool failed and was retried — the kind of detail that explains a slow or wrong answer.

In short: When an agent gives a bad answer, this tree tells you which step to blame — retrieval missed, a tool errored, or the model reasoned badly. Without a trace you'd just be guessing.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab O4.1

Uses the client / objects set up earlier in this lesson, and needs ANTHROPIC_API_KEY set. Run the earlier blocks first.

trace.py# Minimal home-grown span — one structured log line per step
import time, json, uuid

def span(trace_id, name, **fields):
    start = time.perf_counter()
    def done(**extra):
        rec = {"trace_id": trace_id, "step": name,
               "ms": round((time.perf_counter()-start)*1000), **fields, **extra}
        print(json.dumps(rec))          # ship to your log/trace backend
    return done

tid = uuid.uuid4().hex
d = span(tid, "model.plan", model="claude-opus-4-8")
resp = client.messages.create(...)
d(in_tok=resp.usage.input_tokens, out_tok=resp.usage.output_tokens,
  stop=resp.stop_reason)              # quality/cost/safety signals captured per step
▶ How this works

This tiny helper is real, ship-today observability: it wraps each step of your agent so that every step prints one structured log line carrying its timing, tokens, and safety signal. String a trace_id through all the steps and those lines reassemble into the trace tree above.

  1. span(trace_id, name, **fields) is called before a step. It records a start time with time.perf_counter() (a high-precision stopwatch) and returns an inner done function — that's the pattern called a closure: done remembers the start time and the fields.
  2. You call done(**extra) after the step. It builds a record rec with the trace_id, the step name, the elapsed milliseconds (ms), plus any fields you passed in either call.
  3. print(json.dumps(rec)) turns that record into one line of JSON. Printing JSON is the trick: any log system can parse it, so this one print is enough to ship to your log/trace backend.
  4. The bottom shows it in use: tid = uuid.uuid4().hex makes a unique id for this request; d = span(tid, "model.plan", ...) opens the step; after the model call, d(in_tok=..., out_tok=..., stop=resp.stop_reason) closes it, capturing cost (tokens) and a safety signal (stop_reason) for that step.

What the output means: Each step prints a JSON line like {"trace_id": "…", "step": "model.plan", "ms": 1100, "model": "claude-opus-4-8", "in_tok": …, "out_tok": …, "stop": "end_turn"}. Group those lines by trace_id and you have the whole request's timeline.

Try this: Add a second span around a fake tool call (e.g. span(tid, "tool.search")) and print both lines. Sharing the same tid is what lets a tool later stitch them into one trace — exactly what LangSmith automates for you.

Roll your own, then graduate to a toolThe snippet above — a trace id threaded through structured log lines carrying tokens, latency, and stop_reason — is real observability you can ship today. When manual correlation gets painful, adopt a dedicated LLM-tracing tool (LangSmith and others) that visualizes the tree, diffs runs, and links traces to evals. The concept is the same; the tool just makes it navigable. (LangSmith gets its own chapter later in the course.)

Detecting drift & live regressions advanced

Here's the uncomfortable truth about a shipped LLM app: it gets worse while you sleep, and nothing tells you. You didn't change a line of code. But the world your app lives in kept moving — new kinds of questions arrived, your knowledge base aged, the provider quietly swapped the model behind the same API name. The system you validated at deploy is not the system running today, and the gap only widens. This slow, silent decay is called drift, and it's the defining operational hazard of AI systems that ordinary software just doesn't have.

The analogy that makes it click: a deploy-time eval is like a doctor's checkup. It confirms you were healthy on the day you were examined. It says nothing about the months in between — and drift is exactly the thing that develops in the months in between. You can pass every offline test with flying colors and still be degrading in production three weeks later, for reasons that have nothing to do with your code. That's why "we tested it before launch" is a necessary answer and a dangerously incomplete one.

It helps to see that drift isn't one thing but a family, and each member has a different tell. Input drift is your users changing — they start asking about topics your prompts and retrieval were never built for, and your "I don't know" rate quietly climbs. Quality drift is the answers getting worse even on familiar questions, usually from a stale corpus or accumulating edge cases. Model drift is the ground moving — the provider updates the model and behavior shifts under you. Cost drift is the bill creeping — contexts grow, agents loop more, tokens-per-request trend up. Naming them matters because the detection method differs: you cluster inputs to catch the first, judge sampled traffic to catch the second, pin model ids and replay a golden set to catch the third, and trend spend to catch the fourth.

The mistake to avoid is assuming a green error dashboard means a healthy system. Drift never throws an exception — that's precisely what makes it dangerous. The only defense is active, scheduled measurement: sample live traffic through a judge and trend the score, and re-run your golden set against production config on a timer, not just at deploy. You're installing a smoke alarm for a fire that produces no smoke.

Offline evals (Ch 5) catch regressions before deploy. But production changes underneath you — inputs shift, the corpus grows, the provider updates the model. You need to catch degradation after deploy too.

Drift typeWhat it isHow to catch it
Input driftUsers start asking things your prompts/RAG weren't built forCluster/sample live inputs; watch for new topics & rising "I don't know"s
Quality driftAnswers slowly get worse (stale corpus, edge cases)Sample live traffic through an LLM-judge; track the score over time
Model driftThe provider updates the model; behavior shiftsPin model IDs; run the golden set on a schedule & on version change (O1)
Cost driftTokens/request creep up (longer contexts, more tool calls)Trend $/request; alert on step changes
Run your golden set continuously, not just at deployThe most dangerous LLM regression is the silent one: the model version changes or the corpus drifts, quality drops, and no error ever fires. Schedule your golden-set eval to run periodically against production config — it's the smoke alarm that catches what dashboards miss.

Governance: audit, access & data expert

Imagine the phone call. A customer says your AI told them something that cost them money, or a regulator asks you to produce every decision your system made about a particular person, or a security review wants to know exactly what data left your walls and went to the model provider. In that moment, "governance" stops being a compliance buzzword and becomes a single, brutally practical question: can you actually answer? Governance is the machinery that lets you say yes — the answer to "who did what, with whose data, and can we prove it?"

The analogy is a hospital, not a filing cabinet. A hospital doesn't just store records; it controls who can open a chart (access control), stamps every entry with who wrote it and when (audit log), has rules for what leaves the building and how long things are kept (data handling), and requires a second signature for high-risk procedures (human oversight). Governance for an LLM app is that same discipline pointed at a system that talks. It becomes non-optional the moment your app touches real users or regulated data — and unlike features, you can't retrofit it after an incident, because the whole point is that the records had to already exist before the question got asked.

Here's the reframing that makes it concrete for engineers, and it's the section's central idea: the trace you built in the last section is your audit trail. The nested, timed record of who called what, with which prompt version and model, at what cost — that observability artifact and the compliance artifact are the same object viewed from two angles. Access control is least privilege on your tools (an agent shouldn't hold a permission it never uses); data handling is deciding deliberately what you send to the provider and what you retain; human oversight is your HITL approval gate for the actions that can't be undone. None of this is exotic — it's ordinary engineering rigor applied on purpose.

The trap is treating governance as paperwork that lives in a wiki, separate from the running system. Real governance is enforced by the code path: the permission check that actually blocks the call, the redactor that actually scrubs the log, the approval step the agent literally cannot skip. A policy document nobody can enforce is theater; a controlled code path is governance.

Governance is the answer to "who did what, with whose data, and can we prove it?" It becomes non-optional the moment an LLM app touches real users or regulated data.

ControlWhat it means for an LLM app
Audit logImmutable record of every request: who, when, prompt version, model, output, cost. The trace is your audit trail
Access controlWho can call which capability; least privilege on tools (L3, T1); which users see which data
Data handlingWhat you send to the provider, retention, PII redaction, residency; don't log secrets or raw PII (T1)
Human oversightWhich actions require approval (the L5 HITL gate); who reviews flagged outputs
Change controlPrompt/model changes go through review + eval gate + canary (O1, O3)
Logging is a data-handling decisionObservability tempts you to log full prompts and outputs — which may contain PII, secrets, or confidential content. Decide deliberately: redact sensitive fields, set retention limits, control who can read traces. An audit log that itself leaks data is a governance failure, not a governance win. (This is the T1 secrets rule at the observability layer.)

Responsible AI in production expert

There's a failure mode worse than downtime, and it doesn't page anyone. A system that's fully up — fast, cheap, error-free — can still be quietly refusing one group of users more often than another, presenting itself as human, or stating guesses with the confidence of facts. No dashboard turns red, because from the machine's point of view nothing is wrong. Responsible AI is the practice of watching for the harms that work as designed: the outputs that are technically successful and ethically or legally unacceptable.

The mental shift this section asks for is captured in one line: these are signals you monitor, not a checklist you tick. The analogy is food safety versus a health inspection. An inspection is a snapshot — pass it once, frame the certificate, and nothing stops the kitchen from getting dirty the next day. Real food safety is the ongoing habit: temperatures logged every shift, surfaces checked continuously. Responsible AI works the same way. A pre-launch "bias review" that never runs again is an inspection; tracking refusal rates across user groups every day and feeding flagged outputs back into your evals is food safety. Fairness, transparency, and safety aren't states you achieve — they're properties you continuously measure or lose.

Concretely, that turns four abstract virtues into four operational habits. Fairness means you actually test outputs across user groups and put bias cases in the golden set, rather than hoping the model is even-handed. Transparency means telling users they're talking to AI and citing your sources — the same grounding discipline from RAG, now as an honesty requirement. Accountability means a named human owns the system's behavior and the audit log makes every decision traceable back to them. Contestability means a user who gets a bad answer can flag it and reach a person, and that flag flows back into your eval set instead of vanishing.

The mistake is assuming your provider's alignment work covers you. It doesn't — the base model's safety training knows nothing about your users, your domain, or the specific ways your deployment can skew. Responsible AI is your job at the application layer, and it's continuous.

Beyond keeping the system up, governance includes keeping it fair and honest. These are operational practices, not a one-time checklist.

PracticeIn production it looks like…
Fairness / biasTest outputs across user groups; watch for skew in who gets refused or mishandled; include bias cases in the golden set
TransparencyTell users they're talking to AI; cite sources (Ch 3); don't disguise limits
AccountabilityA human owns the system's behavior; the audit log makes decisions traceable
ContestabilityUsers can flag a bad output and reach a human; feedback flows back into evals
Safety monitoringTrack refusals, jailbreak attempts, and harmful-output flags as first-class signals (T1)

Lab O4.2 · The AI incident response expert

Rethink what "incident" even means for an AI system. In classic ops, an incident is something breaking: a server down, a 500 storm, a database on fire. Those still happen — but the incidents that will actually keep you up at night with an LLM are the ones where nothing broke. The system was healthy and did something harmful: it emitted an offensive answer, leaked a customer record into a reply, looped a tool call until it burned $4,000, or got talked past its guardrails by a clever prompt. The machine thinks it succeeded. Only a human — or a safety signal you set up in advance — knows it was a disaster.

Because the failure is behavioral rather than a crash, the response can't be improvised. The analogy is a fire drill: you do not want people reading the evacuation plan while the building burns. You want a runbook rehearsed cold, so that under pressure everyone knows the next move. For AI incidents that runbook has a fixed shape — detect → contain → diagnose → fix → review — and the single most important word in it is contain. Your very first move is not to understand the problem; it's to stop the bleeding: flip the feature to a safe fallback or hit the kill switch. Diagnosis can take twenty minutes, and "we'll have a fix in twenty minutes" is not an acceptable answer while an agent is actively doing harm.

This is exactly where the earlier sections pay off, and it's the reason they came first. Detect only works if you're emitting the four signals — a safety flag or cost spike is what fires the alert. Diagnose only works if you have the trace — it's what tells you which prompt version, model, input, and tool were involved. Contain only works if the kill switch already exists — a flag you can flip with no redeploy. Observability isn't a nice-to-have that supports incident response; it's the thing that makes the runbook executable at all.

The habit that separates teams that improve from teams that just firefight is the last step, and it's easy to skip because the fire is already out: every incident becomes a golden-set case. Capture the exact input that broke it, add it to your evals, and the same mistake can never ship again. Skip it and you'll re-live the same incident next quarter with a different name.

An LLM incident isn't always a crash — it's often the system confidently doing the wrong thing: a harmful output, a leaked record, a costly loop, a jailbreak that worked. You need a runbook for that, and observability is what makes it executable.

AI incident runbook detect → alert fires: safety flag, cost spike, quality drop, user report contain → disable the feature / flip to safe fallback / kill switch diagnose → pull the trace(s): which prompt version, model, input, tool? fix → patch prompt/guardrail/tool; run eval gate; canary review → add the failing case to the golden set so it can't recur
Every incident becomes a test caseThe single highest-leverage habit: when something goes wrong, capture the exact input that broke it and add it to your golden set (Ch 5). Your eval suite then grows to encode every real failure the system has ever had — so the same mistake can never ship twice. This is how an LLM system gets more reliable over time instead of accumulating silent regressions.
You need a kill switchBefore you need it: a feature flag or gateway toggle that instantly disables the LLM feature (or routes to a safe canned fallback) without a redeploy. When an agent is doing something harmful in production, "we'll ship a fix in 20 minutes" is not containment. The ability to stop it now is a design requirement, not an afterthought.

Common pitfalls expert

PitfallFix
Monitoring only latency & errorsTrack all four: quality, cost, latency, safety
Logging single calls, not agent tracesThread a trace id through every step of the loop
Evals only at deploy timeRun the golden set continuously against prod config
Logging raw prompts/outputs with PIIRedact, limit retention, control access to traces
No way to stop a misbehaving feature fastBuild a kill switch / feature flag before launch
Fixing an incident without a regression testAdd the failing input to the golden set every time
Treating responsible AI as a one-off reviewMake bias/transparency/safety ongoing signals

Exercises expert

Exercise O4.1 — Instrument the four signals

Context: The four signals only earn their keep when every request emits them and you can aggregate a day into a handful of numbers a stakeholder understands.

Your task: Wrap one LLM feature so every request emits a structured record with quality, cost, latency, and safety, then aggregate a day of it into four numbers.

Requirements:

  • Emit a quality signal (sampled judge score or user thumb)
  • Emit cost (tokens and dollars)
  • Emit latency (ms and time-to-first-token)
  • Emit safety (stop_reason and any guardrail hit)
  • Aggregate a day's records into four summary numbers

💡 Hint: Design the record first — if a field isn't emitted per request, you can't aggregate it later.

Exercise O4.2 — Trace an agent

Context: A trace only helps during an incident if it captures enough to answer "what happened and what did it cost?" from the trace alone — the gap you find in a drill is exactly what bites you live.

Your task: Thread a trace id through every model and tool call of an agent using the span helper, run a query that triggers a tool error, and confirm the trace pinpoints the failure and its cost.

Requirements:

  • Thread one trace id through every model and tool call
  • Trigger a tool error deliberately
  • Confirm the trace shows how many model calls ran and which tools
  • Confirm the trace shows which step errored, total latency, and total cost
  • Add any missing fields the drill reveals

💡 Hint: If you can't answer all of those questions from the trace, the missing field is your next task — add it before the real incident.

Show what to look for

You should be able to answer, from the trace alone: how many model calls, which tools ran, which step errored, total latency, and total cost. If you can't, add the missing fields — that gap is exactly what bites you during a real incident.

Exercise O4.3 — Write the incident runbook

Context: Every system needs a written five-step runbook with the specific tools named at each step — and a kill switch. If you don't have a kill switch, discovering that is the point of the exercise.

Your task: For a system you have built, write the detect→contain→diagnose→fix→review runbook with the specific tool you would use at each step, and identify your kill switch.

Requirements:

  • Name a concrete tool or signal for each of the five steps
  • Identify your kill switch explicitly
  • If there is no kill switch, name building one as the next task
  • Keep every step actionable, not aspirational

💡 Hint: Containment is impossible without a pre-built kill switch — a flag you can flip with no redeploy — so start there if you don't have one.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Instrument the four signalsBeginner

Context: Classic monitoring watches latency and errors but misses Quality and Cost — the two signals unique to LLM apps. Emitting all four from every request is the foundation drift detection, cost alerts, and safety review build on.

Your task: Given a raw model response record, extract one metric for each of the four signals: Quality, Cost, Latency, Safety.

Requirements:

  • Pull a quality signal (e.g. a judge score or thumb)
  • Compute cost in dollars from input/output token counts
  • Pull latency from the record
  • Pull a safety signal (e.g. stop_reason)
  • Return all four from one response record

💡 Hint: One response feeds all four dashboards — the point is that a single record carries quality, cost, latency, and safety together.

Show solution

Pull all four signals from one response record (pure stdlib):

def signals(resp):
    return {
        "quality":  resp.get("judge_score"),                 # LLM-judge / thumbs
        "cost_usd": round((resp["in_tok"]*3 + resp["out_tok"]*15)/1_000_000, 6),
        "latency":  resp["latency_ms"],                      # p50/p95/p99 later
        "safety":   resp["stop_reason"],                     # refusal / flags
    }

rec = {"judge_score": 0.92, "in_tok": 1200, "out_tok": 300,
       "latency_ms": 840, "stop_reason": "end_turn"}
print(signals(rec))

Classic monitoring watches latency and errors but misses Quality and Cost — the two signals unique to LLM apps. Emitting all four from every request is the foundation everything else (drift detection, cost alerts, safety review) builds on.

Exercise 2 · A tracing span helper for an agentIntermediate

Context: Multi-step agents need per-step traces. A span helper that times a step and emits a structured record lets a request's model, tool, and retrieval calls reassemble into a step tree — your debugger and audit trail in one.

Your task: Implement a span helper that times a step and emits a structured JSON record with a trace id, step name, elapsed ms, tokens, and stop reason.

Requirements:

  • Correlate records by a shared trace_id
  • Time the step from start to a done() call
  • Emit one structured JSON line per step
  • Carry step name, elapsed ms, tokens, and stop reason
  • Demonstrate timing a stand-in model call

💡 Hint: Return a closure from span() that captures the start time and, when called, prints the finished record — one line per step, keyed by trace id.

Show solution

The lesson's span helper — self-contained and runnable (prints structured logs):

import time, json, uuid

def span(trace_id, name, **fields):
    start = time.perf_counter()
    def done(**extra):
        rec = {"trace_id": trace_id, "step": name,
               "ms": round((time.perf_counter() - start) * 1000),
               **fields, **extra}
        print(json.dumps(rec))     # ship to your backend
        return rec
    return done

tid = uuid.uuid4().hex[:8]
d = span(tid, "model.plan", model="claude-opus-4-8")
time.sleep(0.01)                    # stand in for the model call
d(in_tok=1200, out_tok=300, stop="end_turn")

Each span emits one line correlated by trace_id, so a request's model calls, tool calls, and retrievals reassemble into a step tree with timing, tokens, and cost per step. That tree is both your debugger and your audit trail.

Exercise 3 · Detect quality drift on live trafficAdvanced

Context: Quality drift is answers slowly worsening — from a provider model update, corpus drift, or new user topics — and it never throws an error. Trending sampled judge scores against a baseline turns "answers feel worse" into an alert.

Your task: Implement a rolling-window drift detector that flags drift when the recent mean judge score drops more than a threshold below the baseline mean.

Requirements:

  • Compute baseline and recent mean scores
  • Compute the drop between them
  • Flag drift when the drop exceeds a threshold
  • Return the baseline, recent, drop, and flag together
  • Demonstrate a drifting window firing the flag

💡 Hint: It is just two means and a threshold — confirm any flag by replaying the golden set on the current config.

Show solution

A rolling-window drift detector (pure stdlib, runnable):

def detect_drift(baseline_scores, recent_scores, drop_threshold=0.05):
    base = sum(baseline_scores) / len(baseline_scores)
    recent = sum(recent_scores) / len(recent_scores)
    drift = base - recent
    return {
        "baseline": round(base, 3),
        "recent": round(recent, 3),
        "drop": round(drift, 3),
        "drift_flag": drift > drop_threshold,
    }

baseline = [0.91, 0.90, 0.92, 0.89, 0.90]     # last week
recent   = [0.84, 0.82, 0.83, 0.85, 0.80]     # this week
print(detect_drift(baseline, recent))          # drift_flag True

Silent quality decay — from a provider model update, corpus drift, or new user topics — never throws an error. Trending sampled judge scores against a baseline turns "answers feel worse lately" into an alert, which you confirm by replaying the golden set on the current config.

Exercise 4 · Guard the audit log as a data decisionExpert

Context: The trace is the audit trail, but full prompts and outputs may hold PII or secrets — and a log that leaks PII is itself a governance failure. You keep the operational fields but redact and truncate raw content.

Your task: Implement a redactor that keeps the who/when/version/cost fields while masking and truncating raw prompt and output content before it is logged.

Requirements:

  • Keep operational fields (user, timestamp, prompt version, model, cost)
  • Mask secret patterns such as API keys and SSNs in the content
  • Truncate the raw content preview to a capped length
  • Return the redacted record ready to log
  • Demonstrate on content that contains a fake key and SSN

💡 Hint: A regex substitution plus a length cap does the masking — the log must be complete enough to reproduce a decision yet safe enough to store.

Show solution

Redact before logging — a governance requirement, not an afterthought (pure stdlib):

import re

SECRET = re.compile(r"(sk-[A-Za-z0-9]+|\b\d{3}-\d{2}-\d{4}\b)")  # api keys, SSNs

def audit_record(raw):
    def mask(text):
        return SECRET.sub("[REDACTED]", text)[:80]   # cap + scrub
    return {
        "who": raw["user"], "when": raw["ts"],
        "prompt_version": raw["prompt_version"], "model": raw["model"],
        "cost_usd": raw["cost_usd"],
        "prompt_preview": mask(raw["prompt"]),        # scrubbed, truncated
        "output_preview": mask(raw["output"]),
    }

raw = {"user": "alice", "ts": "2026-09-07T10:00Z", "prompt_version": "v7",
       "model": "claude-opus-4-8", "cost_usd": 0.004,
       "prompt": "my key is sk-abc123 and ssn 123-45-6789",
       "output": "noted"}
print(audit_record(raw))

An audit log that leaks PII is itself a governance failure. You keep the who/when/version/cost that make decisions traceable, but redact and truncate raw content and set retention limits — the log must be complete enough to reproduce a decision yet safe enough to store.

Exercise 5 · Run the golden set on a schedule to catch model driftProfessional

Context: Deploy-time evals catch your own changes; scheduled replay catches the provider's. Providers update models silently, so pinning ids and re-running the golden set on a cron is how a silent update becomes a visible alert.

Your task: Model a scheduled golden-set replay that alerts when the pass rate drops below the gate or falls sharply versus the last run.

Requirements:

  • Compute the pass rate over a golden set with a stubbed answer function
  • Alert when the rate is below the gate
  • Alert when the rate dropped sharply versus the previous run
  • Return the rate and an alert-or-ok message
  • Simulate a provider update that breaks one answer

💡 Hint: Two triggers — an absolute gate and a relative drop-versus-last-run — catch both a bad deploy and a slow provider drift.

Show solution

Scheduled golden-set replay with a regression alert (pure stdlib):

def replay(golden, answer_fn, gate=0.90):
    passed = sum(1 for c in golden if c["must_include"] in answer_fn(c["q"]))
    return passed / len(golden)

def scheduled_check(golden, answer_fn, last_rate, gate=0.90):
    rate = replay(golden, answer_fn, gate)
    alert = ""
    if rate < gate:
        alert = f"ALERT: pass rate {rate:.0%} below gate {gate:.0%}"
    elif last_rate is not None and last_rate - rate > 0.05:
        alert = f"ALERT: dropped {last_rate:.0%} -> {rate:.0%} (model drift?)"
    return rate, alert or "ok"

golden = [{"q": "refund window?", "must_include": "30 days"},
          {"q": "reset password?", "must_include": "settings"}]
# simulate a provider model update that broke one answer:
def answer_after_update(q):
    return {"refund window?": "14 days", "reset password?": "settings"}[q]
print(scheduled_check(golden, answer_after_update, last_rate=1.0))

Deploy-time evals catch your own changes; scheduled replay catches the provider's. Pinning ids and re-running the golden set on a cron (and on every version bump) is how a silent model update becomes a visible alert instead of a slow, unexplained quality slide.

Exercise 6 · Run the AI incident and close the loopIndustry scenario

Context: As incident lead you execute the runbook — detect, contain, diagnose, fix, review — where contain flips a pre-built kill switch and review adds the failing case to the golden set so the same mistake cannot ship twice.

Your task: Drive the incident runbook programmatically as an executable checklist that ends by adding the failing case to the golden set.

Requirements:

  • Walk the five steps: detect, contain, diagnose, fix, review
  • Flip a kill switch to a safe fallback in the contain step
  • Use the trace to name the failing step in diagnose
  • Add the failing case to the golden set in review
  • Return the ordered log of what the runbook did

💡 Hint: Containment needs a flag (no redeploy) and diagnosis needs the trace — which is why the kill switch and audit trail are design requirements, not reactions.

Show solution

The incident runbook as an executable checklist (pure stdlib):

def run_incident(signal, kill_switch, trace, golden):
    log = []
    log.append(f"DETECT: alert fired on {signal}")
    kill_switch["enabled"] = False                 # CONTAIN: flip to safe fallback
    log.append("CONTAIN: kill switch flipped -> safe canned fallback")
    cause = trace.get("failing_step", "unknown")
    log.append(f"DIAGNOSE: pulled trace, root cause at step '{cause}'")
    log.append("FIX: patched guardrail; ran eval gate + canary")
    golden.append({"q": trace["input"], "must_include": trace["expected"]})
    log.append(f"REVIEW: added failing case to golden set (now {len(golden)} cases)")
    return log

ks = {"enabled": True}
golden = [{"q": "x", "must_include": "y"}]
trace = {"failing_step": "tool.refund", "input": "refund $9999",
         "expected": "requires approval"}
for line in run_incident("cost_spike", ks, trace, golden):
    print(line)

The lead's discipline is the last step: every incident becomes a golden-set case, so the same mistake cannot ship twice. Containment needs a pre-built kill switch (a flag, no redeploy), and diagnosis needs the trace — which is why the audit trail and kill switch are design requirements, not reactions.

✓ Checkpoint — you can move on when you can…

  • Name the four signals and why cost & quality are LLM-specific.
  • Trace an agent request and read the step tree.
  • Describe input, quality, model, and cost drift and how to catch each.
  • List the governance controls and why logging is a data decision.
  • Run the AI incident runbook and explain the kill switch & golden-set feedback loop.
🏗️ Toward the capstone — module completeThe AI DevOps Engineer is the ultimate test of this chapter: it acts on real infrastructure, so its audit log, four-signal dashboard, per-action approval trail, and kill switch aren't nice-to-haves — they're what make it safe to run at all. Every incident it encounters becomes a new eval case (Chapter 8d). You've now completed MLOps & LLMOps: foundations → stack → deploy/scale → monitor/govern — the full operational discipline behind a trustworthy production agent. See the evals + go-real build →

Knowledge check check yourself

✓ Knowledge check

Classic monitoring watches latency and errors. Which two additional signals does an LLM app require, and why can't the classic two catch them?

Show answer
Cost (it's metered per token and can quietly balloon) and quality (a successful HTTP 200 can still be wrong or hallucinated). A green latency/error dashboard can hide a quality collapse or a cost spike, so every request should emit all four: quality, cost, latency, safety.
✓ Knowledge check

Why is a single log line insufficient for an agent, and what does the lesson's 'every incident becomes a test case' habit achieve?

Show answer
An agent is a loop of model calls, tool calls, and retrievals, so you need a trace — the nested, timed tree of steps — to see which step (retrieval, tool, or reasoning) failed. Capturing each failure's exact input into the golden set makes the eval suite encode every real failure, so the same mistake can't ship twice.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in