Prompting
Specification, not incantation.
A prompt is the text you send a model to tell it what you want: the task, examples of a good answer, the format to reply in, and what to do when it is unsure. It is closer to writing a brief for a capable new colleague than to typing a search query. The model will do what you asked, including the parts you never thought to specify.
Most complaints that a model cannot do something turn out to be tasks nobody described precisely. Vague instructions also produce answers that change shape between calls, which means the code reading them breaks on inputs that looked fine yesterday.
It sits directly on the messages API, because the prompt is what fills the request. It is the cheap lever you try before the expensive ones: structured output turns a request for a format into a guarantee, retrieval supplies facts a prompt cannot contain, and fine-tuning is for when prompting has genuinely run out. Evaluation is what tells you a change helped rather than just felt better.
A prompt is a specification, and an underspecified task is the most common cause of a disappointing answer.
In more detailThe durable techniques are unglamorous: state the task precisely, give examples that cover the edge cases, specify the output format, and tell the model what to do when it is unsure. Chain-of-thought prompting mattered more on older models; current reasoning models often do worse when you micromanage their thinking.
Learn first
A prompt is a specification
By the end: You can write a prompt that gets the right answer reliably, instead of one that works sometimes.
- Being specific about the task
- Saying what the output should look like
- Telling the model what to do when it does not know
- Giving it a role, and when that helps
This is where you stop typing requests and start writing briefs. The section covers being precise about the task, pinning down what the reply should look like, and saying explicitly what to do when the answer is not available, which is the instruction people most often leave out. It is unglamorous work and it recovers more bad answers than moving to a larger model does.
Most failures are underspecified tasks#
When a model gets something wrong, the usual cause is that the request allowed the wrong answer. Write the prompt as though briefing a capable new colleague who cannot ask you follow-up questions.
Weak: "Summarise this."
Better: "Summarise this support ticket in two sentences for an
engineer who will pick it up. Say what broke and what the
customer has already tried. Do not include pleasantries."Four things make the difference: what the task is, who the output is for, how long it should be, and what to leave out.
Say what the output should look like#
If something downstream will read the output, describe its shape exactly. Left to itself the model will vary the format between calls, and that variation is what breaks your parser at three in the morning.
Return one line per finding, in this format:
SEVERITY | FILE:LINE | one sentence
Use only HIGH, MEDIUM or LOW for severity.
Return the single word NONE if there are no findings.That last line matters more than it looks. Without it, a model with nothing to report will write a paragraph explaining that it found nothing, and your parser will choke on prose it never expected.
Give it an escape route#
A model asked for a field that is not in the source will usually invent something plausible rather than refuse. It is not lying; it is doing what you asked, which was to produce that field. Give it a permitted way out and it will take it.
If a value is not present in the text, return null for it.
Do not infer, estimate or guess. An absent field is an
acceptable answer; an invented one is not.This is the single highest-value line in most prompts
Hallucinated fields in extraction tasks drop sharply when the model is explicitly allowed to say it does not know. Most prompts never grant that permission.
Roles#
Opening the system prompt with a role, such as telling the model it is a senior accountant, helps when it genuinely narrows vocabulary and conventions. It does not make the model more capable, and stacking superlatives adds nothing.
Useful: "You are reviewing German invoices for VAT compliance."
Useless: "You are a world-class, elite, 10x expert assistant."Where people go wrong
Iterating on a prompt by eye. You will convince yourself it improved because you remember the bad outputs and not the good ones. Fix a set of test inputs and compare against them.
Build this: Take a weak prompt, write down ten inputs with expected outputs, then improve the prompt and measure how many pass before and after. Notice how often your first instinct made it worse.
Open this section on its own pageExamples, and when they earn their cost
By the end: You can decide whether to include examples and choose good ones.
- Zero-shot and few-shot
- Choosing examples that carry information
- Where examples go in the message list
- What examples cost
Few-shot prompting means showing the model a handful of worked cases rather than only describing what you want. It is the oldest trick here and it is not free, because those cases are re-sent with every request you ever make. This section is about deciding whether they earn that space, and about choosing ones that carry information instead of three variations on the same easy case.
Zero-shot first#
Zero-shot means describing the task with no examples. Few-shot means showing a handful of input and output pairs. Start with zero-shot: modern models are good at following a clear description, and examples cost tokens on every single call.
When examples help#
Add examples when the task is easier to show than to describe. An unusual output format, a subtle judgement call, a tone you cannot quite specify. If you find your instructions getting longer and vaguer, that is the signal to switch.
Input: "the button is broken"
Output: BUG | HIGH | UI
Input: "could you add dark mode"
Output: FEATURE | LOW | UI
Input: "charged twice this month"
Output: BILLING | HIGH | ACCOUNTChoose examples that carry information#
Three well-chosen examples beat ten similar ones. Each should teach something the others do not: a boundary case, an ambiguous input, the case where the answer is nothing.
- Include the empty or unknown case, or the model will never produce it.
- Include at least one genuinely ambiguous input, so the model learns which way to lean.
- Keep the format identical across every example. Any inconsistency will be copied.
- Order matters a little. Models weight the last example slightly more, so do not put your weirdest case last.
Your examples become the specification
The model will imitate whatever your examples actually do, including mistakes. A typo in an example becomes a typo in the output. Proofread them as carefully as the instructions.
Where they go#
Either inside the system prompt, or as alternating user and assistant messages that look like a real exchange. The second form usually works better, because it matches the shape of a conversation the model is trained on, and it keeps the system prompt stable for caching.
messages = [
{"role": "system", "content": TASK_DESCRIPTION},
{"role": "user", "content": "the button is broken"},
{"role": "assistant", "content": "BUG | HIGH | UI"},
{"role": "user", "content": "could you add dark mode"},
{"role": "assistant", "content": "FEATURE | LOW | UI"},
{"role": "user", "content": actual_input},
]Examples are input tokens on every request. Five long examples on a high-volume endpoint is a real line on the bill, so measure whether they are earning it.
Where people go wrong
Examples that are all the easy case. The model learns the easy case and has no idea what to do with the hard one, which is exactly the input you needed help with.
Build this: Build a classifier prompt with zero examples, measure it on twenty inputs, then add three examples chosen to cover the boundary cases and measure again. Report both the accuracy change and the token cost change.
Open this section on its own pageReasoning, and improving a prompt honestly
By the end: You can improve a prompt with evidence instead of impressions.
- Chain of thought, and how it has changed
- Splitting a task instead of overloading one prompt
- Building a test set before you iterate
- Techniques that have aged badly
Two things here, and they belong together. One is the family of techniques for making a model work a problem through in stages instead of answering at once, several of which have aged badly and now make newer models worse. The other is how you would ever know: a small fixed set of cases, so that editing a prompt produces a number rather than a feeling. Without that second half, prompt work is fiddling until the one example you keep re-reading looks right.
What actually moves the number#
Prompt advice is mostly a list of tricks with no ordering, which is useless when you have an afternoon and a prompt that fails one time in five. This is the order worth working in, roughly by how much it changes the result per hour spent.
change why it works
say exactly what the output must be removes the largest source of failure,
which is a good answer you cannot parse
define the boundary between labels most errors sit on one or two boundaries,
not spread evenly over the task
give the model an escape hatch without "unknown" it must guess, and it will
put the long document in delimiters so instructions cannot be read as content
show two or three examples for anything easier to demonstrate than describe,
including the hard case and the empty one
split a prompt doing four jobs each job gets easier, and failures become locatable
ask for reasoning before the answer on older or smaller models, on multi-step tasks
only; measure it, it can now hurt
add flattery, urgency, a tip nothing measurableOutput format is the cheapest win, and you can measure it#
Almost every prompt in production is read by code rather than by a person, so an answer that is right and unparseable is a failure. This one you can quantify without calling a model at all: collect the replies you already have and run your parser over them. The twelve below are the shapes a model actually returns when asked for JSON.
import json, re
FENCE = chr(96) * 3 # the code fence models like to wrap JSON in
replies = [
'{"label": "refund", "confidence": 0.9}',
'Sure! Here is the JSON:\n{"label": "refund", "confidence": 0.9}',
FENCE + 'json\n{"label": "refund", "confidence": 0.9}\n' + FENCE,
FENCE + '\n{"label": "refund", "confidence": 0.9}\n' + FENCE,
'{"label": "refund", "confidence": 0.9,}',
"{'label': 'refund', 'confidence': 0.9}",
'{"label": "refund", "confidence": None}',
'{"label": "refund", "confidence": 0.9}\nLet me know if you need anything else.',
' {"label": "refund", "confidence": 0.9} ',
'{\n "label": "refund",\n "confidence": 0.9\n}',
'The label is refund.',
'{"label": "refund"}',
]
def strict(text):
return json.loads(text)
def tolerant(text):
text = re.sub(r"^[^{]*", "", text, count=1) # drop any preamble
text = re.sub(r"[^}]*$", "", text, count=1) # drop any trailing chat
text = text.replace("'", '"').replace("None", "null")
text = re.sub(r",\s*}", "}", text) # trailing comma
return json.loads(text)
def validated(text):
obj = tolerant(text)
if obj.get("label") not in {"refund", "billing", "other"}:
raise ValueError("label not in the allowed set")
if not isinstance(obj.get("confidence"), float):
raise ValueError("confidence missing or not a number")
return obj
for name, parse in (("json.loads", strict), ("tolerant", tolerant),
("validated", validated)):
ok = 0
for r in replies:
try:
parse(r)
ok += 1
except Exception:
pass
print("%-10s parsed %2d of %d" % (name, ok, len(replies)))
# json.loads parsed 4 of 12
# tolerant parsed 11 of 12
# validated parsed 9 of 12Read those three numbers as three different afternoons. If your pipeline uses the first parser, a third of your failures are formatting rather than judgement, and no amount of rewording the task will fix them. Tolerant parsing is worth having whatever else you do, because the model will wrap JSON in a fence eventually. But notice what tolerance costs: it accepted a reply with a null confidence and one with the field missing, either of which becomes a confident wrong decision downstream unless something checks the shape.
The prompt-side fixes are the same three lines every time. State the exact keys and their types, give the allowed values as a closed list, and say what to emit when the answer is none of them. Where the provider supports a structured output or tool schema, use it and delete the tolerant parser, since a schema the server enforces beats a regular expression that guesses.
Ordering, and where the instruction goes#
Position is not neutral. The reliable rules are the ones that follow from how the input is read rather than from folklore: put long documents in explicit delimiters so nothing inside them can be mistaken for an instruction, and put the actual question where it cannot be buried. With a long document, that means stating the task before it and restating it after, which costs a dozen tokens and removes a whole class of failure where the model summarises a document you wanted classified.
Two orderings genuinely matter and both are cheap to check. In few-shot prompts the order of examples can shift the output distribution, particularly if all the easy cases come first or the labels arrive in a block. And in a long prompt, whatever sits immediately before the model starts writing has more influence than anything else, so the output contract belongs at the end rather than the top. Neither is a law. Both are two runs of your eval away from being settled for your model and your task.
Chain of thought#
Asking a model to work through its reasoning before answering used to produce a large improvement on multi-step problems. It still helps on older and smaller models, and on genuinely multi-step tasks.
It has become much less necessary. Newer reasoning models already do this internally, and instructing them to think step by step can make results worse by interfering with a process that was working. Test it rather than adding it by reflex.
When you do want it, ask for the reasoning in a place you can throw away, such as a separate field or a tagged block before the answer, rather than mixed into the output your code parses. That keeps the format contract intact, and it lets you look at the reasoning when a case fails, which is worth more than the accuracy change on most tasks.
Prompting advice ages faster than almost anything
Techniques that were essential two model generations ago are now neutral or harmful. Treat any prompting tip, including these, as something to verify on your model and your task rather than a rule.
Split the task#
When a prompt is trying to do four things at once, quality on all four drops. Splitting into separate calls usually beats a longer prompt, and it has a large practical advantage: you can tell which step failed.
One prompt: read the document, extract the fields, validate
them, and write a summary
Three calls: extract -> validate -> summarise
The second version costs more per document and is far cheaper
to debug, because a failure points at one step.The other gain is that each step becomes separately measurable. A single prompt gives you one number that mixes four skills, so a change that improves extraction and damages summarising looks like no change at all. Three prompts give you three numbers, and the one that moved tells you what you did.
Iterate against a test set, not against memory#
This is the part people skip, and it is the part that matters. Changing a prompt and eyeballing the next output tells you almost nothing, because you remember the failures you were looking for and not the ones you were not.
- 1.Collect twenty real inputs, including the ones that currently fail.
- 2.Write the expected outcome for each, before you touch the prompt.
- 3.Score the current prompt. That number is your baseline.
- 4.Change one thing. Score again.
- 5.Keep the change only if the number moved.
Twenty cases and a script is enough to start. This is the beginning of evals, and doing it at prompt level makes the full discipline feel obvious later. It is also where the next two experiments come from, because the first question anyone should ask about a number is how much it wobbles on its own.
Twenty cases cannot see what you think they can#
Sixteen out of twenty becoming seventeen out of twenty feels like progress. It is worth knowing exactly how often that happens when nothing has changed at all, and the arithmetic is a few lines of standard library.
from math import comb
def at_least(k, n, p):
"""Probability of scoring k or more out of n when the true rate is p."""
return sum(comb(n, i) * p**i * (1 - p)**(n - i) for i in range(k, n + 1))
print("prompt A scores 16/20. Prompt B scores 17/20. If B is no better:")
print(" P(B scores 17 or more | true rate 0.80) = %.3f" % at_least(17, 20, 0.80))
print(" P(B scores 18 or more | true rate 0.80) = %.3f" % at_least(18, 20, 0.80))
print()
print("How big must the set be before a real 80 -> 90 percent jump is visible?")
for n in (20, 50, 100, 200, 400):
# the score you would need to be 95% sure the old prompt did not produce it
need = next(k for k in range(n + 1) if at_least(k, n, 0.80) < 0.05)
print(" n=%3d: need %3d/%3d (%.0f%%) to clear noise; a true 90%% model "
"delivers that %.0f%% of the time" % (n, need, n, 100 * need / n,
100 * at_least(need, n, 0.90)))
# prompt A scores 16/20. Prompt B scores 17/20. If B is no better:
# P(B scores 17 or more | true rate 0.80) = 0.411
# P(B scores 18 or more | true rate 0.80) = 0.206
#
# How big must the set be before a real 80 -> 90 percent jump is visible?
# n= 20: need 20/ 20 (100%) to clear noise; a true 90% model delivers that 12% of the time
# n= 50: need 45/ 50 (90%) to clear noise; a true 90% model delivers that 62% of the time
# n=100: need 87/100 (87%) to clear noise; a true 90% model delivers that 88% of the time
# n=200: need 170/200 (85%) to clear noise; a true 90% model delivers that 99% of the time
# n=400: need 334/400 (84%) to clear noise; a true 90% model delivers that 100% of the timeThe second table is the one to keep. At a hundred cases you catch a ten-point improvement most of the time, and that is the number to aim at before claiming anything. Below fifty, you are not measuring the prompt, you are measuring which twenty inputs you happened to collect. This does not mean twenty cases are worthless: they are excellent for catching a prompt that is broken, a format that will not parse, or a change that made things obviously worse. They are simply not an instrument for small improvements.
One cheap trick makes a small set go further. Compare the two prompts only on the cases where they disagree, and count how many each got right. If they differ on three cases and the new prompt wins two of them, nothing has been shown, and you have learned that in the time it takes to read three rows.
Why you have to hold cases back#
The trap at the end of this section is usually stated as advice. It is better stated as a measurement. Below, several prompt variants are scored on the same twenty cases and the best one is kept. Every variant is secretly identical, with a true pass rate of 0.75, so nothing in this experiment can possibly be an improvement.
import numpy as np
rng = np.random.default_rng(0)
trials, n_cases, true_rate = 4000, 20, 0.75
print("variants tried best dev score same prompt, fresh cases")
for n_variants in (1, 3, 10, 30):
dev = rng.random((trials, n_variants, n_cases)) < true_rate
winner = dev.mean(axis=2).max(axis=1)
fresh = (rng.random((trials, n_cases)) < true_rate).mean(axis=1)
print("%14d %14.3f %23.3f" % (n_variants, winner.mean(), fresh.mean()))
# variants tried best dev score same prompt, fresh cases
# 1 0.751 0.752
# 3 0.828 0.749
# 10 0.889 0.750
# 30 0.927 0.749That is fourteen points of pure illusion from an afternoon of honest work, and it is why the number you report must come from cases you did not select on. Split the set before you start: two thirds to iterate against, one third you look at once, at the end, and never again. If the held-out number is far below the one you were watching, you have been tuning to noise, and the experiment above says how much of it to expect from how many variants you tried.
A prompt, before and after#
Everything above, applied to one real prompt. The task is routing a support message to a queue.
BEFORE
You are an expert customer support agent with 20 years of
experience. Please carefully analyse the following customer
message and determine which category it belongs to. This is
very important, so think step by step. Categories: refund,
billing, technical, other.
Message: {message}
AFTER
Route a support message to one queue.
<message>
{message}
</message>
Queues, in priority order. Use the first one that applies:
refund the customer is asking for money back
billing a charge, invoice or subscription question,
where no refund is requested
technical the product is not working as documented
other none of the above, or the message is unclear
If two apply, choose the earlier one. If you cannot tell from
the message alone, use "other" rather than guessing.
Reply with one JSON object and nothing else:
{"queue": "refund|billing|technical|other", "confident": true|false}what changed why
dropped "expert with 20 years" no measurable effect, costs tokens
dropped "this is very important" same
dropped "think step by step" untested reflex; measure before adding it
task stated in one line, first the model knows what it is doing by line one
message wrapped in tags a message containing "ignore the above"
is now data, not an instruction
labels given definitions "billing, where no refund is requested"
is the boundary the errors were sitting on
tie-break rule made explicit removes the largest source of disagreement
between two runs of the same prompt
"other" defined as the escape without it the model guesses on unclear input
output contract at the end exact keys, closed value list, nothing else
confident flag added gives you a cheap route to a humanChange one of those at a time and score after each, or you will finish the afternoon knowing the prompt got better and not knowing which line did it. The tie-break rule and the boundary definition usually account for most of the gain on a task like this, because they are the two places where a human labeller would also have hesitated.
Things that have aged badly#
- Offering the model a tip or threatening it. Measured effects were always marginal and are now noise.
- Long preambles about how important the task is. Tokens spent on nothing.
- Stacking superlatives about expertise. Specificity helps; flattery does not.
- Elaborate jailbreak-style framings to get ordinary work done. Usually a sign the task was underspecified.
- Asking for a confidence score and treating it as a probability. It is a token the model produced, not a calibrated estimate, and it is useful only as a rough flag for review.
- Telling the model not to hallucinate. Naming the failure does not prevent it; giving the model a way to say it does not know sometimes does.
Where people go wrong
Tuning a prompt until it passes the cases you have been staring at. That is overfitting. Hold back a few inputs you never look at while iterating, and check against them at the end.
Build this: Take a prompt that fails sometimes. Build a twenty-case test set, score it, then make three separate improvements and score after each. Write down which ones helped and which made it worse.
Open this section on its own pageWhere you use this
A topic is easier to care about when you can see what it is for. These projects lean on it at a named stage.
- Stage 4, Answer, with citations that resolve: Writing the instruction as a rule the output can be checked against, not a request.
- Stage 3, A second tool, and the model choosing wrong: The tool description is a specification, and rewriting it is the main lever on tool choice.
- Stage 4, A judge for what is left: Writing the failure rule precisely enough that you could apply it yourself.
- Stage 2, Two baselines, before you train anything: Writing the strong prompt properly, because a weak baseline is how a pointless fine-tune gets declared a success.
Now actually do it
Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.
1Build a test set before you tune
3hTake a prompt you use. Collect 20 real inputs with expected outcomes written before running anything. Score it. That number is your baseline.
Done when: You have a baseline number and at least one case you expected to pass that did not.
2Improve it one change at a time
4hMake five separate improvements, scoring after each, keeping only what moved the number. Record which ones made it worse.
Done when: At least one change you were confident about made it worse, and you reverted it.
3Find the overfitting
3hHold back five cases you never look at during tuning. After your improvements, score on them. Compare the gap against your tuning set.
Done when: You know how much of your improvement was real and how much was fitting to the cases you stared at.
10 hours of practice
Words used here
3 terms this topic teaches, each with a one-line definition if one of them stopped you.
- System prompt
- Instructions supplied ahead of the conversation that set role, constraints and format. Models are trained to weight it more heavily than user turns, though not absolutely.
- Few-shot prompting
- Putting a handful of worked examples in the prompt so the model infers the pattern from them. It pins down format and edge-case handling far more reliably than describing them in prose.
- Chain of thought
- Having a model write out intermediate steps before its answer, which gives it more computation and a place to keep partial results. It helps most on arithmetic, multi-step logic and anything with a chain of constraints.
Interview questions
9 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.
A prompt is a specification
A prompt works most of the time but occasionally returns the wrong format. What do you do?
First check whether the task and output format are fully specified, including what to do in the empty or unknown case. Then lower the temperature, since format drift is sampling variation. Then use schema-constrained output, which makes the shape a guarantee rather than a request.
Then they push further
You added a schema and it still fails on 1% of requests. Now what?
Look at the failing inputs. Check for truncation from max_tokens, for unusual inputs such as very long or empty ones, and for inputs that contain their own formatting. Then decide between fixing the prompt, validating and retrying once, or failing cleanly.
Is retrying on failure a good idea?
Once, with the validation error passed back, is usually cheap and fine. Unlimited retries hide a real problem and multiply cost. Count retries so you notice when they climb.
What gets marked up. Marked up for reaching for structured output and validation rather than more wording, and for reading the failing inputs before changing anything.
How do you stop a model inventing data during extraction?
Explicitly permit null for missing fields and state that guessing is not acceptable. Models fill gaps because the prompt demanded a value; giving them a legitimate alternative removes the pressure.
Does telling a model it is an expert improve results?
Only when the role genuinely narrows the domain, vocabulary or conventions. Piling on superlatives does nothing measurable. The gain comes from specificity, not flattery.
Examples, and when they earn their cost
When would you use few-shot rather than zero-shot?
When the task is easier to demonstrate than describe: an unusual format, a subtle judgement, a tone. Start zero-shot, because examples cost tokens on every call, and add them when you can measure that they help.
How many examples should you use?
Usually three to five, chosen so each teaches something different. More similar examples add cost without adding information. Always include the empty or unknown case, or the model will never produce it.
What is a risk of few-shot prompting?
The model imitates the examples closely, including any inconsistency or mistake in them, and it can over-fit to their surface pattern. It can also bias the output distribution if all your examples share a property you did not intend to teach.
Reasoning, and improving a prompt honestly
Does chain-of-thought prompting still help?
It depends on the model. On older and smaller models it gives a real improvement on multi-step tasks. Newer reasoning models do it internally, and explicit instructions can interfere. The answer is to measure it rather than assume.
How do you know a prompt change actually improved things?
Score it against a fixed set of inputs with known expected outcomes, before and after. Without a baseline you are comparing against your memory of previous outputs, which is unreliable in a predictable direction.
Then they push further
Your eval went from 80% to 82%. Is that an improvement?
Not necessarily. On a small set that is within noise. Rerun the baseline to see how much it varies on its own, and compare the two versions only on the cases where they disagree.
What gets marked up. Strong answers mention an eval run before and after, a held-out split, and awareness of noise. Weak answers describe trying a few examples by hand.
When would you split one prompt into several calls?
When it is doing several distinct jobs. Quality on each drops when they compete, and a single failure gives you no information about which step went wrong. Separate calls cost more and are far easier to debug and evaluate.
Then they push further
What does splitting cost you?
More latency, more calls to pay for, and more places for errors to creep in between steps. It is worth it when each step becomes easier to get right and to test on its own.
What gets marked up. Marked up for weighing testability and reliability against latency and cost, rather than treating chains as always better.
Resources
Free to watch and read, picked for this topic.
Videos
For a prompt is a specification
For examples, and when they earn their cost
For reasoning, and improving a prompt honestly
Full courses: the whole topic, start to finish
Read and explore
Stop 5 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.