Diagnosis & Optimization
A production system is misbehaving and every option on the screen is plausible. The architect's job is not to guess the fix; it is to run the differential: map the symptom to the plane most likely responsible, name the cheapest test that would confirm it, and only then touch anything. This lab is about diagnosing before optimizing, and optimizing the number that actually matters.
- Diagnose system issues (prompt failure, hallucinations, model mismatch)
- Optimize token usage, latency, and cost-performance trade-offs
- Monitor system performance using logging and observability tools
1. Differential Diagnosis: Symptom → Plane
Lab 1 gave you five planes: ingress, context, reasoning, egress, feedback. Diagnosis is that map read backwards: a symptom is evidence about which plane failed. The skill the exam tests is not knowing every fix; it is refusing to fix the wrong plane. Almost every wrong answer in this domain is a real remedy applied to the plane that wasn't broken.
This is the table to internalize. Read a symptom, name the plane, name the one cheap check that distinguishes it from its neighbors.
| Symptom | Most likely plane | First check | Distinguishing evidence |
|---|---|---|---|
| Confidently wrong answers after a data refresh | Context / retrieval | Inspect the chunks actually retrieved for the failing query | Retrieved chunks are stale or irrelevant; the model faithfully used bad context |
| Answers degrade only on long inputs | Context (window / pruning) | Compare output on the full input vs. a trimmed one | Quality returns when the input fits; something is being truncated or pruned |
| Output violates the schema | Egress / prompt contract | Check whether output_config.format is set and validated | Free-text where structure was required; no downstream validator rejected it |
| An instruction buried mid-prompt is ignored | Prompt structure | Move the instruction to the system block or the end | Behavior changes with position, not with wording |
| Correct on easy cases, fails on hard reasoning | Reasoning (model / effort) | Re-run the hard case with a stronger model at the same prompt | Stronger model or higher effort passes it; the task exceeded the tier |
| Sudden latency spike, prompt unchanged | Infrastructure / retrieval | Look at p95 of the retrieval call and cache-hit rate | The model call time is flat; the time moved to a dependency |
| Output cut off mid-sentence | Egress | Read stop_reason | stop_reason == "max_tokens" — a cap, not a reasoning failure |
| Hallucinated tool arguments | Reasoning / tool schema | Read the tool's input_schema and description | Ambiguous or under-described parameters; the model guessed a plausible shape |
| Agent loops forever | Reasoning / orchestration | Check for a termination condition and an iteration cap | No stop state; nothing ever returns stop_reason == "end_turn" |
Notice how few of these rows point at “the model.” That is the point. In a well-built system the reasoning plane is usually the last suspect, because the other four planes are cheaper to break and cheaper to check.
2. The Guide's Sample 3: The Thing That Changed
This is one of the guide's own sample items, and it is worth teaching in full because it is the differential in miniature:
A RAG system suddenly returns confident but incorrect answers after a document refresh, while latency and model version are unchanged. What is the most likely first place to investigate?
Answer: the retrieval / indexing step is returning irrelevant or stale chunks: a broken re-index, mismatched embeddings, a dimension or model mismatch between the query encoder and the corpus encoder. The reasoning generalizes into a rule you can apply to any incident:
Model unchanged + latency unchanged + data refreshed → the data plane. Two variables were explicitly held constant so you would stop suspecting them. One variable moved. The confident-but-wrong shape is the tell: the model is doing exactly what it should (answering from its context) and the context is poisoned. A confident wrong answer grounded in a bad chunk looks identical to a hallucination and is not one.
3. Hallucination Is a Symptom, Not a Diagnosis
“The model hallucinated” is where lazy incident reviews stop. An architect decomposes it, because the four causes have four different fixes and three of them are not in the model at all.
| Kind | What actually happened | Plane | Architectural fix |
|---|---|---|---|
| No grounding | The answer was never in the context | Context / retrieval | Fix retrieval; require “insufficient context” as a valid output |
| Grounding present but ignored | The fact was in context; the model didn't use it | Prompt / attention | Force citations and validate that the cited span exists |
| Grounding contradictory | The corpus itself disagrees | Data quality | Deduplicate and reconcile sources; the model can't resolve what people didn't |
| Out of distribution | The task is genuinely beyond the model | Reasoning / viability | Stronger model, or accept the task is not viable (Lab 1) |
The architectural move for the second, most common kind is specific: require a citation and reject any output whose cited span does not appear in the retrieved context. That converts a soft “be accurate” into a hard validator the egress plane can enforce. The naive reflex (raise effort and hope, or reach for a sampling knob) is not a fix. (There is no temperature control on current models to turn down; setting it returns a 400.)
4. Prompt Failure vs. Model Mismatch: The Ablation Ladder
Two failures look identical from the outside (a wrong answer) and have opposite fixes. Rewriting the prompt when the model was under-tiered wastes a week; upgrading the model when the prompt was underspecified wastes money forever. You tell them apart with cheap ablations, run in order, each one holding everything constant but one variable.
| Ablation | Hold constant | If it now passes … | … the bug was |
|---|---|---|---|
| Run the failing case with a stronger model | Same prompt, same context | Passes | Prompt underspecified or model mismatched; disambiguate below |
| Simplify the case to its easiest form | Same model, same prompt | Passes on easy, fails on hard | Model mismatch (raise the tier or effort) |
| Run with a hand-built perfect context | Same model, same prompt | Passes | Retrieval, not reasoning |
| Run with the prompt's instruction moved to the system block | Same model, same context | Passes | Prompt structure (position, not wording) |
The disambiguation in row one is the crux. A stronger model passing tells you the current tier was insufficient for this prompt, but a better prompt might have carried the weaker tier. Run the simplify-the-case ablation next: if the weak model handles the easy version, the model can do the task and your prompt was the problem; if it fails even the easy version, the tier was genuinely too low. That is the whole domain in two API calls.
The single most useful ablation is the third: swap retrieved context for a hand-built perfect context. If the case passes on perfect context, you have proven the reasoning plane is fine and localized the bug to retrieval. No prompt engineering required.
import anthropic
client = anthropic.Anthropic()
# The core ablation: same failing case, two sources of context.
# If the hand-built context passes and the retrieved one fails,
# the bug lives in retrieval -- not in the model's reasoning.
def run_case(question, context_text):
return client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
thinking={"type": "adaptive"},
output_config={"effort": "high"},
system=[{"type": "text", "text": context_text}],
messages=[{"role": "user", "content": question}],
)
gold = run_case(FAILING_QUESTION, HAND_BUILT_PERFECT_CONTEXT)
live = run_case(FAILING_QUESTION, retrieve(FAILING_QUESTION))
# gold correct + live wrong -> retrieval plane (fix the index / embeddings)
# both wrong -> reasoning: raise the tier, or the task isn't viable
# both correct -> the incident isn't reproducible; check ingress / traffic
5. Optimization: The Lever Table
Once the system is correct, you make it cheaper and faster, and this is where architects destroy value by optimizing the wrong metric. Every lever below improves something and costs something. The exam rewards knowing the cost, not just the benefit.
| Lever | Improves | Costs / when it is wrong |
|---|---|---|
| Prompt caching | Cost and latency together | Needs a stable prefix ≥ 4096 tokens on Opus 4.8; reads bill ~0.1×, writes 1.25× (5-min) / 2× (1-hour). Breaks silently if anything volatile sits in the prefix |
| Batches API | 50% cost | 24h window, single-turn; results are unordered; key by custom_id. Destroys latency, never for an interactive path |
| Streaming | Perceived latency / TTFT | Does not reduce cost. Same tokens, same price; only the wait feels shorter |
| Model routing | Cost on the easy slice | Haiku for classification, Opus for synthesis. Haiku's context is 200K; the others 1M, so don't route long inputs to Haiku |
Lower effort | Fewer thinking tokens | Lower quality on hard reasoning. Right for subagents and simple tasks, wrong for the case in section 4 |
| Retrieval top-k down / rerank | Fewer input tokens | Risk of dropping the one chunk the answer needed, which re-introduces section 3's first failure |
| Context pruning / compaction | Bounded context on long agents | Summarization is lossy; the pruned turn may be the one that mattered |
Lower max_tokens | Nothing, really | A cap, not a cost control; it truncates the output (stop_reason == "max_tokens"). You pay for what was generated regardless |
Caching is the standout because it is the rare lever that moves cost and latency in the same direction, which is exactly why it is the answer whenever an item asks for both. But it hides a measurement trap.
usage.input_tokens is the uncached remainder only. It excludes cache reads entirely. A cost calculation of input_tokens × rate undercounts every cached request, because the tokens served from cache never appear in that field. True prompt size is input_tokens + cache_read_input_tokens + cache_creation_input_tokens. Verify caching is actually working by reading cache_read_input_tokens: if it is zero across identical-prefix requests, something volatile (a timestamp, a UUID, an unsorted JSON dump) is invalidating the prefix.
The arithmetic that separates architects from tinkerers: optimize cost per resolved task, not cost per request. A cheaper model or a lower effort can cut per-request cost 30% while doubling the retry rate, and two cheap wrong calls plus one expensive right one cost more than one right call at full price. The falling per-request number is the classic false win.
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=[{"type": "text", "text": STABLE_PREFIX,
"cache_control": {"type": "ephemeral"}}],
messages=[{"role": "user", "content": question}],
)
u = resp.usage
OPUS_IN = 5.00 / 1_000_000 # $/token, input
# input_tokens is ONLY the uncached remainder. Add the cache tiers back
# or every cost figure you report is too low.
true_input_cost = (
u.input_tokens * OPUS_IN
+ u.cache_read_input_tokens * OPUS_IN * 0.10 # reads ~0.1x
+ u.cache_creation_input_tokens * OPUS_IN * 1.25 # 5-min writes ~1.25x
)
total_prompt = (u.input_tokens
+ u.cache_read_input_tokens
+ u.cache_creation_input_tokens)
hit_rate = u.cache_read_input_tokens / max(total_prompt, 1)
# A collapsing hit rate is the earliest signal of a poisoned prefix.
assert hit_rate > 0.5, f"cache collapsed to {hit_rate:.0%} -- audit the prefix"
# Emit the request id (public despite the underscore) with every log line.
log.info("call", request_id=resp._request_id,
hit_rate=round(hit_rate, 3), cost=round(true_input_cost, 5))
6. Monitoring in Production
You cannot diagnose what you did not record. The feedback plane from Lab 1 is what makes every earlier section possible, and it has three distinct instruments, each answering a different question.
| Instrument | Answers | Granularity |
|---|---|---|
| Metrics | “Is the fleet healthy right now?” | Aggregate, cheap, alertable (error rate, p95, cost/task) |
| Traces | “Where did this one request spend its time and tokens?” | Per-request span tree: retrieval, model, tools, egress |
| Evals | “Is the output still correct?” | Offline, against a labelled set (Lab 6). The only thing that catches a quality regression metrics can't see |
Metrics tell you something is wrong; traces tell you where; evals tell you whether the fix actually restored quality. A system with all three green dashboards and no eval set is one silent prompt-deploy away from shipping confidently-wrong answers with perfect uptime.
What to alert on, and one non-obvious leading indicator:
- Error rate and p95 latency — the table stakes.
- Cost per resolved task — not per request, per section 5.
- Guardrail-violation rate — schema rejections, refusals, failed citation checks.
- Cache-hit rate falling — the earliest signal that someone put a timestamp or a per-request ID at the front of the prompt. Quality and correctness are unchanged; cost quietly doubles days before anyone notices the bill.
Emit response._request_id (public despite the leading underscore) with every log line. It is the one identifier that lets Anthropic trace a specific failing call end-to-end, and the first thing support will ask for.
# Budget a prompt BEFORE sending it -- observability starts at design time.
# Never estimate with tiktoken: it is OpenAI's tokenizer and undercounts
# Claude by ~15-20% (much more on code), so it will lie about your window.
count = client.messages.count_tokens(
model="claude-opus-4-8",
system=[{"type": "text", "text": STABLE_PREFIX}],
messages=[{"role": "user", "content": question}],
)
print("prompt tokens:", count.input_tokens)
if count.input_tokens >= 4096:
print("prefix is long enough to cache on Opus 4.8 (>= 4096 tokens)")
else:
print("below the Opus 4.8 cache minimum -- cache_control will silently no-op")
7. Design Exercise
Self-driven lab Lab7_Self_Driven_Lab.ipynbObjective: run a real on-call differential end to end. About 45 minutes. Do not propose a fix until step 4.
Scenario. You are paged at 08:00. Overnight: support-answer quality dropped 12% (measured against your eval set); cost per request fell 30%; p95 latency is unchanged. Two things deployed last night: a prompt change and a corpus re-index. Both landed within the same hour. Leadership wants to know what to roll back.
- List what changed. Two variables moved, one held constant (latency). Write the one sentence that rules out an infrastructure or model-tier cause using the constant.
- Read the cost drop as evidence, not good news. Quality fell and cost fell together. What single mechanism produces both at once? (Hint: a shorter effective prompt, whether a truncated prefix, a dropped context block, or a cache-miss pattern that changed what actually reaches the model, sends fewer tokens and starves the answer.) Name the two deploys that could each cause it.
- Design the ablation. You have two suspects and need to isolate one. Write the two runs: the failing eval slice on last night's prompt + yesterday's index, and on yesterday's prompt + last night's index. Which result convicts the re-index? Which convicts the prompt?
- Name the metric that reveals the culprit. Which single number (retrieved-chunk relevance, average input tokens per call, cache-hit rate, cited-span-exists rate) would have caught this before the eval score moved? State why the cost drop points you at it.
- Write the rollback decision rule. A sentence of the form: “Roll back X if the ablation shows Y; otherwise roll back Z.” Force yourself to commit to one, not both.
- Close the feedback loop. What do you add to the deploy pipeline so a quality-down / cost-down pair pages you automatically next time, instead of a human noticing at 08:00?
Step 2 is the one the exam tests: the cost drop is the clue, not the reassurance. Step 3 is the one that gets you the right answer instead of a plausible one.
Check Yourself
Exam-style items. Commit to an answer before expanding.
A customer-support assistant starts returning answers that are fluent, confident, and wrong. Nothing shipped to the prompt or the model this week, but the knowledge base was bulk-reloaded yesterday. p95 latency is flat. Where do you investigate first?
Correct: B. The model and prompt are held constant and latency is flat; the one thing that moved is the corpus. Confident-and-wrong is the signature of the model faithfully answering from poisoned context. Investigate the chunks actually retrieved for a failing query before touching anything else.
A restates the symptom as a cause and reaches for the most expensive fix on the least likely plane. C treats the output as malformed, but a fluent wrong answer passes any schema; the problem is truthfulness, not shape. D is a non-starter: that sampling parameter was removed from current models and returns a 400, and it never controlled hallucination even when it existed. The thing that changed is the thing that broke.
An extraction task fails on complex documents. You re-run one failing case unchanged on a stronger model and it passes; you then re-run the failing case on the original model with a hand-simplified version of the same document and it also passes. What have you learned?
Correct: B. Two ablations, each holding all but one variable. A stronger model passing narrows it to prompt-or-tier. The original model then passing on the simplified document proves the model can do the task when it isn't hard, so the difficulty, not the prompt, was the wall. That is model mismatch: raise the tier or the effort for the hard slice.
A is ruled out because the fields were plainly present; the simplified run on the same model succeeded. C would be the conclusion only if the weak model failed even the easy case; it didn't. D describes an egress problem, but the failure was wrong extractions, not missing structure. The ablation ladder disambiguates prompt failure from model mismatch in exactly these two runs.
A retrieval pipeline sends a 40,000-token policy corpus with every request, then a short question. Leadership wants both a lower bill and a faster first token for the user. Which two changes advance both goals? (Choose two.)
Correct: A and C. Caching a stable prefix cuts per-request cost (reads bill ~0.1×) and time-to-first-token, because the prefix isn't reprocessed. Streaming doesn't cut cost, but the stated goal was a faster first token; streaming reduces perceived latency to first token, exactly the metric named.
B halves cost but annihilates latency: batches run on a 24h window with no interactive SLA, so a waiting user is out of the question. D is a cost myth: max_tokens is a cap that truncates output; you pay for tokens generated regardless, so it doesn't reliably lower the bill and risks stop_reason == "max_tokens". E cuts cost by discarding policy the answer may need; that is section 3's “no grounding” failure, reintroduced on purpose.
After a routing change that sends easy tickets to a cheaper model, the per-request cost dropped 22% but the monthly bill for the ticket queue rose. Quality dashboards are green. What is the most likely explanation?
Correct: B. Per-request cost is the wrong denominator. If the cheap model needs two attempts and an escalation where the strong model needed one, the cost per resolved task rises even as each individual call gets cheaper. The falling per-request number is the classic false win, and the rising bill is the real signal.
A would show up as a collapsing cache-hit rate and a higher input-token cost, not this pattern, and nothing about routing implicates the cache prefix. C is false: streaming never changes token cost, only perceived latency. D is exactly backwards: cheaper-but-more-often is precisely how per-request cost and total cost move in opposite directions. Optimize cost per resolved task, not cost per request.
Key Takeaways
- The thing that changed is the thing that broke. List what moved before forming a theory; the held-constant clauses in a scenario are pointing you at the one variable that didn't stay still.
- Diagnosis is the five planes read backwards: a symptom is evidence about which plane failed. Fixing the wrong plane is the most common wrong answer in the domain.
- Model unchanged + latency unchanged + data refreshed → the data plane. Confident-and-wrong after a corpus change is retrieval until proven otherwise.
- Hallucination is a symptom, not a diagnosis. Decompose it into no-grounding, grounding-ignored, contradictory, or out-of-distribution, and force citations you can validate rather than “raise effort and hope.”
- Separate prompt failure from model mismatch with the ablation ladder: stronger model, simplified case, hand-built context, moved instruction; each holds all but one variable.
- Caching moves cost and latency together; batching trades latency for cost; streaming does not reduce cost;
max_tokensis a cap, not a cost control. usage.input_tokensis the uncached remainder only; true prompt size addscache_read_input_tokensandcache_creation_input_tokens. Naive cost math undercounts every cached request.- Optimize cost per resolved task, not per request. A falling per-request cost while quality drops (or retries rise) is not a win. Alert on a falling cache-hit rate; it is the earliest sign of a poisoned prefix.