Module 01 — Prompt Engineering Fundamentals¶
Time: 2–3 days · Depends on: Setup · Next: Security & privacy
Fictional teaching scenario.
The reply you cannot send
The customer asks, “Any demo slots Tuesday?” You ask an email assistant to draft a reply. It offers 10 a.m. You never gave it a calendar. A second draft avoids the time but leaves out the risks section your reviewer needs. Both sound ready to send; neither is ready to trust.
Case question: How do you make the request reviewable — which facts may the reply use, which sections must it contain, and what should it do when a fact is missing? In the lab, you will test that contract on a real message and record where it still fails.
Try before reading: Write one instruction that would prevent an invented appointment and one that would make a missing section easy to spot. Keep them beside your notes; compare them with the constraints and format below. No model call needed yet.
Learning objectives¶
- Structure prompts with role, task, context, constraints, and output format so a colleague can maintain them
- Prefer specificity and measurable success criteria over clever wording
- Choose temperature and message roles intentionally for extractive vs. generative work
- Treat prompts as versioned config, not one-off chat experiments
- Spot beginner failure modes before they become production incidents
Mental model¶
As a software engineer, you already work with API contracts. An LLM call has a soft contract: the same input does not guarantee the same output. Think of a chat completion as a request lifecycle, not a magic box. Your code assembles messages, the provider samples tokens under a decoding policy, and your code must validate what comes back before it becomes a side effect.
flowchart LR
subgraph app [Your service]
CFG[Prompt config / templates]
U[User or system event]
B[Message builder]
V[Output validator]
end
subgraph provider [Model provider]
M[Chat model]
D[Decoding: temp, max tokens]
end
U --> B
CFG --> B
B -->|system + user messages| M
D --> M
M -->|assistant text / structured| V
V -->|ok| Downstream[DB / UI / tools]
V -->|fail| Retry[Retry / fallback / human]
Roles are privilege levels. system (or provider equivalent) carries policy and product rules. user carries untrusted task data. assistant is model output—never treat it as ground truth without checks. Mixing user text into the system slot is how injection and policy bypasses start (Module 02).
Intuition lock
Sticky picture: A prompt is a soft API contract—like a REST endpoint that returns prose instead of JSON. Message roles are privilege levels on a shared wire: system is the root policy plane; user is an untrusted request body; assistant is a draft response you must still validate before it becomes a side effect. Temperature is a randomness dial on the decoder, not a “creativity magic” slider—turn it down when you need stable labels, not vibes.
Kill this idea: “A good prompt is clever wording that makes the model smart.” → Replace with: A good prompt is a maintainable contract—role, task, context, constraints, format—tight enough that two engineers can grade pass/fail the same way.
Core tutorial¶
1. Anatomy of a good prompt¶
| Part | Purpose | Example |
|---|---|---|
| Role | Stance, expertise, refusal boundaries | “You are a senior support engineer for Acme Billing.” |
| Task | Single clear verb + artifact | “Summarize the ticket and propose next steps.” |
| Context | Facts the model cannot invent | Ticket text, product tier, SLA, today’s date |
| Constraints | Safety, length, what not to do | “No legal claims. Max 120 words.” |
| Format | Machine- or human-parseable shape | Markdown sections / JSON keys |
| Examples (optional) | Few-shot anchors | 1–3 input→output pairs (Module 03) |
If you cannot name the artifact (JSON object, email body, label enum), the prompt is not ready for production.
For the Tuesday reply, “be helpful” leaves both failures open. Your constraint can say “Do not invent availability; ask when it is unknown.” Your format can require ## Summary, ## Response, and ## Risks. One limits unsupported claims; the other makes omissions inspectable. Neither verifies the output for you.
Minimal pattern¶
System: You are {role}. Follow policies: {policies}.
User:
Task: {task}
Context:
{context}
Constraints:
- {c1}
- {c2}
Output format:
{format}
Explainer
Why separate system and user? System content is your product’s constitution: tone, allowed actions, “never invent policy.” User content is this turn’s payload. Keeping them separate lets you log, redact, and unit-test the user path without rewriting policy. Most providers also apply different trust treatment to system vs. user content—do not fight that model by stuffing everything into one blob.
2. Specific beats generic¶
# Weak — no artifact, no success criteria
Analyze this.
# Strong — role, structure, anti-speculation
You are a data analyst. Given the CSV summary below, return:
1) three quantitative findings
2) one data-quality risk
3) two follow-up questions
Use plain language. No speculation beyond the data.
Ask yourself: Could two engineers independently grade whether the output is correct? If not, add criteria.
Think about it
Question: Friday, a VP screenshots one “cold” reply and slacks “make the bot friendlier” before a board demo Monday. Is that a prompt change, a product rule, or both? Where do you put it so you’re not rewriting tone in three services under a deadline—and how do you prove it worked without another screenshot war?
Reveal a strong answer
“Friendlier” is an underspecified product rule, not a vibe you chase in one chat. Encode it as **observable constraints** in versioned system policy (e.g. greet by name when present, avoid sarcasm, offer one clear next step, under 120 words). Measure with a small rubric on fixed tickets (tone checklist + length + “has next step”), not a single demo. One config path beats three hardcoded strings so product can change tone without a scavenger hunt.3. Context is a scarce budget¶
Include facts the model cannot know: internal IDs, policy snippets, current date, plan tier. Exclude noise: entire ticket history when one paragraph suffices; full HTML when plain text will do.
Every extra token has two costs: you wait and pay for it, and you dilute the model’s attention — it has a finite ability to track what matters in a long prompt. Over-stuffed prompts often fail more than tight ones. You are not billed twice; you spend quality as well as money.
Later: packing and memory tiers (Module 05), retrieval (Module 07). For now, curate context by hand.
4. Control the output shape¶
Downstream systems need contracts:
- Humans: Markdown with fixed headings (
## Summary,## Response,## Risks) - Machines: JSON with named fields, or provider structured-output modes (Module 03)
Do not parse “whatever the model felt like saying” with fragile regex in production without a fallback path.
Return to the draft: all three headings are present, but ## Response still offers 10 a.m. Has it passed? Only the shape check. Compare its claims with the supplied facts before sending. The lab records section presence and invented facts separately for this reason.
5. Temperature matches the task¶
What a token is (30 seconds)¶
The model does not read characters or words. It reads tokens — chunks of text from a fixed vocabulary (a whole common word, a subword like ing, a punctuation mark). “Hello” is usually one token; a UUID or a CJK character may be several. Providers bill and cap tokens, not words.
On each step the model outputs a probability distribution over the next token. Sampling picks from that distribution. That is the whole generation loop: tokens in → next-token distribution → sample → append → repeat until a stop token or max_tokens.
Explainer
Temperature sits on that distribution. At 0 the model (almost) always takes the highest-probability next token — good for labels and JSON. At 0.8 it samples farther into the tail — more variety, more chance of a weird key name. Temperature cannot invent a task you did not specify. Some “reasoning” models ignore temperature; treat the table below as a starting point for ordinary chat models, then read your provider’s decoding docs.
Temperature 0 reduces sampling variability, but it does not guarantee exact, bit-for-bit reproducibility. Batching effects, mixture-of-experts routing, floating-point non-associativity across hardware, and silent provider-side model/serving updates can all still change the output for the same prompt. Treat temperature 0 as "much more consistent," not "deterministic" — pin the model/prompt version (Module 13) and use evals, not string equality, to catch drift.
Temperature (and related sampling knobs) trade determinism for diversity. Lower temperature concentrates probability mass; higher temperature samples more creative tails.
| Task | Temperature (starting point) |
|---|---|
| Extraction, classification, routing | 0–0.2 |
| Support replies, summaries | 0.2–0.5 |
| Brainstorming, marketing variants | 0.7–1.0 |
Explainer
Picture the dial, not the muse. You already know the next-token distribution from the token note above. At low temperature the sampler stays near the peak — good when you need the same invoice total twice. Crank it up and you sample longer tails: more variety, more chance of a weird label or invented fact. Turning the dial does not repair a vague task; it only changes how loudly the model explores once the contract is set.
Rules of thumb:
- If you will unit-test field equality, stay low and prefer structured outputs.
- If product wants three draft options, raise temperature or ask for N variants in one call with clear separators—measure cost either way.
- Temperature does not fix a vague prompt. Specificity first.
6. Message roles across providers¶
The chat messages abstraction is portable even when SDKs differ:
| Role | Typical use |
|---|---|
system |
Product policy, role, global constraints |
user |
Task + context for this turn |
assistant |
Prior model turns (multi-turn) or few-shot demonstrations |
tool / tool results |
Structured tool returns (later modules) |
OpenAI-style APIs, Anthropic messages, Gemini, and local Ollama chat endpoints all map onto this idea. Learn the roles, not a single vendor’s method names.
Runnable sketch (OpenAI-style)¶
Preserve this as a pattern: system policy separate from user payload; temperature set deliberately; empty content handled.
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY in env
def smart_email_responder(email_content: str) -> str:
response = client.chat.completions.create(
model="gpt-4o-mini", # swap for Claude / Gemini / Ollama via your stack
messages=[
{
"role": "system",
"content": (
"You are a professional email assistant. "
"Be concise, polite, and action-oriented. "
"Do not invent company policies or prices."
),
},
{
"role": "user",
"content": (
f"Write a reply to this email.\n\n"
f"Email:\n{email_content}\n\n"
f"Requirements:\n"
f"- Acknowledge the request\n"
f"- Answer questions if possible\n"
f"- Propose a clear next step\n"
f"- Under 150 words"
),
},
],
temperature=0.3,
)
return response.choices[0].message.content or ""
if __name__ == "__main__":
print(
smart_email_responder(
"Hi, interested in a product demo next week. Any slots Tuesday?"
)
)
Use Anthropic / Gemini / Ollama with the same message roles idea; only the SDK and model id differ.
This sketch separates messages, but its requirements do not yet ask for the lab’s three headings or forbid invented appointment times. Carry your opening instructions into your lab version; copying the sketch alone does not complete the contract.
7. Prompts as config (production mindset)¶
Ad-hoc f-strings in business logic do not scale. Prefer named templates, versioned files, and a single render path.
This course ships src.prompts:
from src.prompts import list_templates, render
print(list_templates())
# ['classify', 'email_reply', 'rag_answer', 'summarize']
user_msg = render(
"email_reply",
max_words=150,
content="Hi, interested in a product demo next week. Any slots Tuesday?",
)
print(user_msg)
# src/prompts.py pattern (simplified)
from string import Template
TEMPLATES = {
"email_reply": Template(
"You are a professional email assistant.\n"
"Write a polite reply under $max_words words.\n\n"
"Email:\n$content\n\n"
"Requirements:\n"
"- Acknowledge the request\n"
"- Answer questions if possible\n"
"- Propose a clear next step"
),
}
def render(name: str, **kwargs) -> str:
return TEMPLATES[name].safe_substitute({k: str(v) for k, v in kwargs.items()})
Operational habits:
- Version prompts (
prompts/v1/email_reply.md→v2/...) when behavior changes. - Code review prompt diffs like API contract changes.
- Log
prompt_versionwith each request for debugging. - Never embed API keys, other users’ data, or secrets in templates.
- Hand off to evals (Module 04) before promoting a prompt version.
Explainer
Prompt vs. weights. Changing a prompt is a config deploy: fast, reversible, cheap to A/B. Fine-tuning (Module 06) changes model behavior more deeply but costs data pipelines and eval rigor. Default path for product features: prompt + retrieval + tools, then consider fine-tuning only when those saturate.
8. One prompt per job¶
A single “do everything” prompt is the LLM equivalent of a 2,000-line god function. Prefer pipelines:
- Classify intent → label enum
- Extract fields if needed → JSON
- Generate user-facing text with a dedicated template
Each stage can have its own temperature, model size, and tests. Fail closed on stage 1 when confidence is low.
Think about it
Question: You have one model call that both extracts invoice fields and writes a customer email. Parse rate is 70%. What do you change first—temperature, model, or architecture—and why?
Reveal a strong answer
**Architecture first:** split extraction from generation. Extraction wants low temperature (or structured outputs) and a schema validator; generation wants tone and length constraints. A coupled prompt forces one decoding policy to serve two jobs, which usually hurts parse reliability. After the split, use a small/cheap model for extraction if quality holds, and only then tune temperature or upgrade the generator.Common failure modes¶
| Failure mode | Root cause | Fix |
|---|---|---|
| Vague verbs (“handle this”) | No named artifact or success criteria | State output sections or schema |
| Free-text where JSON was needed | No format + no validator | Schema / structured output + retry |
| Wild variance across runs | High temperature + vague task | Lower temp; tighten constraints |
| Hallucinated policy / prices | Missing context; no “don’t invent” rule | Inject policy snippets; forbid invention |
| Overlong context | Paste-driven prompting | Summarize; retrieve; drop noise |
| One mega-prompt for all tasks | Convenience over modularity | Classify → extract → write |
| Prompt only lives in Slack | No ownership | Versioned templates + prompt_version logs |
| Secrets in prompts | Convenience | Tools / server-side fetch; secret manager |
Case checkpoint
Opening failure: The reply looked polished while inventing an appointment and omitting a required section.
What this lab demonstrates: Your ten-run comparison tests section presence and unsupported claims, then ties one new constraint to a failure you actually observed.
What it does not prove: A small sample does not guarantee factual correctness on future messages or authorize the reply to be sent automatically.
Lab¶
Artifact: a small script (or notebook cell) that produces a stable Markdown reply for a real email or GitHub issue you wrote.
The opening drafts were illustrative. Your evidence comes from the outputs you collect here: check the required sections and compare claims with your input. If the failures differ from the opening, record those instead.
Steps
- Capture one real input (redact secrets/PII).
- Write system + user messages using the six anatomy parts.
- Require Markdown sections:
## Summary,## Response,## Risks. - Run 5 times at
temperature=0.2and 5 times at0.8. Note variance (length, section presence, invented facts). - Add one negative constraint that removes a failure mode you observed (e.g. “Do not invent meeting times; ask if unknown”).
- Optional: move the user body into
src.promptsviarender("email_reply", ...)or a new template name in a local fork of the pattern. - Log notes in your progress tracker.
Acceptance criteria
- System policy is separate from user payload
- Output always includes the three headings at temp 0.2
- You recorded at least one qualitative difference between 0.2 and 0.8
- One negative constraint is present and motivated by an observed failure
# If you wire into the course package templates:
poetry run python -c "from src.prompts import render; print(render('email_reply', max_words=120, content='Hello'))"
poetry run pytest tests/test_prompts.py -v
Knowledge check (quiz)¶
Quiz · +25 XP
Why keep system policy in a separate message role from the user’s email text?
Quiz · +25 XP
You are building a router that must return one of: billing, tech, sales. Best starting temperature?
Open source materials¶
Read with purpose—skim for patterns you will copy into your templates:
- dair-ai/Prompt-Engineering-Guide / promptingguide.ai — anatomy, tactics, and pitfalls; use as a catalog, not a checklist to apply all at once.
- mlabonne/llm-course — broader LLM engineering map; situate prompting among fine-tuning and tooling.
- Provider docs (OpenAI / Anthropic / Google / Ollama) — message roles, temperature, and structured outputs for the API you actually call.
- Course
src.prompts— minimal template registry; study how named templates beat ad-hoc strings.
Keep 3–4 sources open while you do the lab; do not try to memorize every technique yet (Module 03).
Checkpoint¶
- You can name the six prompt parts from memory
- You can explain system vs. user roles as a trust boundary
- You have one runnable script that returns structured Markdown or uses
src.prompts - You know when to lower temperature and why
Conceptual self-test
- Rewrite “Improve this ticket” into a prompt with task, constraints, and format.
- Where would you store prompt text so a PM can request a tone change without a mystery deploy?
- What breaks if user content is concatenated into the system message?
Mark this module complete when you can teach the mental model and ship the lab artifact.
Exercise¶
- Catalog: EX-01 — Templates
- Prove: A named template renders; untrusted user text stays out of the system role.
- Test:
pytest tests/test_prompts.py -v
Your result is a reviewable reply contract and a record of observed variation—not a guarantee that every future reply is safe. The same discipline transfers to the running app’s triage output; this lab does not modify that service or authorize refunds.
Next: Security & privacy — the request has clearer rules; now examine what happens when the message itself tries to override them.