Prompt Engineering Fundamentals
A prompt is a spec: role, task, constraints, examples, output shape. How to write one you can version, test, and stop rewriting by feel.
- prompts
- prompt-engineering
If changing a comma “fixes” production, you do not have a prompt. You have an untested function with string arguments. Prompt engineering is writing that function so you can version it, evaluate it, and stop rewriting by feel.
The model is a sampler. Adjectives are not types. “Be careful” does not validate an order id. A spec does: role, task, constraints, examples (only when they pay rent), output shape. Everything else is folklore, including “act as a senior engineer with 20 years of experience.”
Treat the text like code that happens to execute on someone else’s GPU.
Key takeaways
- A prompt is a spec with inputs and a defined output, not a pep talk.
- Split role, task, constraints, examples, and shape so you can change one without rewriting all.
- Few-shot examples cost tokens and can overfit. Add them when an eval says they help.
- Version the prompt and run a golden set. A comma that “fixes” one ticket is not a release.
Prompt engineering is a function spec
Write it like you would write a handler: named inputs, a return type, failure behavior. The system prompt is the durable part. The user turn is the call.
type PromptParts = {
role: string;
task: string;
constraints: string[];
examples?: { input: string; output: string }[];
outputShape: string;
};
function renderPrompt(p: PromptParts, input: string): {
system: string;
user: string;
} {
const constraints = p.constraints.map((c, i) => `${i + 1}. ${c}`).join('\n');
const examples = (p.examples ?? [])
.map((ex, i) => `Example ${i + 1}\nInput: ${ex.input}\nOutput: ${ex.output}`)
.join('\n\n');
return {
system: [p.role, p.outputShape].filter(Boolean).join('\n'),
user: [p.task, constraints && `Constraints:\n${constraints}`, examples, `Input:\n${input}`]
.filter(Boolean)
.join('\n\n'),
};
}
const TRIAGE: PromptParts = {
role: 'You classify support tickets for a docs site. You do not invent product policy.',
task: 'Assign one topic and one priority. If the input is not a support ticket, refuse.',
constraints: [
'topic must be one of: billing, auth, docs, other',
'priority must be one of: low, normal, high',
'If required fields are missing, set needsMoreInfo to true and leave topic as other',
],
outputShape: 'Return JSON only, matching the schema the API enforces.',
};
// Failure mode: "You are a world-class support lead. Think deeply."
// That line cannot be evaluated. TRIAGE.constraints can.
The rendered strings are what you store in git (or a prompt registry) with an id. The model never sees your TypeScript names. It sees tokens. You still want the source to be structured so a PR diff is readable.
The five parts that earn their tokens
Role. One or two sentences. Who this route is, who it is not. Not a resume. Anthropic’s prompting guide starts here for a reason: a role orients the model. A paragraph of personality does not.
Task. The verb and the object: classify, extract, answer from sources, propose a tool. One task per prompt. Pipelines beat mega-prompts.
Constraints. Numbered, testable. “Never cite a URL that is not in <sources>” is a constraint. “Be helpful” is not.
Examples (few-shot). Use them when the task has a format the model misses, or a boundary case (empty retrieval, mixed language). Each example is billed on every call. If three examples do not move your golden set, delete them.
Output shape. JSON schema, markdown contract, or “three bullets, no preface.” Prefer a schema and structured outputs over begging in prose.
| Part | Earns its tokens when | Cut it when |
|---|---|---|
| Role | The route has a clear job | It restates the company mission |
| Task | You can name the verb | You stacked four verbs |
| Constraints | You can write an assert | They contradict each other |
| Examples | Eval score moves | They duplicate the schema |
| Output shape | A parser or UI depends on it | You already enforce a schema |
OpenAI and Anthropic both say: be explicit, show the format, put examples in a consistent pattern. Neither says to pile on prestige titles.
Few-shot is not free
An example is a hidden test case you now ship to production on every request. It can help the model lock a format. It can also teach the wrong prior: if every example is a high-priority billing ticket, “other” / “low” will starve.
Rules:
- Cover the boring case and the refusal case, not only the hero case.
- Keep input/output labels identical across examples.
- Do not paste PII. Few-shot from production without redaction is a leak.
- After you add structured outputs, re-evaluate. The schema often replaces the examples.
Chain-of-thought in the prompt (“think step by step”) can help some models and waste tokens on others. Reasoning models may already think internally. Follow the model card. Do not cargo-cult a 2012 prompting trick into every route.
Version it like code
A prompt without an id is latest in production.
type PromptRelease = {
id: 'triage.v4';
hash: string;
parts: PromptParts;
};
function assertTriage(obj: {
topic: string;
priority: string;
needsMoreInfo: boolean;
}) {
const topics = new Set(['billing', 'auth', 'docs', 'other']);
const priorities = new Set(['low', 'normal', 'high']);
if (!topics.has(obj.topic) || !priorities.has(obj.priority)) {
throw new Error('triage invariant');
}
}
Ship process:
- Change
partson a branch. - Run the golden set (exact match on enums, rubric on prose).
- Record
idon the trace so a bad answer maps to a version. - Canary. Do not hot-edit the string in a dashboard that nobody diffs.
This is the same habit as testing UI: cheap asserts on the shape, fewer slow “does it look good” passes. A comma change that is not in git is not a fix. It is drift.
If one example in production “needs a tweak,” add it to the eval set first. If you only patch the prompt, you taught the model that ticket and untaught two others. You will not know.
What rewriting by feel hides
Contradictions. “Be concise” next to “list every edge case.” The model picks one. You call it flaky.
Policy in adjectives. “Never reveal PII” in the prompt, then you log the raw user turn. The control is elsewhere.
Mega-prompts. Ten product lines in one system string. Split routes. A billing classifier should not also write release notes.
Invisible inputs. A server wrapper that prepends the date, the plan tier, and a retrieved blob. If it is not in the versioned parts, it will not be in the PR.
No refusal path. Spec what happens when sources are empty. Otherwise the model will improvise, and you will call it a hallucination.
Prompt engineering is the first lever. It is not the last. Tools, retrieval, and evals exist because a better paragraph will not check inventory.
Out loud, in an interview
“Prompt engineering is writing a spec: role, task, constraints, optional few-shot, and an output shape I can parse. I version that spec and run a golden set, so a comma is a diff, not a ritual. I keep the system prompt short and put the task in the user turn. I do not use ‘act as a senior’ theater. If the output must be typed, I enforce a schema in the API and assert business rules in code.”
Further reading (primary sources)
Related on this site
- Chat messages and system prompts
- Structured outputs and JSON Schema
- Golden datasets
- Testing pyramid for frontend
Related guides
- Chat Messages and System PromptsSystem, user, assistant, and tool roles are the real API. How message lists grow, how to keep a system prompt short, and where policy belongs.
- Context EngineeringContext engineering is selecting, compressing, and ordering what the model sees. Prompts are one input. History, tools, and retrieval are the rest.
- Agent Failure ModesAgents loop, lie about tool results, skip confirmation, and get stuck apologizing. Catalog the failures and the circuit breakers.
- Agent HandoffsA handoff passes the conversation to another agent with a slimmer prompt and different tools. What to copy, what to drop, and how to avoid ping-pong.
- Agent MemoryAgents forget when the context window ends. Working memory, session summaries, and long-term stores are three different systems. Do not mix them.