Build and test grounded prompts with clear roles, few-shot examples, structured outputs, evidence checks, and failure-focused evaluation.
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.
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:
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]developer, system, or expose it separately. It holds stable behavior and format constraints.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]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.
1Summarize this.
2Runbook: P1 incidents must page on-call within 30 minutes.1Summarize the P1 paging rule for an on-call assistant.
2Focus on required action and time window.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:
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
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. |
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.
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. |
1Classify issue as bug, docs, or feature.
2
3Issue: "The tutorial builds, but the sample output is wrong."
4Label: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: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.
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: PASSOne 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.
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.
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.
Use visible reasoning when:
Avoid long visible reasoning when:
Application prompts should ask for structured evidence instead of unrestricted reasoning.
Think step by step and show all your reasoning.
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.
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.
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.
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']Even good prompts fail when common mistakes slip in. Watch for these four anti-patterns.
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.
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.
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.'"
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.'}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, 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. |
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]}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 passedBefore writing a prompt, answer three questions:
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. |
| 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 |
Build a prompt-debug regression for one issue classifier:
The useful prompt artifact is a tested contract between input, model output, and application validation.
Answer every question, then check your score. Score 75% or higher to mark this lesson complete.
8 questions remaining.
Language Models are Few-Shot Learners.
Brown, T., et al. · 2020 · NeurIPS 2020
Rethinking the Role of Demonstrations: What Makes In-Context Learning Work?
Min, S., Lyu, X., Holtzman, A., Artetxe, M., Lewis, M., Hajishirzi, H., & Zettlemoyer, L. · 2022 · EMNLP 2022
Chain-of-Thought Prompting Elicits Reasoning in Large Language Models.
Wei, J., et al. · 2022 · NeurIPS
Reasoning models
OpenAI · 2026
Structured outputs
OpenAI · 2024
Lost in the Middle: How Language Models Use Long Contexts
Liu, N.F., et al. · 2023 · TACL 2023
Questions and insights from fellow learners.