Advanced Prompting Techniques
Chapter 2 taught you clear instructions and structured output. This chapter is the next layer: the reasoning-shaping patterns — few-shot, chain-of-thought, self-consistency, decomposition, ReAct — that turn a capable model into a reliable one on hard, multi-step problems.
Before you fine-tune or self-host, you can get dramatically better results just by how you prompt and what context you provide. This section goes past 'write a good prompt' into systematic techniques: structured prompting, context engineering, and automatic prompt optimization.
The words you'll hear (in plain terms):
| Term | What it actually means |
|---|---|
| prompt engineering | deliberately designing the instructions you give a model. |
| context engineering | choosing what information to put in the model's window, and how. |
| few-shot | including examples in the prompt so the model imitates the pattern. |
| system prompt | the standing instructions that shape all the model's replies. |
| prompt optimization | systematically improving prompts (even automatically, e.g. DSPy). |
What you need before starting:
- Any experience prompting an LLM; the Ch 2 prompting basics first.
- Python basics for the optimization labs (
pip install dspy). - This section is mostly runnable with just an API key.
New to the topic? Read this box, then take the chapters in order — each section is tagged essential → expert so you always know the depth you're at.
Learning objectives
- Pick the right technique for a task instead of reaching for one habitually.
- Write few-shot prompts whose examples actually teach the pattern.
- Use chain-of-thought and self-consistency where reasoning matters — and know their cost.
- Decompose a hard task into a prompt chain you can debug.
- Understand ReAct as the bridge from prompting to agents (Ch 4).
claude-opus-4-8) but the patterns transfer.A map of the techniques essential
These are not competing options — they're tools for different failure modes. Match the technique to what's actually going wrong.
| Technique | Use when… | Main cost |
|---|---|---|
| Zero-shot + clear instructions | The task is common and well-specified (Ch 2) | None — always try this first |
| Few-shot (in-context examples) | You need a specific format, style, or edge-case behavior | Tokens; examples can bias output |
| Chain-of-thought (CoT) | Multi-step reasoning, math, logic | Latency + output tokens |
| Self-consistency | CoT answers vary; you need a robust answer | N× the calls |
| Decomposition / prompt chaining | One prompt is doing too much; you can't debug it | Orchestration complexity |
| ReAct (reason + act) | The model needs tools/live data to answer | You're now building an agent (Ch 4) |
Few-shot: teach by example essential
A few-shot prompt includes worked examples before the real input. The model infers the pattern from them — this is in-context learning (a direct consequence of attention; see K5). It's the highest-leverage technique because it shows rather than tells.
System: You classify support tickets. Respond with only the label.
Example 1
Ticket: "I was charged twice this month."
Label: BILLING
Example 2
Ticket: "The app crashes when I tap export."
Label: BUG
Example 3
Ticket: "Can you add a dark mode?"
Label: FEATURE_REQUEST
Now classify:
Ticket: "{user_ticket}"
Label:
This is a few-shot prompt: instead of describing the format in words, you show the model a few solved examples and then hand it a fresh input to finish. The whole thing is one block of text you send as the prompt — read it top to bottom the way the model does.
- The first line is the instruction ("You classify support tickets. Respond with only the label."). It sets the job and the exact output shape you want.
- Example 1–3 are the shots: each shows a
Ticket:line and the correctLabel:for it (BILLING,BUG,FEATURE_REQUEST). The model learns the pattern from these — this is called in-context learning. - The final block (
Now classify:with a real ticket and a danglingLabel:) leaves the answer blank on purpose. The model continues the pattern and fills in the label. {user_ticket}is a placeholder your code replaces with the real ticket text before sending — the curly braces are not sent to the model.
What the output means: The model replies with just one label (e.g. BILLING) because every example trained it to answer with a single word and nothing else.
Try this: Reorder the three examples so the trickiest one is last. Models weight recent examples more, so the closing example nudges the pattern hardest — that's why the lesson says "put the important pattern last".
| Do | Don't |
|---|---|
| Cover the edge cases you care about (the tricky label, the ambiguous input) | Give three near-identical easy examples |
| Keep format identical across examples and the real query | Vary the format between examples and the query |
| Order matters — models weight recent examples; put the important pattern last | Assume order is irrelevant |
| Use 2–5 examples; measure whether more helps | Stuff 30 examples "to be safe" (cost + dilution) |
Chain-of-thought & self-consistency essential
Chain-of-thought asks the model to reason step by step before answering. It measurably improves accuracy on math, logic, and multi-constraint problems — because generating intermediate steps lets each step condition on the last (again, next-token prediction, K4).
This diagram contrasts two ways a model can answer a hard question: jump straight to the answer, or reason step by step first. It's split into a red (left/top) path and a green (right/bottom) path.
- The red path (top, "Direct") goes
question→answerin one hop. On hard tasks that single leap is where the model often goes wrong. - The green path ("CoT") goes
step1→step2→answer. The one big leap is broken into several small, checkable moves. - The caption line — each step conditions on the previous — is the key: the model reads its own step 1 before writing step 2, so later steps build on earlier ones instead of guessing all at once.
- Colour is the shorthand: red = risky one-shot, green = safer stepwise. Same question, fewer errors on the right.
In short: Chain-of-thought = let the model show its working. On modern models you often just add "Think step by step, then give the final answer" and get the green path automatically.
claude-opus-4-8 you don't hand-roll CoT — enable adaptive thinking (thinking: {type: "adaptive"}) and the model reasons before answering, returning the answer cleanly. Reach for prompt-level CoT mainly with models that lack a thinking mode, or when you want the reasoning visible in the output for auditing. (See C2.)Self-consistency runs CoT several times (with sampling) and takes the majority answer. It trades cost for robustness on problems where a single reasoning path is unreliable.
Setup to run this snippet
class _Any:
'''stands in for any undefined demo value; supports call/attr/index/
iteration and basic arithmetic (as 0.7) so demo snippets run.'''
def __call__(self, *a, **k): return _Any()
def __getattr__(self, k): return _Any()
def __getitem__(self, k): return _Any()
def __iter__(self): return iter([])
def __len__(self): return 0
def __contains__(self, o): return True
def __enter__(self, *a): return _Any()
def __exit__(self, *a): return False
def __float__(self): return 0.7
def __int__(self): return 1
def __lt__(self, o): return True
def __gt__(self, o): return False
def __le__(self, o): return True
def __ge__(self, o): return False
def __add__(self, o): return o
def __radd__(self, o): return o
def __bool__(self): return True
def __repr__(self): return 'demo'
def __str__(self): return 'demo'
def extract_final(*a, **k): # demo stub
return _Any()
def majority_vote(*a, **k): # demo stub
return _Any()
def model(*a, **k): # demo stub
return _Any()
prompt = _Any()answers = []
for i in range(5):
r = model(prompt, temperature=0.7) # sample diverse paths
answers.append(extract_final(r))
final = majority_vote(answers) # robust to one bad path
This is self-consistency: instead of trusting one answer, you ask the model the same question several times, let it reason differently each time, then keep the answer that comes up most often. It's a vote. (The setup box above just fakes the model, extract_final and majority_vote helpers so the snippet runs — ignore it; the real idea is these 5 lines.)
answers = []starts an empty list to collect each run's final answer.for i in range(5):repeats the call 5 times.temperature=0.7tells the model to be a bit random, so each run may reason down a different path — that variety is the whole point.extract_final(r)pulls just the final answer out of each (possibly long) reasoning reply, andanswers.append(...)stores it.majority_vote(answers)returns the answer that appeared most often. If four of five runs agree, one bad reasoning path gets outvoted — that's the robustness you paid for.
What the output means: final holds the majority answer across 5 tries — more reliable than any single run, but it cost you 5 API calls instead of 1.
Try this: Because this makes 5× the calls, only reach for it when correctness really matters. Compare it against one call with thinking enabled first — often a single good call wins for far less money.
Lab E1.1 · Decomposition & prompt chaining intermediate
When one prompt tries to do five things, it does all of them mediocrely and you can't tell which part failed. Decompose: split into a chain of focused prompts, each with one job and a checkable output.
This diagram shows decomposition: taking one prompt that tries to do everything and breaking it into a chain of small prompts, each doing one job. Read it left to right.
- On the left, the red box
one mega-promptis labelled "hard to debug" — when it fails, you can't tell which of its five jobs broke. - The arrow (→) means "is rewritten as" the chain on the right.
- The green boxes —
extract→analyze→summarize— are the chain. Each arrow passes one step's clean output into the next step's input. - The caption spells out the payoff: each step is one job · testable output · swappable — you can test each link alone and even use a cheaper model for the easy steps.
In short: A chain beats a monolith whenever a task is non-trivial: you can see exactly which link failed and fix just that one, instead of guessing inside one giant prompt.
- Take a "do everything" prompt you have (e.g. "read this email, extract the request, draft a reply, and flag urgency").
- Split it into three prompts: extract → classify urgency → draft reply from the extracted fields.
- Give each a structured output (Ch 2) so the next step consumes clean data, not prose.
- Note which step was actually failing before — you usually couldn't tell in the monolith.
ReAct: the bridge to agents intermediate
ReAct interleaves reasoning and acting: the model thinks, calls a tool, observes the result, thinks again. It's the pattern under most agent loops — when a prompt technique needs live data or actions, you've crossed into agent territory.
Thought: I need the current error rate to answer.
Action: query_metrics("error_rate", "1h")
Observation: 4.2%
Thought: That's above the 2% threshold. I should check recent deploys.
Action: list_deploys("1h")
Observation: deploy #481 at 14:02
Thought: The spike aligns with deploy #481.
Answer: Error rate is 4.2%, likely caused by deploy #481.
This is a ReAct trace (Reason + Act). It shows the model alternating between thinking and using a tool until it has enough facts to answer. It isn't Python you run — it's the transcript of one agent loop, and it's exactly the pattern Chapter 4 builds for real.
- Thought: the model reasons out loud about what it still needs — here, "I need the current error rate". No made-up numbers yet.
- Action: the model calls a tool to get real data, e.g.
query_metrics("error_rate", "1h"). This is the model reaching outside itself for a fact it can't know. - Observation: the tool's result comes back (
4.2%). The model now has a real fact to reason about, not a guess. - The loop repeats — think, act, observe — checking recent deploys, until the model has enough to give the final Answer: tying the error spike to deploy #481.
What the output means: A grounded answer built from live data: "Error rate is 4.2%, likely caused by deploy #481." Every number came from a tool call, not the model's memory.
Try this: Notice each Action is followed by an Observation the next Thought reacts to. When your prompt keeps needing facts it doesn't have, that's the signal to give it a tool — you've crossed from prompting into building an agent (Ch 4).
Common pitfalls advanced
| Pitfall | Fix |
|---|---|
| Reaching for CoT/self-consistency on easy tasks | Start zero-shot; climb the ladder only when evals demand it |
| Hand-writing CoT on a model with native thinking | Use adaptive thinking (C2) and keep the prompt clean |
| Few-shot examples that don't cover edge cases | Curate examples for the hard cases, not the easy ones |
| One mega-prompt you can't debug | Decompose into a chain with structured hand-offs |
| Judging a technique by one anecdote | Measure on a held-out set (Ch 5, E3) |
Exercises advanced
Exercise E1.1 — Technique selection
Context: The escalation ladder is only useful if you can place a real task on the lowest rung that fits — over-climbing wastes tokens, latency, and calls.
Your task: For each task, name the lowest ladder rung that fits and justify it: (a) extract an invoice total, (b) solve a multi-step word problem, (c) answer "what's our current uptime?", (d) rewrite text in a specific house style.
Requirements:
- Give a rung and a one-line justification for each of the four tasks
- Recognise a well-specified extraction needs only zero-shot
- Recognise multi-step reasoning needs CoT / native thinking
- Recognise a live-data question needs tools/ReAct, and style is best shown by example
💡 Hint: Ask what each task is missing at zero-shot — reasoning depth, live data, or a style the model can't infer — and pick the cheapest rung that supplies it.
Show reasoning
(a) zero-shot — well-specified extraction; (b) CoT / native thinking — multi-step reasoning; (c) ReAct/tools — needs live data, no prompt can know it; (d) few-shot — style is best shown by example.
Exercise E1.2 — Break the monolith
Context: Adding a length and a tone constraint to a style-rewrite task is where a single prompt starts to strain — a short chain with structured hand-off keeps each concern testable.
Your task: Take the house-style rewrite from E1.1(d), add a length constraint and a tone constraint, and design a two-step chain — deciding where structured output goes.
Requirements:
- Split the task into two focused steps
- Show the structured output passed between the steps
- Account for both the length and the tone constraint
- Explain why the split makes each step independently checkable
💡 Hint: Structured output belongs at the seam between the steps — that is what lets you verify the first step before the second one runs.
Exercise E1.3 — When is self-consistency worth it?
Context: Self-consistency's 2-point accuracy gain costs 5× the calls, so shipping it is a judgement call about stakes and volume, not a reflex.
Your task: Given a classifier at 91% with one call and 93% with 5-call self-consistency, state two questions you would ask before shipping the 5× cost.
Requirements:
- Ask at least two concrete decision questions
- Weigh the value of the 2-point accuracy gain against the 5× cost
- Consider the stakes/consequences of an error and the request volume
- Reach a defensible ship / don't-ship stance
💡 Hint: Two points can be decisive or negligible depending on what an error costs and how many requests you run — anchor your questions there.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Few-shot prompting teaches by example: a handful of labelled cases before the real input. In-context learning weights those recent examples heavily, so a clean, balanced set is the whole lever.
Your task: Build a ticket-classifier prompt — a system line, three labelled examples (BILLING, BUG, FEATURE_REQUEST), then the slot to classify — and explain why the examples are ordered as they are.
Requirements:
- Include a system line stating the task and the output format
- Provide exactly one example per label so no class is over-represented
- Use an identical format across every example so the model copies the pattern
- End with the new ticket and an empty label slot
- Explain the balance/format reasoning in a sentence
💡 Hint: This is a string builder — no model call needed; the teaching point is that balance and consistent formatting drive the classification, not clever wording.
Show solution
The few-shot template (a string builder — runnable, no model needed):
EXAMPLES = [
("I was charged twice this month.", "BILLING"),
("The app crashes when I tap export.", "BUG"),
("Can you add a dark mode?", "FEATURE_REQUEST"),
]
def build_prompt(ticket):
lines = ["System: You classify support tickets. Respond with only the label.", ""]
for i, (tk, label) in enumerate(EXAMPLES, 1):
lines += [f"Example {i}", f'Ticket: "{tk}"', f"Label: {label}", ""]
lines += ["Now classify:", f'Ticket: "{ticket}"', "Label:"]
return "\n".join(lines)
print(build_prompt("My invoice looks wrong."))
Each label appears exactly once so no class is over-represented (which would bias the output), and the format is identical across examples so the model copies the pattern, not the wording. In-context learning weights these recent examples heavily — a clean, balanced set is the whole lever.
Context: Chain-of-thought makes the model reason step by step before answering, trading latency and output tokens for a visible, auditable reasoning path. The Observations are the evidence; the Answer is only as good as that chain.
Your task: Given a Thought/Action/Observation trace for an incident, programmatically extract the final Answer and the evidence chain that led to it.
Requirements:
- Parse the trace line by line
- Extract the single final Answer
- Extract every Observation as the evidence chain, in order
- Keep it pure-stdlib string handling
- Print the answer alongside its supporting evidence
💡 Hint: The line prefixes (Thought/Action/Observation/Answer) are the whole grammar — split on them; the Observations are what make the conclusion auditable.
Show solution
Parse a CoT trace to separate reasoning from the answer (pure stdlib):
trace = '''Thought: I need the current error rate to answer.
Action: query_metrics("error_rate", "1h")
Observation: 4.2%
Thought: That's above the 2% threshold. I should check recent deploys.
Action: list_deploys("1h")
Observation: deploy #481 at 14:02
Thought: The spike aligns with deploy #481.
Answer: Error rate is 4.2%, likely caused by deploy #481.'''
lines = trace.splitlines()
answer = next(l.split("Answer:",1)[1].strip() for l in lines if l.startswith("Answer:"))
evidence = [l.split("Observation:",1)[1].strip() for l in lines if l.startswith("Observation:")]
print("answer :", answer)
print("evidence:", evidence) # ['4.2%', 'deploy #481 at 14:02']
CoT trades latency and output tokens for a visible reasoning path: the Observations are the evidence, and the final Answer is only as trustworthy as that chain. Exposing the steps is what lets you audit why the model concluded what it did.
Context: Self-consistency samples several reasoning paths at temperature and takes the majority answer, so a single bad path can't sink the result. It costs N× the API calls, so it is reserved for high-stakes questions.
Your task: Implement the majority-vote aggregator over N sampled answers, with the sampler stubbed so it runs offline.
Requirements:
- Sample N answers from a stubbed sampler that occasionally returns a bad path
- Take the majority answer as the final result
- Report the winning answer, its vote count, and all sampled answers
- Make it reproducible with a fixed seed
- Show the majority surviving a lone bad path
💡 Hint: collections.Counter.most_common(1) gives you the winner and its votes — the vote is what makes the result robust to one hallucinated chain.
Show solution
The self-consistency aggregator — sampling stubbed so it runs offline:
from collections import Counter
import random
def sample_answer(prompt, temperature=0.7):
# STUB for the model call: mostly correct, occasional bad path
return random.choice(["42", "42", "42", "17"])
def self_consistency(prompt, n=5, temperature=0.7):
answers = [sample_answer(prompt, temperature) for _ in range(n)]
winner, votes = Counter(answers).most_common(1)[0]
return winner, votes, answers
random.seed(1)
final, votes, all_ = self_consistency("What is 6 * 7?", n=5)
print("answers:", all_)
print(f"final: {final} ({votes}/5 votes)") # majority survives one bad path
Any single sampled path can go wrong; the majority of N paths usually will not. Self-consistency costs N times the API calls, so reserve it for high-stakes questions where robustness beats latency — the vote is what makes it resilient to a lone hallucinated chain.
Context: One prompt doing extract + classify + draft is an opaque failure when it goes wrong. Decomposing it into focused stages that pass structured output turns one unreadable failure into three testable, swappable stages.
Your task: Decompose a do-everything prompt into a three-stage chain — extract, classify, draft — passing structured output between the stages, and model it offline.
Requirements:
- Make each stage a pure function of the previous stage's output
- Stage 1 extracts structured fields from the ticket
- Stage 2 classifies from those fields
- Stage 3 drafts a reply from the classification
- Compose them into one pipeline and run it on a sample ticket
💡 Hint: Structured output between stages is what makes each independently testable — you can tell whether extraction, classification, or drafting broke.
Show solution
Prompt chaining — each stage is a pure function of the last (offline stubs):
def extract(ticket): # stage 1: pull structured fields
return {"text": ticket, "has_charge": "charged" in ticket.lower()}
def classify(fields): # stage 2: label from fields
fields["label"] = "BILLING" if fields["has_charge"] else "OTHER"
return fields
def draft_reply(fields): # stage 3: write the response
if fields["label"] == "BILLING":
return "We're reviewing the duplicate charge and will refund it."
return "Thanks for reaching out -- a specialist will follow up."
def pipeline(ticket):
return draft_reply(classify(extract(ticket)))
print(pipeline("I was charged twice this month."))
Decomposition turns one opaque failure into three testable stages: if the reply is wrong you can tell whether extraction, classification, or drafting broke. The cost is orchestration complexity, but each step is now debuggable and independently swappable.
Context: The techniques form an escalation ladder — zero-shot → few-shot → CoT → self-consistency → decomposition → ReAct — each costing more (tokens, latency, calls, complexity). You climb only as far as the failure demands.
Your task: Given a failure symptom, recommend the lowest-cost technique that addresses it and name its cost.
Requirements:
- Map format/consistency failures to few-shot
- Map skipped multi-step reasoning to chain-of-thought
- Map flaky hard reasoning to self-consistency
- Map an overloaded prompt to decomposition, and tool needs to ReAct
- State each technique's cost, and default to zero-shot first
💡 Hint: Reaching straight for ReAct when few-shot would fix a formatting issue is over-engineering — the recommender should return the cheapest sufficient rung.
Show solution
The escalation ladder as a recommender (pure stdlib):
LADDER = [
("wrong format / inconsistent labels", "few-shot", "token cost; examples can bias"),
("skips reasoning on multi-step math", "chain-of-thought", "latency + output tokens"),
("flaky on hard reasoning", "self-consistency", "N x the API calls"),
("one prompt does too much", "decomposition", "orchestration complexity"),
("needs to call tools / look things up","ReAct", "you are now building an agent"),
]
def recommend(symptom):
for sig, tech, cost in LADDER:
if any(w in symptom.lower() for w in sig.split(" / ")[0].split()):
return tech, cost
return "zero-shot + clear instructions", "none -- always try first"
print(recommend("the labels are inconsistent"))
print(recommend("it needs to look things up in a system"))
Always try zero-shot with clear instructions first, then climb only as far as the failure demands — each rung costs more (tokens, latency, calls, or complexity). Reaching straight for ReAct when few-shot would fix a formatting issue is over-engineering.
Context: As lead you don't apply one technique everywhere — you segment traffic and spend complexity only where it pays. A triage system has cheap high-volume classification, a few hard diagnoses, and some requests that must query live systems.
Your task: Assign a prompting technique to each request class of a support-triage system and justify the cost.
Requirements:
- Route high-volume classification to cheap few-shot at zero temperature
- Route hard diagnoses to CoT (with self-consistency where stakes are high)
- Route requests needing live data to a ReAct agent with tools
- Justify the cost of each choice
- Return a technique-per-class plan
💡 Hint: The high-volume path stays cheap; only the tool-using class escalates to a full agent — segment first, then spend complexity where it earns its keep.
Show solution
Route each request class to the cheapest sufficient technique (pure logic):
def strategy(request_class):
plan = {
"bulk_classify": ("few-shot, zero temperature",
"cheap + consistent; runs on every ticket"),
"hard_diagnosis": ("chain-of-thought (+ self-consistency if stakes high)",
"reasoning depth where accuracy matters most"),
"needs_live_data":("ReAct with tools",
"must query metrics/deploys -- it becomes an agent"),
}
return plan.get(request_class, ("zero-shot", "start simple"))
for cls in ["bulk_classify", "hard_diagnosis", "needs_live_data"]:
tech, why = strategy(cls)
print(f"{cls:16s}: {tech}\n{'':16s} ({why})")
A lead does not apply one technique everywhere — they segment traffic and spend complexity only where it pays. The high-volume path stays cheap few-shot, the rare hard cases get CoT/self-consistency, and only the tool-using class escalates to a full ReAct agent.
✓ Checkpoint — you can move on when you can…
- Choose a technique by the failure mode, not by habit.
- Write a few-shot prompt that teaches edge cases in a fixed format.
- Explain when CoT/self-consistency earns its cost — and when native thinking replaces it.
- Decompose a mega-prompt into a debuggable chain.
- Recognize when ReAct means you should build an agent.
Knowledge check check yourself
The lesson frames the techniques as an 'escalation ladder'. What is the rule for climbing it, and what over-engineering mistake does it prevent?
Show answer
Self-consistency improves robustness but costs 5× the calls. What does the lesson say to compare it against first on Claude, and why?