A model never sees your program's variables — it only ever sees one big string. So the first job of prompt engineering is assembly: turn your instructions, your data, and your examples into that single string, deterministically, on every call. Hand-stitching that string with + works for one prompt and collapses at twenty.
A prompt template is a string with named holes you fill from variables, and a template renderer is the tiny function that fills them. This lesson builds both, then adds few-shot examples — sample input/output pairs placed in the prompt so the model copies the shape you want. You will leave able to render a template, splice in examples, and, just as important, judge when those examples are actually worth their tokens.
flowchart LR T["template with placeholders"] --> R["template renderer"] V["variables from your code"] --> R R --> P["final prompt string"] style R fill:#8b5cf6,color:#fff style P fill:#166534,color:#fff
How examples teach: format, not facts
A model is a next-token predictor trained on millions of texts. Drop two or three worked examples into its prompt and it imitates their pattern — the same field names, the same ordering, the same style of answer. This is in-context learning: the examples shape behaviour for this one call without changing any weights. The leverage is biggest for output format. If you need JSON with keys label and confidence, three examples pin that down far more reliably than a paragraph describing it.
Notice what the examples do not do. They do not teach the model new facts — it already knew those, or it did not. They teach the shape of the answer. Keep that distinction sharp: few-shot is a formatting and steering tool, and reaching for it when the model merely lacks knowledge will burn tokens for no gain.
function renderTemplate(template, vars) {
let out = template;
for (const key in vars) {
out = out.split("{{" + key + "}}").join(vars[key]);
}
return out;
}
const t = "Classify: {{input}}\nLabel: {{label}}";
console.log(renderTemplate(t, { input: "blue sky", label: "weather" }));
Assembly order is part of the prompt
The renderer fills placeholders, but the order of the pieces is its own decision. A reliable assembly is: system instructions first (they set the task and the rules), then few-shot examples (they prime the format), then the target input last (so the model simply continues the pattern). Putting the target last is not cosmetic — it lets the model treat the examples as a lead-in and emit its answer as the natural next line.
Order also fights a real failure called lost in the middle: models attend best to the start and the end of a long prompt, and weakest to the centre. Keeping the instructions at the front and the target at the back means the two things the model most needs are in the high-attention zones, while examples sit in the middle where they only have to nudge, not carry, the answer.
flowchart TD I["system instructions"] --> P["assembled prompt"] E["few-shot examples"] --> P G["target input"] --> P P --> M["model call"] style P fill:#b45309,color:#fff style M fill:#166534,color:#fff
function assemblePrompt(parts) {
return parts.filter(p => p && p.length > 0).join("\n\n");
}
const prompt = assemblePrompt([
"You classify support tickets.",
"Input: refund request\nOutput: billing",
"Input: login fails\nOutput: tech",
"Input: how do I upgrade?\nOutput: "
]);
console.log(prompt);
When examples help, and when they waste tokens
Examples earn their cost when the task has a rigid or unusual shape the model will not guess on its own: a fixed label set, a strict JSON schema, a step-by-step reasoning format, or a delicate classification boundary. They are wasted on tasks the model already does well from a clear instruction — there you are paying input tokens on every single call for no measurable gain.
That cost is not abstract. Every example is billed as input tokens on every call, and the whole assembled prompt must fit inside the context budget. Stack six fat examples and you have six fewer retrieved-document chunks you can afford to pass in. Treat examples like any scarce resource: keep the smallest set that reliably pins the format, and drop one whenever the output quality does not change.
The discipline: zero-shot first
The strongest habit in prompt work is to start zero-shot — instructions only, no examples — and look hard at the output. If the model already produces the right shape, ship it and keep the tokens. Only when it drifts do you add the smallest example set that fixes the drift, testing after each addition. This loop — cheapest thing that works — keeps prompts small and forces every example to earn its place, instead of accumulating examples defensively until the prompt is bloated, slow, and expensive for no reason.
flowchart LR B["fixed context budget"] --> A["assembled payload"] E["each example costs tokens"] --> A R["retrieved documents"] --> A A --> O["overflow is trimmed"] style A fill:#b91c1c,color:#fff style B fill:#3776ab,color:#fff
function formatExamples(examples) {
return examples.map(e => "Input: " + e.input + "\nOutput: " + e.output).join("\n");
}
const block = formatExamples([
{ input: "2+2", output: "4" },
{ input: "3+3", output: "6" }
]) + "\nInput: 5+5\nOutput:";
console.log(block);
What does this print? The renderer replaces every placeholder with its value.
function render(t, v) {
let out = t;
for (const k in v) out = out.split("{{" + k + "}}").join(v[k]);
return out;
}
console.log(render("{{greeting}}, {{who}}!", { greeting: "Hi", who: "Ada" }));
Each {{key}} is swapped for its value.
"Hi" fills greeting, "Ada" fills who.
Write renderTemplate(template, vars) that returns template with every occurrence of each {{key}} replaced by vars[key]. Replace all occurrences, not just the first.
function renderTemplate(template, vars) {
// replace every {{key}} with vars[key]
}
Loop over each key in vars.
Use out.split("{{" + key + "}}").join(value) to replace every occurrence at once.
function renderTemplate(template, vars) {
let out = template;
for (const key in vars) {
out = out.split("{{" + key + "}}").join(vars[key]);
}
return out;
}
This fill is meant to replace every {{key}}, but it uses String.replace, which swaps only the first match — so a template with two {{x}} comes out half-filled. Fix it so all occurrences are replaced.
function fill(template, vars) {
let out = template;
for (const k in vars) {
out = out.replace("{{" + k + "}}", vars[k]);
}
return out;
}
replace() changes only the first match.
split(delimiter).join(value) replaces every match.
function fill(template, vars) {
let out = template;
for (const k in vars) {
out = out.split("{{" + k + "}}").join(vars[k]);
}
return out;
}
Few-shot examples help MOST for which kind of task?
Examples teach the shape of the answer. They pay off when that shape is rigid or unusual; for a task the model already handles, they add input-token cost on every call for no gain.
Write buildFewShot(examples, targetInput) that returns a prompt string: each example as an Input:/Output: pair, a blank line between pairs, then the target as a final Input: with an empty Output: ready for the model to complete.
function buildFewShot(examples, targetInput) {
// format each example, separate pairs with a blank line, append the target
}
Build the string by looping over examples, appending each pair plus a blank line.
After the loop, append the target as a final Input: with an empty Output:.
function buildFewShot(examples, targetInput) {
let out = "";
for (const ex of examples) {
out += "Input: " + ex.input + "\nOutput: " + ex.output + "\n\n";
}
out += "Input: " + targetInput + "\nOutput:";
return out;
}
Recap
- A prompt template is a string with
{{placeholders}}; a renderer fills them from variables so assembly is deterministic, not hand-stitched with+. - In-context learning means few-shot examples shape the format of the answer for one call without changing weights — they teach shape, not facts.
- Assemble in a fixed order: instructions, then examples, then the target last, so the model continues the pattern and the ends (high attention) carry the load.
- Examples are worth their tokens for rigid or unusual output shapes, and wasted on tasks the model already does well — they cost input tokens on every call.
- Replace every occurrence of a placeholder:
String.replaceswaps only the first, so usesplit(...).join(...)orreplaceAll. - Watch label leakage: never put the target answer inside an example's input.
Next: packing a whole context window — instructions, history, retrieved chunks, and these examples — under a fixed token budget.