Lab 7 · Domain 4 · 16%

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.

Answer key Lab7_Complete.ipynb
Exam objectives covered
  • 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
The thing that changed is the thing that broke. Before you form any theory, list what moved: the prompt, the model version, the corpus, the index, the traffic. The exam builds whole items around a single held-constant clause (“latency and model version are unchanged”) precisely to point you at the one variable that did move. A candidate who reaches for “the model is hallucinating” when the data was refreshed last night has skipped the differential.

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.

SymptomMost likely planeFirst checkDistinguishing evidence
Confidently wrong answers after a data refreshContext / retrievalInspect the chunks actually retrieved for the failing queryRetrieved chunks are stale or irrelevant; the model faithfully used bad context
Answers degrade only on long inputsContext (window / pruning)Compare output on the full input vs. a trimmed oneQuality returns when the input fits; something is being truncated or pruned
Output violates the schemaEgress / prompt contractCheck whether output_config.format is set and validatedFree-text where structure was required; no downstream validator rejected it
An instruction buried mid-prompt is ignoredPrompt structureMove the instruction to the system block or the endBehavior changes with position, not with wording
Correct on easy cases, fails on hard reasoningReasoning (model / effort)Re-run the hard case with a stronger model at the same promptStronger model or higher effort passes it; the task exceeded the tier
Sudden latency spike, prompt unchangedInfrastructure / retrievalLook at p95 of the retrieval call and cache-hit rateThe model call time is flat; the time moved to a dependency
Output cut off mid-sentenceEgressRead stop_reasonstop_reason == "max_tokens" — a cap, not a reasoning failure
Hallucinated tool argumentsReasoning / tool schemaRead the tool's input_schema and descriptionAmbiguous or under-described parameters; the model guessed a plausible shape
Agent loops foreverReasoning / orchestrationCheck for a termination condition and an iteration capNo 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.

“Confident and wrong” after a data change is a retrieval symptom until proven otherwise. The guide's own wrong answer for this item (“the temperature setting is too low”) is a trap on two counts: that parameter was removed from current models and now returns a 400, and even on the models that had it, it was never a hallucination control. Candidates who reach for a sampling knob have skipped every plane between the prompt and the model.

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.

KindWhat actually happenedPlaneArchitectural fix
No groundingThe answer was never in the contextContext / retrievalFix retrieval; require “insufficient context” as a valid output
Grounding present but ignoredThe fact was in context; the model didn't use itPrompt / attentionForce citations and validate that the cited span exists
Grounding contradictoryThe corpus itself disagreesData qualityDeduplicate and reconcile sources; the model can't resolve what people didn't
Out of distributionThe task is genuinely beyond the modelReasoning / viabilityStronger 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.)

Hallucination is a symptom, not a diagnosis. When an item offers “the model is hallucinating” as an answer, it is usually the distractor: a restatement of the symptom dressed up as a cause. The credential-worthy answer names the plane: which hallucination, and therefore which fix.

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.

AblationHold constantIf it now passes …… the bug was
Run the failing case with a stronger modelSame prompt, same contextPassesPrompt underspecified or model mismatched; disambiguate below
Simplify the case to its easiest formSame model, same promptPasses on easy, fails on hardModel mismatch (raise the tier or effort)
Run with a hand-built perfect contextSame model, same promptPassesRetrieval, not reasoning
Run with the prompt's instruction moved to the system blockSame model, same contextPassesPrompt 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.

python
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.

LeverImprovesCosts / when it is wrong
Prompt cachingCost and latency togetherNeeds 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 API50% cost24h window, single-turn; results are unordered; key by custom_id. Destroys latency, never for an interactive path
StreamingPerceived latency / TTFTDoes not reduce cost. Same tokens, same price; only the wait feels shorter
Model routingCost on the easy sliceHaiku for classification, Opus for synthesis. Haiku's context is 200K; the others 1M, so don't route long inputs to Haiku
Lower effortFewer thinking tokensLower quality on hard reasoning. Right for subagents and simple tasks, wrong for the case in section 4
Retrieval top-k down / rerankFewer input tokensRisk of dropping the one chunk the answer needed, which re-introduces section 3's first failure
Context pruning / compactionBounded context on long agentsSummarization is lossy; the pruned turn may be the one that mattered
Lower max_tokensNothing, reallyA 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.

python
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.

InstrumentAnswersGranularity
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.

python
# 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.ipynb

Objective: 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.

  1. 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.
  2. 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.
  3. 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?
  4. 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.
  5. 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.
  6. 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?
  • A. The reasoning plane: the model has started hallucinating and needs a stronger tier.
  • B. The retrieval / indexing step: the reload likely produced stale or mismatched chunks.
  • C. The egress plane: add a schema validator to reject the bad answers.
  • D. Lower the temperature to make the answers less creative and more grounded.

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?
  • A. Retrieval is broken; the context never contained the fields.
  • B. The original model can do the task, but the hard documents exceeded its tier; raise the tier or effort.
  • C. The prompt is underspecified and must be rewritten before anything else.
  • D. The output schema is wrong and is silently dropping fields.

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.)
  • A. Place the corpus at the start of the system block and enable prompt caching.
  • B. Move the workload to the Batches API for the 50% discount.
  • C. Stream the response so the user sees the first token sooner.
  • D. Cut max_tokens in half to reduce the per-request cost.
  • E. Drop the corpus to its first 5,000 tokens to shrink the input.

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?
  • A. Prompt caching stopped working after the routing change.
  • B. The cheaper model resolves fewer tickets on the first try, so retries and escalations raised the cost per resolved task above the savings.
  • C. Streaming was disabled, which increased token cost.
  • D. The dashboards are wrong; per-request cost cannot fall while the bill rises.

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_tokens is a cap, not a cost control.
  • usage.input_tokens is the uncached remainder only; true prompt size adds cache_read_input_tokens and cache_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.