Prompt Anatomy
12 min
By the end of this lesson you will stop writing prompts as one-off requests and start writing them as engineered interfaces: you will know the five structural components of a production prompt, why each exists, how the system/user role split actually changes model behavior, and you will have a reusable annotated template you can adapt to any feature you ship.
Roles are not decoration
Every chat-style API takes an array of messages, each with a role. The two you'll use constantly:
system— instructions from you, the developer, about how the model should behave for this entire interaction. Models are specifically trained to treat system content as higher-priority, standing policy.user— the input for this specific request: the end-user's message, the document to process, the data to transform.
The practical rule: behavior goes in system, data goes in user. When developers cram everything into a single user message — persona, rules, format spec, and the actual input, all interleaved — three things go wrong. The model weights your instructions no higher than the user's text. Instructions and data blur, so a user who types "ignore previous instructions" is standing in the same channel as your rules (the root of prompt injection — we harden against it in a later module). And you can't reuse anything, because the prompt and the input are welded together.
Separate them and you get a clean function signature: the system prompt is the function body; the user message is the argument.
Persona: operating principles beat titles
Almost every prompt guide says "give the model a persona," and almost everyone implements it as a job title: "You are an expert copywriter." This is close to useless. The model already writes like a generic expert; a title adds no new constraint.
What actually changes output is operating principles — the specific, checkable rules that expert follows:
```text
Weak: You are an expert code reviewer.
Strong: You are a code reviewer for a production TypeScript codebase.
Operating principles:
- Flag correctness bugs first, style issues last.
- Every finding must cite the specific line and explain the failure
scenario, not just the rule violated. - If you are not confident an issue is real, say "possible issue"
and state what you'd need to verify it. - Approve small, working improvements; do not demand rewrites.
```
The title told the model what to be. The principles tell it how to decide. When you're tempted to make the title fancier, add a principle instead.
Output contracts: specify the shape or inherit the chaos
If your code parses the model's output, the format specification is not a nicety — it's an API contract. Spell it out with the same precision you'd use in an OpenAPI spec:
```text
Return ONLY a JSON object with this exact shape, no markdown fences, no preamble:
{
"sentiment": "positive" | "neutral" | "negative",
"confidence": <number 0-1>,
"key_phrases": [<up to 3 strings, each under 6 words>]
}
If the input is empty or not analyzable, return:
{ "sentiment": "neutral", "confidence": 0, "key_phrases": [] }
```
Notice the last two lines: the contract includes the failure case. Un-specified edge cases are where models improvise, and improvisation is what breaks your JSON.parse at 2 a.m. (Many providers also offer structured-output/JSON modes that enforce a schema mechanically — use them when available; the written contract still governs semantics, like what "confidence" means.)
Calibration: teach the model where the lines are
The biggest quality gap between amateur and production prompts is calibration — instructions that define thresholds, edge behavior, and uncertainty handling:
- Thresholds: "Flag a transaction as suspicious only if at least two indicators are present. One indicator alone is 'worth review,' not 'suspicious.'"
- Uncertainty: "If the document does not contain the answer, say exactly:
Not found in the provided document.Never infer or guess a value." - Escalation: "If the user asks about refunds over $500, do not answer; respond that you're connecting them with a human."
Calibration is where you encode your product judgment. The model can imitate a tone in one example; it cannot guess your risk tolerance. Every hallucination complaint you've heard about a chatbot traces back to a missing uncertainty instruction.
Variables: the seam between prompt and code
A production prompt is a template with named slots your code fills at request time. Use unambiguous delimiters so injected content can't masquerade as instructions:
```text
Analyze the customer message below and classify it.
<customer_message>
{{message}}
</customer_message>
<account_tier>{{tier}}</account_tier>
```
Tagged delimiters like these do two jobs: the model reliably distinguishes data from instructions, and your code has an obvious, greppable seam. Keep templates in version control next to your code — a prompt change is a behavior change and deserves a diff, a review, and a rollback path exactly like any deploy.
Before and after: a weak prompt made strong
Here's a realistic "support triage" prompt, first as most people write it:
```text
You are a helpful support agent. Read this ticket and tell me how urgent
it is and what team should handle it. Be accurate. Ticket: {{ticket}}
```
Problems: no role separation, "helpful" and "accurate" are unenforceable vibes, no output contract, no calibration for ambiguous tickets, and the ticket text sits inline where it can collide with instructions.
Now the production version:
```text
SYSTEM:
You are the triage engine for a B2B SaaS support desk.
Operating principles:
- Classify on the customer's actual impact, not their tone. An angry
ticket about a typo is still low urgency. - "critical" is reserved for: data loss, security exposure, or complete
inability to use the product in production. - When impact is unclear, choose the lower urgency and set
needs_clarification to true — never inflate urgency to be safe.
Output contract — return ONLY this JSON, no other text:
{
"urgency": "critical" | "high" | "normal" | "low",
"team": "billing" | "auth" | "api" | "frontend" | "other",
"needs_clarification": <boolean>,
"summary": "<one sentence, under 20 words>"
}
USER:
<ticket>
{{ticket}}
</ticket>
```
Same feature, maybe two minutes more work — but this version is testable (fixed contract), tunable (edit one principle, re-run your eval set), injection-resistant (data is fenced), and honest under ambiguity (calibrated escape hatch). That is the entire difference between a demo and a product.
The reusable template
Here is the artifact for this lesson: a fully annotated prompt template. Paste it, replace the bracketed sections, delete the annotations, and you have a production-shaped prompt for almost any single-shot feature.
```text
============================================================
PROMPT TEMPLATE — [feature name] (version this file in git)
============================================================
--- SYSTEM MESSAGE ---
1. PERSONA — who the model is, scoped to THIS task.
You are [specific role] for [specific product/context].
2. OPERATING PRINCIPLES — 3-6 checkable decision rules.
Each one should be falsifiable: you could look at an output
and say "this violated principle 2."
Operating principles:
- [rule about what to prioritize]
- [rule with a concrete threshold or definition]
- [rule for handling uncertainty — what to do when unsure]
- [rule about what NOT to do]
3. OUTPUT CONTRACT — exact shape, including the failure case.
Output contract — return ONLY:
[exact format: JSON schema, markdown structure, or plain-text rules]
If [edge case: empty input / no answer / out of scope], return:
[exact fallback output]
4. CALIBRATION — thresholds, escalation, refusals.
Additional rules:
- [when to escalate or refuse, and the exact wording to use]
- [scale anchors: what makes something "high" vs "low"]
--- USER MESSAGE ---
5. VARIABLES — data only, fenced in named tags.
[One line of task framing, e.g. "Process the input below."]
<input>
{{input}}
</input>
<context>
{{optional_context}}
</context>
```
Exercise
Take one real prompt from your own work — or write the naive version of a feature you want to build — and rewrite it using the template: persona with at least three operating principles, an explicit output contract with a defined failure case, and fenced variables. Run both versions against the same five inputs at temperature 0. Success looks like: the structured version produces parseable, consistent output on all five inputs — including at least one deliberately empty or nonsense input — while the naive version breaks format or improvises on at least one.
You can now specify what the model should do with precision — next we tackle which model should do it, and what that choice costs you per user, per month, in actual arithmetic.