Lab 3 · Domain 2 · 13%

Claude Models, Prompting & Context Engineering

This domain is where candidates who write good prompts fail and architects who understand economics pass. The exam does not ask you to write the cleverest prompt. It asks which model a stage should run on, why a guardrail in the system prompt does not actually enforce anything, and what happens to the bill when you put the varying content before the stable content. Every item here has a defensible number behind it: a token price, a cache multiplier, a context ceiling. Know the numbers.

Answer key Lab3_Complete.ipynb
Exam objectives covered
  • Select appropriate Claude models based on trade-offs
  • Design system prompts, templates, and guardrails
  • Apply prompt engineering techniques (zero-shot, few-shot, chain-of-thought)
  • Optimize context windows and manage token usage
  • Implement prompt reuse strategies (caching, modular prompts, Skills)
The most-tested single fact in this domain is a billing gotcha. usage.input_tokens is the uncached remainder only, the tokens processed at full price. The cached portion is reported separately in usage.cache_read_input_tokens. If you multiply input_tokens by the input price to estimate cost, you undercount every request that hit the cache, and you will conclude caching "didn't help" when it saved you 90%. Total prompt size is the sum of all three usage fields, never the one.

1. Model Selection Is a Trade-Off Table, Not a Preference

An architect does not have a favorite model. They have a routing policy: a mapping from task shape to the cheapest model that clears the quality bar for that task. The exam tests whether you can read that mapping off a table of real specifications rather than reaching for the biggest name.

ModelModel IDContextInput $/1MOutput $/1M
Claude Opus 4.8claude-opus-4-81M$5.00$25.00
Claude Sonnet 5claude-sonnet-51M$3.00$15.00
Claude Haiku 4.5claude-haiku-4-5200K$1.00$5.00
Claude Fable 5claude-fable-51M$10.00$50.00

Read the table before you read the question. Three facts jump out and each is an exam item:

  • Haiku's context is the odd one out. Every other model here carries a 1M window; Haiku 4.5 caps at 200K. If a scenario feeds a stage 400K tokens of retrieved context and offers Haiku as the "cost-optimized" answer, Haiku is disqualified by the context limit, not merely a weaker choice. The request would fail.
  • Output is five times the input price on every model. A model that "thinks out loud" or emits a verbose rationale costs far more per token on the way out than in. Output-token discipline is a cost lever, not a style preference.
  • Fable 5 is the most expensive tier, above Opus. It is not the default upgrade. Reach for it only when a task genuinely needs the most capable widely released model; otherwise Opus 4.8 is the top of the routing ladder.

The routing discipline

Route on task fit, not on a blanket cost rule. Cheap, high-volume, well-specified work (classification, extraction, routing, short summaries) goes to Haiku. Hard synthesis, long-horizon reasoning, and anything where a wrong answer is expensive goes to Opus. The middle (production workloads that need near-Opus quality at lower cost) is Sonnet's territory.

Task shapeRoute toWhy not the cheaper tier
Binary / enum classification, PII detection, intent routingHaiku 4.5Nothing cheaper exists, and the task is fully specified
High-volume summarization, structured extractionSonnet 5Haiku may drop nuance; Sonnet is the volume sweet spot
Multi-step synthesis, ambiguous judgment, code generationOpus 4.8A wrong answer here costs more than the token delta
Frontier reasoning, longest-horizon autonomous workFable 5Only when Opus is demonstrably insufficient
"Switch to the smallest model regardless of task fit" is always a wrong answer. The exam constructs it as a tempting cost optimization, but downgrading a hard synthesis stage to Haiku to save $4/1M does not save money; it produces wrong outputs that get retried, escalated, or shipped and later corrected. Cost per correct output is the metric, and a cheaper model that fails the task has an infinite cost per correct output. Route down only when the smaller model still clears the bar.

2. Effort and Adaptive Thinking: The Architectural Dial

Beyond model choice, two request-level controls trade tokens and latency for answer quality. Together they are the finest-grained cost/quality dial you have, and the exam expects you to know they exist and default correctly.

  • Adaptive thinkingthinking={"type": "adaptive"} lets Claude decide when and how deeply to reason. On the Opus tier, omitting the thinking parameter means the model runs with no thinking at all. If you want reasoning, you must ask for it explicitly.
  • Effortoutput_config={"effort": ...} sets how much the model deliberates and spends overall. The default is high. Lower effort means fewer tokens and lower latency at some cost to depth; raise it when correctness matters more than the bill.
Current — adaptive thinking with an effort dial, correct nesting
python
import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=8000,
    # On Opus-tier, omitting this means NO thinking. Ask for it explicitly.
    thinking={"type": "adaptive"},
    # effort is NESTED inside output_config, not a top-level parameter.
    output_config={"effort": "high"},   # low | medium | high | xhigh | max
    messages=[{"role": "user", "content": task}],
)

The nesting is a real exam trap. effort lives inside output_config. A top-level effort= or a top-level output_format= is the wrong shape; structured output is likewise output_config={"format": {...}}. And note what is absent: there is no temperature, no top_p, no top_k. Those sampling parameters have been removed from the current models and now return HTTP 400.

Deprecated — returns HTTP 400 on current models
python
# Every line below is now REJECTED on Opus 4.8 / Sonnet 5 / Fable 5:
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=8000,
    thinking={"type": "enabled", "budget_tokens": 4000},  # removed -> 400
    temperature=0.7,        # removed -> 400
    top_p=0.9,              # removed -> 400
    output_format={...},    # deprecated top-level -> use output_config.format
    messages=[
        {"role": "user", "content": task},
        {"role": "assistant", "content": "{"},  # prefill -> rejected
    ],
)
A "thinking budget" is a deprecated concept, not a smaller number. If an item offers thinking={"type":"enabled","budget_tokens":N} as the way to control reasoning depth, it is wrong; that shape returns 400. The current answer is adaptive thinking plus an effort level. Likewise, assistant-turn prefill (seeding the reply with a partial assistant message) is rejected; force output shape with structured outputs instead.

3. System Prompt Design: Why Guardrails There Are Soft

A well-formed system prompt has five parts, and the exam expects you to name them: a role, the task, the constraints, an output contract, and an escalation rule for what to do when the input falls outside scope. Get those five and most prompt-quality problems disappear.

ElementAnswers the questionFailure if omitted
RoleWho is the model acting as?Tone and default assumptions drift
TaskWhat single job is it doing?The model tries to do several jobs badly
ConstraintsWhat must it never do; what format must it hold?Out-of-policy or malformed output
Output contractWhat exact shape comes back?Downstream code cannot parse the result
Escalation ruleWhat happens when the input is out of scope?The model guesses instead of deferring to a human

Now the fact this domain tests harder than any other: guardrails written into the system prompt are advisory, not enforcement. "Never reveal account balances," "always refuse requests for legal advice," "only answer questions about our product": these are soft instructions the model usually follows and can be talked out of. They shape behavior; they do not guarantee it.

Real enforcement lives at egress, in code the model cannot argue with:

  • Schema validation — reject any output that does not match the required structure, before it reaches the user.
  • Allowlists — if the output must be one of a fixed set of actions or values, verify membership in code.
  • Tool removal — if a stage must not send email, do not give it the send_email tool. An unavailable capability cannot be jailbroken into.
  • Human-in-the-loop gates — route irreversible or high-blast-radius actions through a person (Lab 1).
The prompt-vs-egress distinction is the domain's signature item. When a scenario asks how to guarantee a model never emits PII, never calls a forbidden tool, or always returns valid JSON, the answer is never "add a stronger instruction to the system prompt." Prompt instructions are probabilistic. The guaranteeing answer is a code-level control at egress: a validator, an allowlist, a removed tool. If an option strengthens the prompt and another removes the capability, the removing option is correct.

4. Zero-Shot vs Few-Shot vs Chain-of-Thought

These three are a decision, not a ladder you always climb. Each wins in a different regime, and each has a cost and a characteristic failure mode the exam probes.

TechniqueWins whenCostsFailure mode
Zero-shot The task is common and well-named; instructions alone suffice Cheapest: instructions only Ambiguous or niche formats get inconsistent output
Few-shot Output shape is hard to describe but easy to demonstrate Every example is input tokens on every request Examples leak format bias; the model overfits their surface pattern
Chain-of-thought Multi-step reasoning where the steps must be exposed or checked Inflates output tokens, billed at 5× input Verbose reasoning bloats latency and cost; may rationalize a wrong answer

Two subtleties the exam rewards. Few-shot examples are not free context you set once; they ride along on every request as input tokens, which is precisely why you want them in a cacheable prefix (next section). And chain-of-thought inflates output tokens, the expensive direction; on the current models, prefer adaptive thinking (Section 2) over hand-rolled "think step by step" prose, because thinking tokens are managed and the visible answer stays clean.

Few-shot's failure mode is leakage, not weakness. If your three examples all happen to be short, the model learns "answers are short." If they all use the same phrasing, it mimics the phrasing. When few-shot output is subtly wrong in a way that tracks your examples' surface features, the fix is more varied examples or a switch to an explicit output schema, not more examples of the same kind.

5. Prompt Caching: the Exam's Favorite Lever

No mechanism in this domain is tested more than prompt caching, because it is the rare lever that improves cost and latency at once. The guide's own Sample 2 is built on it:

An application sends the same 8,000-token system prompt plus a policy document on every request, followed by a short varying user message. Both latency and cost are concerns. The correct answer is to place the static content before the dynamic content and enable prompt caching.

That is the guide's Sample 2 verbatim, and it encodes the one invariant everything else follows from: caching is a prefix match. The cache key is the exact bytes from the start of the prompt up to each breakpoint. Any change anywhere in the prefix invalidates everything after it. So stable content goes first, the varying user message goes last, and you put the cache breakpoint at the boundary.

Current — static policy first, breakpoint on the stable block, question last
python
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": POLICY_DOCUMENT,          # ~8,000 stable tokens
            "cache_control": {"type": "ephemeral"},  # breakpoint HERE
        },
    ],
    messages=[
        # The varying part comes AFTER the cached prefix.
        {"role": "user", "content": customer_question},
    ],
)

# Verify the cache actually hit:
print(response.usage.cache_read_input_tokens)   # ~8000 on a hit
print(response.usage.input_tokens)              # only the uncached remainder

Now the mistake the exam builds a distractor from, putting the varying content first, so the prefix changes every request and the cache never hits:

Deprecated — dynamic content before static; cache never hits
python
# WRONG: the question changes every request, so it invalidates the
# entire prefix that follows it. cache_read_input_tokens stays 0 forever.
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[
        {"type": "text", "text": customer_question},      # varies -> poisons prefix
        {"type": "text", "text": POLICY_DOCUMENT,
         "cache_control": {"type": "ephemeral"}},          # never reused
    ],
    messages=[{"role": "user", "content": "Answer using the policy above."}],
)

The rules the exam quizzes on caching

RuleValue
Match typePrefix only — any byte change in the prefix invalidates all breakpoints after it
Render ordertoolssystemmessages. Stable content first, in that order
Max breakpoints4 per request
Cache read price0.1× the input price
Cache write price1.25× input (5-minute TTL) or (1-hour TTL)
Minimum cacheable prefix (Opus 4.8)4096 tokens — shorter prefixes silently do not cache
Verify withusage.cache_read_input_tokens — zero across identical prefixes means a silent invalidator

The economics follow from the multipliers. A 5-minute write costs 1.25×; a read costs 0.1×. So two requests that share a prefix already win: 1.25 + 0.1 = 1.35× versus uncached. The 1-hour TTL doubles the write to 2× but keeps the entry alive across gaps, worth it only when traffic is bursty enough that a 5-minute entry would expire between requests.

Below 4096 tokens on Opus 4.8, your breakpoint does nothing, silently. There is no error. cache_creation_input_tokens comes back zero and you keep paying full price wondering why the "cache" saved nothing. The minimum cacheable prefix is model-dependent, and Opus 4.8's is 4096. A 3,000-token shared prompt that caches fine on a lower-minimum model will not cache on Opus. If an item says "caching was enabled but reads are zero," the shortlist is: prefix under the minimum, a silent invalidator (a timestamp or UUID in the prefix), or dynamic-before-static ordering.

6. Context Window Optimization

The 1M-token window is a budget, not a target. Cost is linear in tokens, so filling the window because it is available is the same mistake as buying the biggest model because it exists. The architect's job is to put the fewest tokens in front of the model that let it succeed.

TechniqueWhat it does
Retrieval, not stuffingFetch the few relevant passages instead of pasting the whole corpus (Lab 4)
Progressive disclosureLoad detail only when the task reaches for it, the model of Skills
Tool-output pruningClear stale tool results from the transcript once they are no longer needed
CompactionSummarize earlier turns server-side as a long conversation approaches the limit
Subagent isolationA subagent reads 40 files and returns a 200-token finding; the parent never carries the corpus (Lab 2)

One token-counting rule the exam states plainly: count with the API, never with a foreign tokenizer.

Current — count against the actual model
python
# Token counts are model-specific. Pass the model you will actually call.
count = client.messages.count_tokens(
    model="claude-opus-4-8",
    system=[{"type": "text", "text": POLICY_DOCUMENT}],
    messages=[{"role": "user", "content": customer_question}],
)
print(count.input_tokens)

# NEVER use tiktoken here: it is OpenAI's tokenizer and UNDERCOUNTS for
# Claude (worse on code and non-English), so any budget built on it is wrong.
tiktoken is the wrong tokenizer and undercounts for Claude. It is OpenAI's, and it under-reports Claude token counts by roughly 15–20% on ordinary text and far more on code. Any context budget, cost estimate, or "will this fit" check built on tiktoken is optimistic in a way that bites you in production when the real prompt overflows the window. Use client.messages.count_tokens() with the model you will call.

7. Prompt Reuse: Caching, Modular Prompts, and Skills

Reuse is one objective with three mechanisms, and they compose. Caching (Section 5) reuses rendered bytes. Modular prompts and Skills reuse authored content in a way that keeps those bytes cacheable.

  • Modular prompts — compose the prompt from stable blocks (role, policy, format contract) plus a small volatile tail. Because the stable blocks render identically every time, they form a cacheable prefix. Modularity is what makes caching survive as the prompt evolves.
  • Skills — versioned, reusable capability folders (instructions plus resources) that Claude progressively discloses: the short description sits in context by default, and the full instructions load only when the task calls for them. That is context-window optimization and reuse in one primitive.

There is a single architecture rule that ties reuse to caching, and it is worth memorizing as one sentence: anything that varies goes last.

Current — modular assembly: stable blocks first, volatile tail last
python
# Stable, reusable blocks render byte-identical every request -> cacheable.
system_blocks = [
    {"type": "text", "text": ROLE_AND_TASK},        # module 1 (stable)
    {"type": "text", "text": OUTPUT_CONTRACT},      # module 2 (stable)
    {
        "type": "text",
        "text": POLICY_DOCUMENT,                    # module 3 (stable, large)
        "cache_control": {"type": "ephemeral"},     # breakpoint after the last stable block
    },
]

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=system_blocks,
    # Everything that varies is here, AFTER the cached prefix.
    messages=[{"role": "user", "content": customer_question}],
)
Do not interpolate the current date, a session ID, or a user name into the system prompt. Those sit at the front of the prefix and silently invalidate the cache for every request, the classic reason cache_read_input_tokens reads zero. Inject anything volatile after the last breakpoint, in a message. "Anything that varies goes last" is not a style tip; it is the difference between a 90% cost cut and paying full price on every call.

8. Design Exercise: A Support System at 40k/Day

Self-driven lab Lab3_Self_Driven_Lab.ipynb

Scenario. A support platform sends the same 8,000-token policy prefix on every request, followed by a short varying customer question. Volume is 40,000 requests per day. Leadership wants both lower cost per ticket and faster first response. Work the numbers and the routing.

  1. Confirm the shape matches Sample 2. Large stable prefix, short varying suffix, both cost and latency in play. This is the caching case. Place the 8k policy in the system array before the question, with a cache_control breakpoint on the policy block.
  2. Check the minimum. 8,000 tokens clears Opus 4.8's 4096-token minimum cacheable prefix comfortably, so the breakpoint will actually take.
  3. Compute the cache economics. Without caching, each request pays for 8,000 prefix tokens at input price. With caching, the first request of each 5-minute window writes at 1.25× and every subsequent request reads at 0.1×. At 40k/day the prefix is re-read constantly, so almost every request pays 0.1× instead of 1× on the prefix, roughly a 90% reduction on the 8k prefix cost, plus lower time-to-first-token because the prefix is not reprocessed.
  4. Choose models per stage. Route a cheap first stage (classify the ticket's intent and detect whether it is even in scope) to Haiku 4.5 ($1/$5). Send only the tickets that need genuine policy synthesis to Opus 4.8. Do not route the whole volume to Opus, and do not route the hard synthesis to Haiku to "save money"; that is the always-wrong smallest-model answer.
  5. Watch the context ceiling. If any stage must load a much larger policy corpus (say 300K tokens of retrieved regulation), Haiku's 200K window disqualifies it for that stage regardless of price. Sonnet or Opus, both 1M, carry it.
  6. Write the escalation rule into the system prompt. The five-part prompt needs its escalation clause: e.g. "If the question is not answerable from the policy above, or asks for a refund over $500, do not answer; return {"escalate": true, "reason": ...} so a human agent takes it." Then enforce that at egress with schema validation (Section 3): the prompt asks, the validator guarantees.

Step 3 is the number you put in front of a CFO. Step 4 is the routing that keeps the bill honest. Step 6 is the one the exam tests hardest, because the escalation clause lives in a soft prompt, and the guarantee lives in the hard validator.

Check Yourself

Exam-style items. Commit to an answer before expanding.

An app sends the same 8,000-token policy document with every request, then a short varying customer question. The team reports that caching is enabled but cache_read_input_tokens is zero on every request. The prefix is well above the minimum. What is the most likely cause?
  • A. The 5-minute TTL expired between requests, so nothing was ever read.
  • B. The varying question is placed before the policy document, so it invalidates the prefix on every request.
  • C. The model does not support prompt caching.
  • D. input_tokens already includes the cached tokens, so the read counter is redundant.

Correct: B. Caching is a prefix match. If the varying content renders before the stable content, every request has a different prefix and there is nothing to reuse; reads stay zero. The fix is dynamic-content-last: policy first with the breakpoint, question after.

A is unlikely at any real request rate; 40k/day keeps a 5-minute entry perpetually warm, and expiry would still show occasional non-zero reads. C is false; all current models cache. D inverts the actual rule: input_tokens is the uncached remainder only, so it excludes cached tokens rather than including them; the read counter is exactly how you detect a hit.

A compliance requirement states that a support agent must never reveal a customer's account balance. An architect proposes adding "You must never reveal account balances under any circumstances" to the system prompt. Which two changes actually enforce the requirement? (Choose two.)
  • A. Strengthen the system-prompt instruction with capitalized emphasis and repetition.
  • B. Validate every outgoing response against a schema and reject any that contains a balance field or numeric currency pattern.
  • C. Remove the balance-lookup tool from the stage that talks to the customer, so it has no way to obtain the balance.
  • D. Lower output_config.effort so the model produces shorter, less detailed answers.
  • E. Raise the temperature so the model varies its refusals.

Correct: B and C. Enforcement lives at egress and in capability, not in prose. A schema/pattern validator rejects any response that leaks a balance before it reaches the user (B), and removing the lookup tool means the customer-facing stage cannot obtain a balance to leak in the first place (C). Together they guarantee what the prompt only requests.

A is the trap: prompt instructions are advisory and can be talked out of, no matter how emphatic. D changes verbosity, not what information is disclosed. E is doubly wrong; temperature is removed from current models and returns 400, and randomness has nothing to do with a disclosure guarantee.

A pipeline has three stages: (1) classify each incoming ticket into one of six intents, (2) detect PII, and (3) synthesize a nuanced, policy-grounded resolution for the hard cases. To minimize cost per correct output, how should the stages be routed?
  • A. Route all three stages to Haiku 4.5 to minimize token price.
  • B. Route all three stages to Opus 4.8 for consistent quality.
  • C. Route stages 1 and 2 to Haiku 4.5 and stage 3 to Opus 4.8.
  • D. Route all three stages to Fable 5 because it is the most capable model.

Correct: C. Classification and PII detection are cheap, well-specified tasks Haiku handles at $1/$5. The nuanced synthesis is where a wrong answer is expensive, so it earns Opus. Routing by task fit (cheap model for the easy stages, capable model for the hard one) minimizes cost per correct output.

A is the always-wrong "smallest model regardless of fit" answer: Haiku on stage 3 produces weak resolutions that get retried or escalated, so its cost per correct output is worse, not better. B overpays on two stages that Haiku clears easily. D reaches for the most expensive tier ($10/$50) with no evidence Opus is insufficient; Fable is not the default upgrade.

An engineer wants Claude to reason step-by-step before answering on the current Opus model and sets thinking={"type":"enabled","budget_tokens":4000} with temperature=0.3. The request fails with HTTP 400. What is the correct configuration?
  • A. Keep budget_tokens but remove temperature.
  • B. Use thinking={"type":"adaptive"} and output_config={"effort":"high"}; remove temperature entirely.
  • C. Move effort to a top-level parameter and keep budget_tokens.
  • D. Prefill the assistant turn with "Let me think step by step:" to force reasoning.

Correct: B. On the current models the fixed thinking-budget shape is removed and returns 400; adaptive thinking replaces it, with depth controlled by the nested output_config.effort. Sampling parameters like temperature are also removed, so the request must drop it entirely.

A still sends the deprecated budget_tokens, which is itself the 400. C keeps both a removed parameter and puts effort at the wrong level; effort must be nested inside output_config. D relies on assistant-turn prefill, which is rejected on current models; force structure with structured outputs, and get reasoning from adaptive thinking, not a prefilled preamble.

Key Takeaways

  • usage.input_tokens is the uncached remainder only. Total prompt size is the sum of input_tokens + cache_creation_input_tokens + cache_read_input_tokens. Cost math on the single field undercounts every cache hit.
  • Route by task fit: Haiku 4.5 for cheap well-specified work, Opus 4.8 for hard synthesis, Sonnet 5 for the volume middle, Fable 5 only when Opus is demonstrably insufficient. "Smallest model regardless of fit" is always wrong.
  • Haiku's 200K context is the odd one out; a stage that needs more than 200K tokens disqualifies Haiku on capacity, not price.
  • On Opus-tier, omitting thinking means no thinking. Use thinking={"type":"adaptive"} and the nested output_config={"effort":...} dial. budget_tokens, temperature, top-level output_format, and assistant prefill all return 400.
  • System-prompt guardrails are soft. Real enforcement is at egress: schema validation, allowlists, tool removal, human gates. When an item asks how to guarantee behavior, the answer is a code-level control, not a stronger instruction.
  • Caching is a prefix match: stable content first, breakpoint on the last stable block, varying content last. Order is toolssystemmessages; max 4 breakpoints; reads ≈ 0.1×, writes 1.25× (5m) or 2× (1h). The guide's Sample 2 is exactly this.
  • On Opus 4.8 the minimum cacheable prefix is 4096 tokens; below it, the breakpoint silently does nothing. Verify hits with cache_read_input_tokens.
  • The 1M window is a budget, not a target. Retrieve instead of stuffing; prune tool output; compact; isolate corpora in subagents. Count tokens with client.messages.count_tokens(), never tiktoken.
  • Reuse composes: modular prompts keep a cacheable prefix, Skills progressively disclose. The one rule that ties it together: anything that varies goes last.