Chapter 02 · Section I · 24 min read
Writing prompts that hold up in production
The difference between a chat prompt that impresses a friend and a prompt that runs 10,000 times a day without failing. Five habits, one worked example, and the discipline that turns prompting from art into engineering.
There is a folk category of people online who call themselves “prompt engineers” and share screenshots of clever prompts that produce viral outputs. Almost none of what they write survives contact with production. A real production prompt runs ten thousand times a day, on inputs the author never anticipated, and either does its job silently or breaks something noisy and expensive. The habits that make prompts survive are not clever — they are boring, mechanical, and unfashionable. This section is those habits.
The five habits, up front
Every prompt that ships to production has, roughly:
- A clear role and task — who is the model, what is it doing?
- Explicit constraints — length, format, language, tone.
- Concrete examples — one or two, for anything non-obvious.
- Error handling in the prompt itself — what to do when the input is weird.
- A stable structure — same shape across variations, so downstream code can rely on it.
Every one of these is unglamorous. Every one of them saves your bacon at 2am when a real user does something the model has never seen.
Habit 1 — role and task
Every serious prompt starts with a system message that says who the model is and what it’s doing. Compare:
Bad: "Summarise this loan application."
Good: "You are a loan-processing assistant for a Nepali microfinance
institution. Given a raw loan application in Nepali or English,
produce a two-sentence English summary highlighting: (1) the loan
amount and purpose, (2) any credit-history flags. Do not include
personal identifying details in the summary."
The bad version is under-specified. The model has to guess what “summarise” means — length, style, audience, what to include, what to leave out. Different calls produce different answers.
The good version is a job description. The model knows exactly what to produce because you told it exactly what to produce. Different inputs still produce different outputs, but the shape is stable.
Habit 2 — explicit constraints
Whenever you have a preference about the output, say it explicitly:
- Length: “in two sentences”, “under 100 words”, “in exactly three bullet points”.
- Format: “as valid JSON”, “as a numbered list”, “as plain text with no markdown”.
- Language: “respond in Nepali using Devanagari script”, “respond in English”.
- Tone: “in the tone of a bank teller — polite and neutral”, “in the tone of a friendly Nepali neighbour”.
- Style: “avoid technical jargon”, “assume the reader is a Class 10 student”.
Every constraint you leave implicit becomes a source of variability. Every constraint you make explicit is a lever you can turn.
Habit 3 — concrete examples (few-shot)
For anything the model might get wrong, give it examples. This is called few-shot prompting, and it’s the biggest quality lever short of a bigger model.
Compare:
Bad: "Classify this SMS as spam or ham."
Good: "Classify each SMS as SPAM or HAM. Examples:
Input: 'Congratulations! You won Rs 50,000. Reply YES to claim.'
Output: SPAM
Input: 'Bhai, are we still meeting for chiya at 5?'
Output: HAM
Input: 'URGENT: Your account will be suspended. Click here to verify.'
Output: SPAM
Now classify:
Input: '{message}'
Output:"
The examples do two things: they show the model the exact format you want (single-word labels, no explanation), and they anchor the model’s sense of what “spam” looks like in your context. For Nepali applications, examples in Nepali help enormously.
For very hard tasks, use more examples — five or ten. For simple tasks, two is often enough. Don’t use fifty; the model wanders as the prompt gets long, and every example costs tokens.
Habit 4 — error handling in the prompt
Real user input is chaos. Real prompts anticipate that:
"Extract the vendor and amount from the following receipt text.
If the vendor cannot be determined, return "UNKNOWN" as the vendor.
If the amount is missing or unparseable, return null.
If the input is not a receipt at all (e.g. it's a random sentence),
return { "error": "not_a_receipt" }.
Receipt text:
{text}"
The prompt itself contains the rules for handling weird input. Now the downstream code has three predictable failure modes to handle — a real vendor, "UNKNOWN", or an error — rather than an unbounded set of “what the hell is this response”.
The pattern is the same at any scale. Ask yourself: “what does the ugly version of the input look like?” and tell the model what to do about it.
Habit 5 — stable structure
For anything the downstream code will parse, define the exact structure and repeat it consistently:
"Return your answer as JSON with exactly these fields:
{
"summary": string, 2-3 sentences
"topics": array of strings, at most 5
"language": string, one of ['en', 'ne', 'mixed']
}
Return nothing else. No markdown code fences, no explanations."
Now the downstream code can json.loads the response with confidence, because the prompt guarantees the shape. We cover structured output in depth in the next section.
A worked prompt — loan officer summary
Putting all five habits together. A prompt for the loan officer scenario from Course 03:
SYSTEM_PROMPT = """You are an assistant to loan officers at a Nepali microfinance
institution. Given a loan application, produce a structured English
summary they can review in 30 seconds.
Guidelines:
- Two-sentence summary, focused on decision-relevant facts.
- Highlight red flags if present: irregular income, high loan-to-income,
missing collateral information, or applicant is a first-time borrower.
- Never include the applicant's citizenship number, phone, or exact
address in the summary.
- Respond in English regardless of the application's language.
"""
USER_PROMPT_TEMPLATE = """Loan application:
---
{application_text}
---
Return your response as JSON:
{{
"summary": string, 2 sentences,
"red_flags": array of strings (at most 3),
"confidence": string, one of ["high", "medium", "low"]
}}
If the application text is incomplete, empty, or clearly not a
loan application, return:
{{"error": "invalid_application", "reason": "brief explanation"}}
"""
def summarise_application(application_text: str) -> dict:
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=600,
temperature=0.0,
system=SYSTEM_PROMPT,
messages=[{
"role": "user",
"content": USER_PROMPT_TEMPLATE.format(application_text=application_text),
}],
)
return json.loads(response.content[0].text)
Read the prompt. It has:
- A clear role (loan officer assistant) and task (structured summary in 30 seconds).
- Explicit constraints (two sentences, English, no PII).
- Implicit examples through the field structure.
- Error handling for empty/invalid input.
- A stable JSON output schema.
Temperature at 0.0. max_tokens at 600 — generous but bounded. This prompt will run reliably on hundreds of applications without surprises.
Iterating on a prompt
Prompts are not written once. The workflow that actually works:
- Draft the first version, following the five habits.
- Try 5-10 realistic inputs, including at least one ugly one.
- Note what the model got wrong — wrong format, wrong emphasis, wrong length, hallucination.
- Edit the prompt to address the most common failure. Usually you add a constraint or an example.
- Re-run the same 5-10 inputs. Some old failures may resurface — that’s the sign your constraints are conflicting.
- Repeat until the prompt handles the whole test set reliably.
By the time a prompt is production-ready, it has usually been through 5-15 iterations. This is boring, honest work. It is also what separates prompts that work from prompts that don’t.
What not to do
Three anti-patterns worth naming:
Don’t over-constrain. Every rule you add costs input tokens and can conflict with other rules. Keep constraints tight and few.
Don’t smuggle rules in via examples that contradict your instructions. If the instructions say “always respond in English” but your examples show Nepali responses, the model will be confused. Consistency between instructions and examples matters.
Don’t confuse the model with unnecessary role-play. “You are the greatest loan officer who ever lived, having decades of experience in Nepali microfinance…” adds tokens, doesn’t add quality, and makes the prompt harder to maintain. Concrete role and task > grandiose persona.
Check your understanding
Quick check
—A colleague's prompt says only 'Summarise this article.' Their app produces summaries of wildly varying lengths — some one paragraph, some ten. What's the first fix, in the language of this section?
Quick check
—A production prompt for classifying SMS occasionally returns explanations like 'This looks like SPAM because...' instead of just 'SPAM'. The downstream code then fails to parse. What's the cleanest fix?
What comes next
You can write prompts. The next section is the specific case of getting structured JSON output — the workhorse pattern for LLM-in-a-pipeline applications. Every real system moves data around in JSON; getting the model to produce it reliably is a whole small craft of its own.