Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
You already know that a modern large language model (LLM) predicts the next token from its input context. That doesn't give it a live copy of your service runbook. If on-call assistant Luna needs an answer about incident INC-10234, your application must supply the applicable runbook line or use tool calling to retrieve it from a trusted source.
Prompt engineering is the practice of packaging instructions, context, examples, and output constraints so a model response can be checked. It isn't a hunt for secret phrases. The useful work is ordinary engineering: state the task, supply trusted facts, isolate untrusted input, define the response shape, and evaluate failures.
Grounding boundary: Prompts don't make an answer trustworthy on their own. A grounded task needs trusted context, a response contract, and a test that catches unsupported outputs.
What a prompt controls
A common beginner mistake is treating a plain LLM call like a database lookup. Without attached retrieval or tools, a generation request doesn't fetch a live runbook during inference. Transformer layers process the supplied tokens and the decoder generates one continuation token after another.
A prompt shapes the next-token distribution. The prompt is the text on the screen so far, and the model's job is to continue it in a way that looks coherent. At the model-input level, prompting is probability shaping: you arrange the input so useful continuations become more likely.
This has two practical consequences:
- Grounded facts need a path into the request. If the paging rule isn't in supplied context or a tool result, the response isn't grounded in your actual rule.
- The context window has a budget. Longer inputs require more processing and, for metered hosted APIs, can add billable input tokens. Keep evidence that matters and measure cost and latency in the next chapter.
Message roles in chat-style requests
Chat-style APIs structure a conversation into roles. Exact role names vary by provider and API, but the idea is the same: separate application instructions, user requests, and prior assistant outputs.
A provider's chat-style request might represent a grounded incident-triage turn like this:
1[
2 {"role": "system", "content": "Answer incident questions only from supplied runbook lines. Return JSON."},
3 {"role": "user", "content": "Runbook P-7: P1 incidents must page on-call within 30 minutes. Incident INC-10234 has P1 severity for 12 minutes."},
4 {"role": "assistant", "content": "{\"action\":\"page_on_call\",\"source_line_ids\":[\"P-7\"]}"},
5 {"role": "user", "content": "Add a one-sentence agent summary for the same decision."}
6]- Application instructions: Higher-priority rules for the active request or managed conversation. APIs may call this layer
developer,system, or expose it separately. It holds stable behavior and format constraints. - User message: The specific request or question from the human.
- Assistant message: The model's previous replies. In a stateless request pattern, you pass these back in so the model can use the prior conversation.
Why roles help
Roles aren't security boundaries. They're structure.
Without roles, the model receives a blob of text and has to infer which parts are instructions, which parts are user data, and which parts are prior answers.
Roles make that separation explicit:
| Role | Plain-English job | Beginner mistake |
|---|---|---|
Application instructions (developer, system, or provider-specific layer) | Stable instructions for behavior and format. | Putting user-specific data here and making it hard to update. |
| User | Current request or task. | Mixing instructions with untrusted pasted content. |
| Assistant | Prior model output in the conversation. | Forgetting to include prior turns when calling a stateless API. |
If an API is stateless, it won't remember old turns unless your application sends them again. Browser chat apps usually manage that history for you. API clients often make you manage it yourself.
Build the message list in code so you can test where dynamic data goes. The assertion prevents incident details from leaking into stable application instructions.
1import json
2
3system_rules = "Answer incident questions only from supplied runbook lines. Return JSON."
4runbook_line = "P-7: P1 incidents must page on-call within 30 minutes."
5incident_context = "Incident INC-10234 has P1 severity for 12 minutes."
6messages = [
7 {"role": "system", "content": system_rules},
8 {"role": "user", "content": f"{runbook_line}\n{incident_context}"},
9]
10
11assert "INC-10234" not in messages[0]["content"]
12assert "INC-10234" in messages[1]["content"]
13print(json.dumps(messages, indent=2))1[
2 {
3 "role": "system",
4 "content": "Answer incident questions only from supplied runbook lines. Return JSON."
5 },
6 {
7 "role": "user",
8 "content": "P-7: P1 incidents must page on-call within 30 minutes.\nIncident INC-10234 has P1 severity for 12 minutes."
9 }
10]You need the model to summarize incident INC-10234 using the current alert status. Which message should hold the changing incident data?
Answer
Put changing incident data in the current request context, usually the user message or a trusted context block your app attaches to that request. Keep the application-instruction layer for stable behavior rules such as tone, role, and format. If you put per-incident data there, it becomes harder to update, audit, and isolate from user-provided text.
From vague to engineered
A prompt can evolve from a vague request to a testable one. Imagine you're building an on-call tool that decides whether an incident needs paging from runbook text.
Vague
1Summarize this.
2Runbook: P1 incidents must page on-call within 30 minutes.Improved
1Summarize the P1 paging rule for an on-call assistant.
2Focus on required action and time window.Engineered
1Role: You are an incident triage assistant.
2
3Task: Decide whether the incident requires paging.
4
5Rules:
6- Use only facts from the runbook and incident context.
7- If the runbook doesn't answer something, say "Not specified."
8
9Runbook:
10<runbook>
11P-7: P1 incidents must page on-call within 30 minutes.
12</runbook>
13
14Incident context:
15<incident>
16Incident INC-10234 has P1 severity for 12 minutes.
17</incident>
18
19Format: Return valid JSON with keys "action", "sla_minutes", "overdue", and "source_line_ids".The engineered version does four useful things:
- Names the task and the role.
- Separates rules from source data.
- Puts the untrusted or changeable text inside a delimiter.
- States the expected output shape so your application knows what to parse and validate.
The delimiter doesn't make prompt injection impossible. It clarifies the layout for the model and gives your application a place to apply extra checks.
Now render the same contract from variables rather than pasting facts into an untestable string:
1from textwrap import dedent
2
3runbook = "P-7: P1 incidents must page on-call within 30 minutes."
4incident = "Incident INC-10234 has P1 severity for 12 minutes."
5question = "Should this incident page on-call?"
6
7prompt = dedent(f"""\
8 Task: Decide incident paging from supplied facts.
9 Rules: Use only <runbook> and <incident>. If unsupported, answer "not_specified".
10 <runbook>
11 {runbook}
12 </runbook>
13 <incident>
14 {incident}
15 </incident>
16 Question: {question}
17 Output JSON fields: action, sla_minutes, overdue, source_line_ids
18""")
19
20assert "<runbook>" in prompt and "</runbook>" in prompt
21assert "P-7" in prompt and "INC-10234" in prompt
22print(prompt)1Task: Decide incident paging from supplied facts.
2Rules: Use only <runbook> and <incident>. If unsupported, answer "not_specified".
3<runbook>
4P-7: P1 incidents must page on-call within 30 minutes.
5</runbook>
6<incident>
7Incident INC-10234 has P1 severity for 12 minutes.
8</incident>
9Question: Should this incident page on-call?
10Output JSON fields: action, sla_minutes, overdue, source_line_ids
Building prompts in layers
Reliable prompts are built in layers. You put stable rules first, add trusted context and examples, isolate untrusted user data, then check the output shape.

At each layer, you choose a prompting pattern based on what the model needs to know:
| Pattern | Add to context | Best beginner use | Failure to watch |
|---|---|---|---|
| Zero-shot | Task instruction only | Common tasks with obvious format | Model guesses hidden requirements. |
| Few-shot | Two or more examples | Exact output shape or style | Bad examples teach the wrong pattern. |
| Structured output | Schema and delimiters | JSON, extraction, tool inputs | User data leaks into instructions. |
| Checkable explanation | Short evidence or calculation fields | Multi-step decisions a reviewer must inspect | Long free-form reasoning adds cost without a useful check. |
Why do we isolate untrusted user text instead of mixing it into the instruction paragraph?
Answer
Isolation makes the prompt easier for the model and the application to inspect. The delimiter doesn't create a security boundary by itself, but it marks which text is data, not instructions. Your app can then validate tools, schemas, and permissions outside the prompt instead of trusting the model to infer that boundary from one long paragraph.
Teaching the model with examples
A zero-shot prompt asks the model to do something without examples. It relies on the model's training, instruction tuning, and the instruction text in the prompt.
User: Classify this issue as
bug,docs, orfeature: "The quickstart code returns a 500 when the token expires."
A few-shot prompt provides examples in the prompt to teach the model the exact format or logic you want. This uses the model's in-context learning ability, made famous by GPT-3.[1]
User: Issue: "The API returns a 500 when the token expires." Label: bug Issue: "Add streaming examples to the SDK docs." Label: docs Issue: "Support batch uploads in the CLI." Label: feature Issue: "The tutorial builds, but the sample output is wrong." Label:
Few-shot prompting is useful when you need a specific JSON shape, classification boundary, or unusual style.
One-shot vs few-shot
You'll also hear one-shot, which sits between zero-shot and few-shot.
| Pattern | Examples in prompt | Best use |
|---|---|---|
| Zero-shot | 0 | Simple tasks where the instruction is enough. |
| One-shot | 1 | Show one output format example. |
| Few-shot | 2 or more | Teach a pattern, rubric, or edge-case boundary. |
Zero-shot prompt
1Classify issue as bug, docs, or feature.
2
3Issue: "The tutorial builds, but the sample output is wrong."
4Label:One-shot prompt
1Classify issue as bug, docs, or feature.
2
3Issue: "Add streaming examples to the SDK docs."
4Label: docs
5
6Issue: "The tutorial builds, but the sample output is wrong."
7Label:Few-shot prompt
1Classify issue as bug, docs, or feature.
2
3Issue: "The API returns a 500 when the token expires."
4Label: bug
5
6Issue: "Add streaming examples to the SDK docs."
7Label: docs
8
9Issue: "Support batch uploads in the CLI."
10Label: feature
11
12Issue: "The tutorial builds, but the sample output is wrong."
13Label:The few-shot version teaches a boundary: a documentation page that builds but shows wrong output is a bug, not a plain docs request.

Bad examples teach bad behavior
On classification and multiple-choice tasks tested by Min et al., randomly replacing demonstration labels barely reduced performance across the evaluated models; label space, input distribution, and format were important signals.[2] Don't misread that result as permission to use wrong examples. A product prompt still needs correct demonstrations and held-out tests because a wrong boundary can create a wrong application action.
| Bad example pattern | Failure |
|---|---|
| Examples use invalid JSON. | Model may copy invalid JSON. |
| Labels contradict each other. | Model learns unclear boundaries. |
| Examples are too easy. | Edge cases still fail. |
| Examples include hidden business decisions. | Model may copy decisions without explanation. |
If a prompt feels unreliable, inspect the examples before blaming the model. Many prompt bugs are data bugs in miniature.
Render each shot strategy from the same label rule, then keep the target issue out of its demonstrations:
1examples = [
2 ("The API returns a 500 when the token expires.", "bug"),
3 ("Add streaming examples to the SDK docs.", "docs"),
4 ("Support batch uploads in the CLI.", "feature"),
5]
6target = "The tutorial builds, but the sample output is wrong."
7
8def make_prompt(shots: int) -> str:
9 lines = ["Classify issue as bug, docs, or feature."]
10 for text, label in examples[:shots]:
11 lines.extend([f'Issue: "{text}"', f"Label: {label}", ""])
12 lines.extend([f'Issue: "{target}"', "Label:"])
13 return "\n".join(lines)
14
15for name, shots in [("zero-shot", 0), ("one-shot", 1), ("few-shot", 3)]:
16 prompt = make_prompt(shots)
17 print(f"{name}: examples={shots}, chars={len(prompt)}")
18 print(prompt.splitlines()[-2:])1zero-shot: examples=0, chars=109
2['Issue: "The tutorial builds, but the sample output is wrong."', 'Label:']
3one-shot: examples=1, chars=176
4['Issue: "The tutorial builds, but the sample output is wrong."', 'Label:']
5few-shot: examples=3, chars=297
6['Issue: "The tutorial builds, but the sample output is wrong."', 'Label:']Examples used in the prompt aren't evaluation evidence. Reserve separate gold cases so you can see whether a revised prompt generalizes beyond the demonstrations:
1demonstrations = {
2 "issue_train_01": "bug",
3 "issue_train_02": "docs",
4 "issue_train_03": "feature",
5}
6held_out_expected = {
7 "issue_eval_wrong_sample_output": "bug",
8 "issue_eval_missing_doc_example": "docs",
9}
10
11overlap = set(demonstrations) & set(held_out_expected)
12assert not overlap, f"leaked eval cases into prompt: {sorted(overlap)}"
13print("demonstrations:", len(demonstrations))
14print("held_out_cases:", len(held_out_expected))
15print("leakage_check: PASS")1demonstrations: 3
2held_out_cases: 2
3leakage_check: PASSReasoning prompts
One important prompt engineering result is chain-of-thought prompting: Wei et al. supplied worked intermediate steps as few-shot exemplars and reported improvements on arithmetic, commonsense, and symbolic reasoning benchmarks for sufficiently large models.[3] Advanced reasoning patterns return later in the roadmap.
If you ask an LLM a multi-step token-budget question and demand only the final number, it can fail silently. A short, checkable calculation gives a reviewer intermediate values to verify.
Standard prompt
Q: A prompt starts with 5 retrieved chunks, removes 2 stale chunks, adds 3 fresh chunks, and each chunk is 2,000 tokens. How many retrieved-context tokens remain? A:
For models and tasks where it helps, ask for useful intermediate work: a short calculation, cited source lines, or a few-shot example with checkable steps. Don't expose unrestricted deliberation. Request evidence a reviewer or test can verify.
Worked-reasoning prompt
Q: A prompt starts with 5 retrieved chunks, removes 2 stale chunks, adds 3 fresh chunks, and each chunk is 2,000 tokens. How many retrieved-context tokens remain? A: Short solution: 5 chunks - 2 stale chunks = 3. Then 3 + 3 fresh chunks = 6 chunks. Each chunk is 2,000 tokens, so 6 x 2,000 = 12,000. The answer is 12,000.
By generating intermediate steps, the model adds those steps to its context window. It can use its visible output as a scratchpad, making the final answer easier to predict.
For applicable OpenAI reasoning models, current API documentation recommends giving the task, constraints, and desired output format, then treating reasoning.effort as a tuning knob rather than the primary way to recover quality.[4] Don't assume visible chain-of-thought prompts help every provider or model. Compare prompt variants on held-out cases and measure answer quality, tokens, and latency.
When visible reasoning helps
Use visible reasoning when:
- The task has multiple steps.
- The answer can be checked.
- The model benefits from writing intermediate values.
- You aren't asking for sensitive hidden deliberation.
Avoid long visible reasoning when:
- The task is simple.
- You need a short production answer.
- A reasoning-specific model already has an internal reasoning interface.
- The reasoning could expose private deliberation you don't want to store or display.
Application prompts should ask for structured evidence instead of unrestricted reasoning.
- Avoid:
Think step by step and show all your reasoning.
- Prefer:
Return:
- answer
- 2-sentence explanation
- source line IDs used
- missing facts, if any
That gives users something useful to inspect without turning every response into a long scratchpad.
For an incident decision, a compact evidence contract is easier to check than an open-ended monologue:
1runbook_lines = {"P-7": "P1 incidents must page on-call within 30 minutes."}
2incident = {"incident_id": "INC-10234", "severity": "P1", "minutes_active": 12}
3
4should_page = incident["severity"] == "P1"
5answer = {
6 "action": "page_on_call" if should_page else "not_specified",
7 "overdue": should_page and incident["minutes_active"] > 30,
8 "calculation": f"{incident['minutes_active']} > 30",
9 "source_line_ids": ["P-7"],
10}
11
12assert all(line_id in runbook_lines for line_id in answer["source_line_ids"])
13print(answer)1{'action': 'page_on_call', 'overdue': False, 'calculation': '12 > 30', 'source_line_ids': ['P-7']}The 30-minute value is a deadline, not an eligibility window. Every P1 incident pages; overdue separately records whether the page is already late.
Locking the output shape
Prompts become much easier to test when the output has a shape.
For example, an incident triage tool might need this object:
1{
2 "action": "page_on_call",
3 "sla_minutes": 30,
4 "overdue": false,
5 "needs_human": true,
6 "source_line_ids": ["P-7"],
7 "summary": "Incident INC-10234 is P1 and must page on-call within 30 minutes."
8}You can ask for JSON in plain text, but production systems should use a provider's schema-constrained output facility when it supports the needed schema. In OpenAI's API, Structured Outputs with a supported strict JSON schema is distinct from JSON mode: Structured Outputs enforces supported schema adherence, while JSON mode targets valid JSON and still has documented edge cases that callers must handle.[5] Regardless of provider, your application must still handle refusals or incomplete output and validate business facts and permissions.
Minimal schema prompt
If you're using a plain prompt, still define the shape explicitly:
1Return valid JSON with these fields:
2- action: one of ["page_on_call", "escalate_to_human", "not_specified"]
3- sla_minutes: integer or null
4- overdue: boolean; true only when a required page is past its SLA
5- needs_human: boolean
6- source_line_ids: list of runbook line IDs
7- summary: one sentence
8
9Return only the JSON object.Then validate the result in code. A prompt isn't a parser.
| Output issue | What to do |
|---|---|
| Missing field | Reject and retry with validation error. |
| Extra text | Strip only if safe, otherwise retry. |
| Invalid enum | Reject and ask the model to choose an allowed value. |
| Unsupported claim | Re-run with source text and require citations. |
| Refusal, safety filter, or incomplete structured output | Fail and inspect (or try one bounded repair with a fixed prompt). Blind retries create a loop; the next lesson owns the retry classifier. |
This is the bridge from prompt engineering to system design: prompts are only one layer. Schemas, validators, tests, and fallback paths make the behavior reliable.
Why isn't "return JSON" enough for a production prompt?
Answer
It describes the desired shape to the model, but it doesn't guarantee valid output. Production code should still parse the response, reject missing fields or invalid enum values, and retry or fall back with a clear validation error.
You can start with a validator before you ever call a model. One candidate is correct; the other contains a plausible but unsupported SLA:
1import json
2
3known_runbook_lines = {"P-7": 30}
4incident = {"severity": "P1", "minutes_active": 12}
5candidates = [
6 '{"action":"page_on_call","sla_minutes":30,"overdue":false,"needs_human":true,"source_line_ids":["P-7"]}',
7 '{"action":"page_on_call","sla_minutes":90,"overdue":false,"needs_human":true,"source_line_ids":["P-7"]}',
8 '{"action":"page_on_call","sla_minutes":30,"overdue":true,"needs_human":true,"source_line_ids":["P-7"]}',
9 '{"action":"page_on_call","sla_minutes":30,"overdue":"false","needs_human":true,"source_line_ids":["P-7"]}',
10]
11
12def validate(candidate: str) -> list[str]:
13 data = json.loads(candidate)
14 errors = []
15 allowed_actions = {"page_on_call", "escalate_to_human", "not_specified"}
16 if data.get("action") not in allowed_actions:
17 errors.append("invalid action")
18 source_ids = data.get("source_line_ids", [])
19 if source_ids != ["P-7"] or data.get("sla_minutes") != known_runbook_lines["P-7"]:
20 errors.append("SLA isn't supported by P-7")
21 overdue = data.get("overdue")
22 expected_overdue = incident["minutes_active"] > known_runbook_lines["P-7"]
23 if type(overdue) is not bool:
24 errors.append("overdue must be a boolean")
25 elif overdue != expected_overdue:
26 errors.append("overdue conflicts with trusted incident duration")
27 return errors
28
29for index, candidate in enumerate(candidates, start=1):
30 errors = validate(candidate)
31 print(f"candidate_{index}:", "PASS" if not errors else f"FAIL {errors}")1candidate_1: PASS
2candidate_2: FAIL ["SLA isn't supported by P-7"]
3candidate_3: FAIL ['overdue conflicts with trusted incident duration']
4candidate_4: FAIL ['overdue must be a boolean']Anti-patterns that break prompts
Even good prompts fail when common mistakes slip in. Watch for these four anti-patterns.
The wall of text
Without visible boundaries, instruction drift is easier. When instructions, user data, and examples are all mashed together in one paragraph, the model has to guess where one ends and the other begins. Instead, use headers, XML tags, or triple backticks to create visible boundaries.
The hallucination trap
Asking a model for facts it doesn't have, without providing a knowledge base, invites fabrication. If your prompt asks for an incident escalation rule but the runbook line isn't supplied, the model may emit a plausible SLA. Retrieve trusted evidence (or use a tool), then require source line IDs.
Negative constraints
Positive constraints are usually more actionable than negative-only instructions. Instead of "Don't make up details," say "Only use facts from the provided runbook. If something is missing, say 'Not specified.'"
Token waste
Including pages of unrelated runbook text when only one rule is relevant adds input work and distractors. Before sending a long document, ask: what is the smallest evidence slice needed for this question? Retrieve relevant chunks and place decisive evidence close to the final request; long-context studies have found lower performance when relevant information is placed in the middle of long inputs.[6]
This toy keyword retriever isn't a production search engine, but it proves the prompt-building idea: carry only lines that can support the answer and keep their IDs.
1runbook_lines = {
2 "P-1": "P2 incidents page during business hours.",
3 "P-7": "P1 incidents must page on-call within 30 minutes.",
4 "P-9": "Deploy freezes require incident commander approval.",
5 "P-12": "Security incidents need a separate response lead.",
6}
7incident = "Incident INC-10234 is P1. Should we page on-call?"
8query_terms = {"p1", "30"}
9
10selected = {
11 line_id: text
12 for line_id, text in runbook_lines.items()
13 if query_terms & set(text.lower().replace(".", "").split())
14}
15assert "P-7" in selected
16print("all_runbook_lines:", len(runbook_lines))
17print("selected_lines:", selected)1all_runbook_lines: 4
2selected_lines: {'P-7': 'P1 incidents must page on-call within 30 minutes.'}Delimiters don't grant permissions
Putting operator text in <incident> tags helps identify it as data. It can't stop the model from proposing an unauthorized tool action. Authorization belongs in code outside the prompt.
1allowed_actions = {"draft_update", "escalate_to_human"}
2model_proposals = [
3 {"action": "draft_update", "incident_id": "INC-10234"},
4 {"action": "delete_index", "incident_id": "INC-10234"},
5]
6
7for proposal in model_proposals:
8 action = proposal["action"]
9 authorized = action in allowed_actions
10 print(f"{action:17s} -> {'ALLOW' if authorized else 'BLOCK'}")1draft_update -> ALLOW
2delete_index -> BLOCKWhen a prompt fails
When a prompt fails, don't rewrite the whole thing first. Debug it like a program.

| Symptom | Likely cause | Fix |
|---|---|---|
| Output format changes every run | Format examples are missing or inconsistent. | Add schema or few-shot examples. |
| Model ignores key instruction | Instruction is buried in long context. | Repeat critical rule near final request. |
| Model follows malicious pasted text | Untrusted data isn't isolated. | Delimit user data and add tool permissions outside the prompt. |
| Answer is plausible but false | Source text is missing or stale. | Retrieve trusted runbook lines and require line IDs. |
| Too verbose | Prompt asks for reasoning but app needs answer. | Ask for concise answer plus short explanation. |
A tiny prompt test
Test this prompt by hand:
1Task:
2Decide incident paging and extract the SLA from the supplied runbook and incident context.
3
4Runbook:
5<runbook>
6P-7: P1 incidents must page on-call within 30 minutes.
7P-9: Deploy freezes require incident commander approval.
8</runbook>
9
10Incident context:
11<incident>
12Incident INC-10234 has P1 severity for 12 minutes.
13</incident>
14
15Return JSON:
16{"action": string, "sla_minutes": number, "overdue": boolean, "source_line_ids": [string]}Expected output
1{
2 "action": "page_on_call",
3 "sla_minutes": 30,
4 "overdue": false,
5 "source_line_ids": ["P-7"]
6}You shouldn't trust that shape by inspection alone. Parse it and check the fields you care about:
1import json
2
3candidate = """
4{"action": "page_on_call", "sla_minutes": 30, "overdue": false, "source_line_ids": ["P-7"]}
5"""
6
7data = json.loads(candidate)
8errors = []
9trusted_minutes_active = 12
10
11required = {"action", "sla_minutes", "overdue", "source_line_ids"}
12missing = sorted(required - data.keys())
13if missing:
14 errors.append(f"missing keys: {missing}")
15
16if data.get("action") != "page_on_call":
17 errors.append("action should be page_on_call")
18
19if data.get("sla_minutes") != 30:
20 errors.append("sla_minutes should be 30")
21
22overdue = data.get("overdue")
23if type(overdue) is not bool:
24 errors.append("overdue must be a boolean")
25elif overdue != (trusted_minutes_active > data.get("sla_minutes", 0)):
26 errors.append("overdue conflicts with trusted incident duration")
27
28if data.get("source_line_ids") != ["P-7"]:
29 errors.append("source_line_ids should identify P-7")
30
31print("PASS" if not errors else "FAIL")
32for error in errors:
33 print(error)1PASSIf the model returns 90 minutes, it contradicts supplied evidence. If it omits source_line_ids, it failed the traceability contract.
That's how prompt engineering becomes engineering: define expected behavior, run examples, and check failures.
Once a failing case exists, turn it into a regression test for candidate prompt versions. Here, outputs are fixtures standing in for results collected from two prompt versions; the test logic is real.
1expected = {
2 "p1_12_minutes": ("page_on_call", 30, ["P-7"]),
3 "missing_runbook": ("not_specified", None, []),
4}
5outputs_by_version = {
6 "vague_v1": {
7 "p1_12_minutes": ("page_on_call", 90, []),
8 "missing_runbook": ("page_on_call", 30, []),
9 },
10 "grounded_v2": {
11 "p1_12_minutes": ("page_on_call", 30, ["P-7"]),
12 "missing_runbook": ("not_specified", None, []),
13 },
14}
15
16for version, outputs in outputs_by_version.items():
17 passed = sum(outputs[case] == answer for case, answer in expected.items())
18 print(f"{version}: {passed}/{len(expected)} held-out checks passed")1vague_v1: 0/2 held-out checks passed
2grounded_v2: 2/2 held-out checks passedChoosing the right pattern
Before writing a prompt, answer three questions:
- What information must the model know?
- What output shape must the app consume?
- How will you tell whether the response is wrong?
If you can't answer those, the prompt isn't ready for production.
Use this quick decision table for common tasks:
| Task | Best starting pattern | Why |
|---|---|---|
| Summarize one alert | Zero-shot | The task is familiar and format is simple. |
| Extract incident action into JSON | Structured output | Parser reliability matters. |
| Classify tricky issue reports | Few-shot | Examples teach category boundaries. |
| Estimate context-token budget | Checkable calculation or reasoning model | Intermediate quantities can be verified. |
| Answer from a runbook document | Retrieved context plus source quote | Local facts matter more than model memory. |
Mastery check
Key concepts
- Chat-style API roles and message structure
- Prompt anatomy: stable rules, trusted context, examples, user task, output shape
- Zero-shot vs One-shot vs Few-shot prompting
- Checkable explanations and when reasoning prompts help or hurt
- Clear instructions, delimiters, output schemas, and regression tests
Evaluation rubric
- Foundational: Explains the difference between application instructions (such as developer, system, or platform rules), user messages, and assistant messages
- Foundational: Separates stable instructions, changing context, examples, untrusted user data, and output constraints
- Foundational: Differentiates between Zero-shot and Few-shot prompting
- Intermediate: Designs checkable evidence fields for a multi-step answer without demanding unrestricted reasoning
- Intermediate: Explains how schemas and validators turn prompts into testable application behavior
- Intermediate: Diagnoses ignored instructions, malformed JSON, missing source context, and unauthorized actions with regression tests
Follow-up questions
Why might a short, checkable calculation help on a multi-step token-budget question?
Answer
The model gets intermediate quantities to work with, and a reviewer gets values to verify before accepting the final result. Whether it improves accuracy depends on the model and task, so compare it on held-out cases. For APIs with their own reasoning controls, follow provider guidance and request concise evidence rather than unrestricted reasoning.
You need the model to classify issue reports into three labels, and you have two gold examples for each label. Should you start zero-shot, one-shot, or few-shot?
Answer
Start few-shot because the task depends on label boundaries more than generic world knowledge. A couple of representative examples per class help the model see what distinguishes bug, docs, and feature issues. Zero-shot is still worth testing as a baseline, but the examples give you a stronger first controlled prompt.
The model keeps returning text like Here is the JSON: before the object, which breaks your parser. What should you change first?
Answer
Tighten the output contract, not the downstream hope. Tell the model to return only the JSON object, show one valid example, and validate the parsed structure before using it. If the application needs strict formatting, use a provider-supported schema constraint when available and still parse and validate the result so malformed output fails fast instead of leaking into business logic.
Your system prompt says Answer with the provided runbook only, but the reply still invents an escalation rule that is not in the context. What is the first debugging question to ask?
Answer
Ask whether the needed runbook text was actually present in the prompt or retrieved context. Ignored-instruction bugs often look like model disobedience when the real issue is missing source material, weak grounding format, or too much distractor text around the critical rule. Verify the input before rewriting the prompt again.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Model invents an escalation rule or runbook detail | Needed fact was never placed in prompt, retrieved context, or tool result | Add trusted runbook lines, require source line IDs, and fail when the source is missing |
| Model ignores a key instruction | Critical rule is buried inside long context or mixed with noisy data | Move the rule near the final request, isolate data with delimiters, and rerun the exact failing case |
| User text hijacks the task | Delimiters were treated like security instead of layout | Keep user data isolated, but also enforce tool permissions, schema validation, and logging outside the prompt |
| JSON breaks parser in production | Prompt asked for JSON, but app trusted raw text without validation | Parse response, reject missing fields or invalid enums, and retry with exact validation error |
| Refusal or incomplete structured output storms retries | App treats every schema/provider failure as transient | Classify refusal, content-filter, incomplete, and business-validation failures as fail-and-inspect (or one repair), not exponential retry |
Practice drill
Build a prompt-debug regression for one issue classifier:
- Write the system or application instruction, the user issue, two good few-shot examples, and one expected JSON schema.
- Add three failing cases: missing runbook evidence, user-injected instruction text, and malformed JSON.
- Define the validator checks for schema, allowed labels, source-line support, and permitted actions.
- Keep the fixed cases in a regression table with expected output and the prompt layer changed.
The useful prompt artifact is a tested contract between input, model output, and application validation.