ESC

Type to search the knowledge base.

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.

beginner6 min read
  • 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:

  1. Cover the boring case and the refusal case, not only the hero case.
  2. Keep input/output labels identical across examples.
  3. Do not paste PII. Few-shot from production without redaction is a leak.
  4. 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:

  1. Change parts on a branch.
  2. Run the golden set (exact match on enums, rubric on prose).
  3. Record id on the trace so a bad answer maps to a version.
  4. 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 guides