Skip to content
AI Full-Stack Wiki

Prompt and Context Engineering for LLM Apps (with TypeScript)

Write prompts that work in production: clear instructions, examples, XML-tagged context, and request assembly that is ordered for caching and safe with untrusted data.

Difficulty: Intermediate17 min readUpdated
On this page· Anatomy of a request

Why this matters

The model knows nothing about your product, your user, or your rules except what is in the request. Prompt engineering is writing instructions the model follows reliably; context engineering is choosing everything else that goes into the request: documents, history, tools, and their order. Together they decide answer quality more than any other choice you make, and unlike model choice, they are entirely under your control.

Anatomy of a request#

Every request is assembled from parts with different jobs and different lifetimes. Some never change (who the assistant is, how it formats answers), some change per user, and some change every request.

Assembling a request from stable and changing partsInstructions, output format, and examples form a stable system prompt; tool definitions are also stable; retrieved documents, history, and the new question change per request. Stable parts go first so they can be cached.Role & rulesOutput formatExamplesTool definitionsRetrieved docsHistoryNew messageRequeststable → changing

Hover or tap a component to see what it does.

Prompt and context engineering: assemble each request from a stable, cacheable prefix (tools, role, format, examples) followed by per-request context (retrieved documents, history, the new message).
Show text description

Every part of the request has a job. Ordering by stability, most stable first, makes requests cheaper with prompt caching and easier to reason about.

  • Role & rules: Who the model is acting as, what it must and must not do, how to handle uncertainty.
  • Output format: Length, structure, tone. For machine-read output, use structured outputs instead of prose instructions.
  • Examples: Two to five input/output pairs that show the behavior you want, including an edge case.
  • Tool definitions: Names, descriptions, and input schemas. They render before the system prompt.
  • Retrieved docs: Facts for this request, from search or your database.
  • History: Earlier turns, possibly trimmed or summarized.
  • New message: The user's latest input, placed last.
  • Request: The rendered order is tools, then system, then messages. Caching matches from the start, so stable content must come first.

Two rules follow from the picture. Order by stability: the request renders as tools, then system, then messages, and prompt caching matches from the beginning, so stable content first means cheaper requests. Keep instructions and data apart: instructions belong in the system prompt; documents, user profiles, and the question go in messages, clearly delimited.

  • System prompt
  • Tool definitions
  • Conversation history
  • Retrieved documents
  • New user message
  • Output budget (max_tokens)
Anatomy of the context window: every request carries the system prompt, tool definitions, conversation history, retrieved documents, and the new message, plus room reserved for the output. Typical token counts for three kinds of requests.
Show text description

Three representative requests, with token counts per part of the context. The counts are typical for well-built features, not limits; current models accept far larger contexts, but every input token costs money and latency.

Tokens per context part
RequestSystem promptTool definitionsConversation historyRetrieved documentsNew user messageOutput budget (max_tokens)Total
Simple chat, turn 380001,40001202,0004,320
Support agent with tools2,4003,2006,50001804,09616,376
RAG answer with citations1,80002,20012,000904,09620,186

Writing instructions that work#

Current models follow instructions closely, so the biggest gains come from being clear and specific, not from tricks.

DoInstead of
State the audience and goal: "You help on-call engineers triage alerts quickly""You are a helpful assistant"
Explain why: "Keep answers under 80 words; they appear in a mobile notification""Be concise"
Say what to do: "Answer only from the provided documents; if they don't cover it, say so and suggest contacting support""Don't make things up"
Show the format with an exampleDescribing the format in abstract words
Use calm, plain languageALL CAPS, "CRITICAL", "you MUST"
Give the model a way out: "If the request is ambiguous, ask one clarifying question"Forcing an answer to every input

Examples beat descriptions#

Two to five examples of input and ideal output teach format and judgment faster than paragraphs of rules. Make them diverse (include an edge case and a "can't answer" case), keep them short, and put them in the stable system prompt so they are cached.

Delimit data with tags#

When you insert documents, user data, or other content into a prompt, wrap each piece in descriptive XML-style tags. The model then knows where instructions end and data begins, and you can refer to sections by name ("answer using the documents in <documents>").

A production prompt, versioned as a file#

Keep prompts in files in your repository, so changes are reviewed, diffed, and tested like code:

prompts/support.md
You are the support assistant for Acme Billing, helping small-business owners with invoices, payments, and subscriptions. Most users are not technical and are often in a hurry.
 
How to answer:
- Answer only from the documents provided in <documents>. If they do not cover the question, say you are not sure and suggest contacting support at support@acme.example.
- Lead with the answer in one or two sentences, then give steps if needed.
- Use the customer's plan from <customer> to tailor the answer; never reveal internal plan names or prices not in the documents.
- If the question is ambiguous, ask one short clarifying question instead of guessing.
- Cite the documents you used as [doc-id] after the sentence that relies on them.
 
Example:
<example>
Question: How do I download last month's invoice?
Answer: Go to Billing → Invoices and select "Download PDF" next to last month's invoice [doc-12]. Invoices are available for 7 years [doc-12].
</example>
 
<example>
Question: Can you change my company's tax ID on an already-paid invoice?
Answer: I'm not sure that's possible from the documents I have. Please contact support@acme.example with the invoice number, and the team can check.
</example>

Assembling the request#

This builder keeps the system prompt stable (and cached), and puts per-request context in the user message, ordered from documents to question:

lib/build-request.ts
import { readFileSync } from "node:fs";
import type Anthropic from "@anthropic-ai/sdk";
 
// Loaded once per process. Never interpolate per-user data into it: it is the cached prefix.
const SYSTEM_PROMPT = readFileSync(new URL("../prompts/support.md", import.meta.url), "utf8");
 
export interface Doc {
  id: string;
  title: string;
  text: string;
}
 
export function buildSupportRequest(input: {
  docs: Doc[];
  history: Anthropic.MessageParam[];
  question: string;
  plan: "free" | "pro" | "business";
}): Anthropic.MessageCreateParamsNonStreaming {
  const documents = input.docs
    .map((d) => `<document id="${d.id}" title="${d.title}">\n${d.text}\n</document>`)
    .join("\n");
 
  return {
    model: "claude-opus-5-5",
    max_tokens: 4096,
    output_config: { effort: "medium" },
    system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
    messages: [
      ...input.history,
      {
        role: "user",
        // Long context first, the question last.
        content: `<documents>\n${documents}\n</documents>\n\n<customer plan="${input.plan}" />\n\n<question>\n${input.question}\n</question>`,
      },
    ],
  };
}
ask.ts
import Anthropic from "@anthropic-ai/sdk";
import { buildSupportRequest } from "./lib/build-request.ts";
 
const client = new Anthropic();
 
const request = buildSupportRequest({
  plan: "pro",
  history: [],
  question: "I was charged twice this month. What should I do?",
  docs: [
    { id: "doc-7", title: "Duplicate charges", text: "If you see two charges in one month, the second is usually a pending authorization that disappears within 5 business days. If it remains after 5 days, request a refund from Billing → Payments." },
    { id: "doc-12", title: "Invoices", text: "Invoices are under Billing → Invoices and can be downloaded as PDF for 7 years." },
  ],
});
 
const message = await client.messages.create(request);
console.log(message.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(""));
console.log("cache:", message.usage.cache_creation_input_tokens, "written,", message.usage.cache_read_input_tokens, "read");

Run it twice within five minutes and the second run reads the system prompt from cache, provided the prompt is long enough to cache (the minimum is model-dependent; a few hundred tokens on current models).

Changing instructions mid-conversation#

Sometimes instructions change during a conversation: the user switches to a "brief answers" mode, or an account upgrade changes what you may offer. Editing the top-level system prompt would invalidate the cached prefix for the whole conversation. On current models you can instead append a system message after the latest user turn:

mode-switch.ts
import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
const history: Anthropic.MessageParam[] = [
  { role: "user", content: "Explain what a webhook is." },
  { role: "assistant", content: "A webhook is an HTTP request one system sends to another when an event happens…" },
];
 
const message = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 2048,
  system: "You are a patient programming tutor.",
  messages: [
    ...history,
    { role: "user", content: "And how do I verify one?" },
    // Operator instruction for this point in the conversation; the cached prefix above stays intact.
    { role: "system", content: "The user enabled brief mode: answer in at most three sentences." },
  ],
});
console.log(message.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(""));

A system message must follow a user turn and be either last or followed by an assistant turn. It is also the right channel for instructions from you, as opposed to text inside user messages, which users (or documents) can forge.

Iterate like an engineer#

Prompt work is empirical. The loop that works:

  1. Collect 10 to 30 real inputs, including hard and "should refuse" cases.
  2. Run the prompt on all of them and read every output.
  3. Change one thing, run again, and compare side by side.
  4. Keep the change only if it helps more cases than it hurts.
  5. Turn the set into an automated eval once it stabilizes (see the evals article).

Common mistakes#

  • Interpolating per-user data into the system prompt. It breaks caching and mixes instructions with data. Put user data in messages.
  • Mixing instructions into retrieved content. Delimit documents with tags and tell the model to treat them as reference material, not instructions.
  • Over-constraining. A prompt full of absolute rules makes the model rigid and odd at the edges. Explain the goal and let it use judgment.
  • Prompt changes without a test set. Fixing one example often breaks three others. Measure across your set every time.
  • Prompts stored in a database without history. You cannot review, diff, or roll back a change. Keep them in Git.

Try it

Improve a weak prompt with evidence

  1. Write the weakest reasonable version of the support prompt: "You are a helpful support bot. Answer the question."
  2. Run it and the full prompts/support.md version on six questions: two answerable, two not covered by the documents, one ambiguous, one asking for an internal price.
  3. Score each answer pass/fail against the rules in the full prompt. Note which rule fixed which failure.

Check your understanding

Why should per-user data stay out of the system prompt?
What is the best way to teach a model an output format?
An account upgrade mid-conversation changes what the assistant may offer. How do you tell the model without losing the cache?

Key takeaways

  • Assemble requests from stable to changing: tools, system prompt, then history, documents, and the question last.
  • Write clear, specific instructions with reasons, examples, and a way out for uncertainty; avoid shouting.
  • Delimit data with XML-style tags and keep instructions out of data.
  • Keep prompts in Git, change one thing at a time, and measure on a set of real inputs.

Next steps#

Finished this article? Track it to see your progress on the roadmap.