
Prompt Engineering for Agents: A Structured, Testable, Version-Controlled Approach
Prompt engineering isn't vibes. Treat prompts like code — with schemas, versions, evals, diffs, and rollback. The full production workflow that scales past a single developer, with copy-paste patterns for structured output, few-shot design, LLM-as-judge, and prompt registries.
Ship prompts like software. Use a five-block skeleton (role · context · task · constraints · examples), enforce structured output with JSON schemas, store prompts in a registry with versioned rollout, and gate every change through three eval suites (unit, regression, adversarial). Teams that adopt this pattern cut prompt-related incidents by ~70% and stop arguing about wording in review.
- Five-block prompt skeleton removes 90% of the guessing in prompt authoring.
- Structured output (JSON schema / function calling) makes prompts testable and reviewable.
- A prompt registry with version + is_active flag enables instant rollback without redeploy.
- Three eval suites (unit, regression, adversarial) gate every PR — unit must be 100% green.
- LLM-as-judge scales evals, but only after you validate the judge against human labels.
Prompts are code — treat them that way
In 2023 prompt engineering was a party trick. In 2026 it's a shipping discipline: versioned in Git, validated with schemas, gated by evals, rolled out gradually, and rolled back in one click. The teams that treat prompts like code ship 3× more experiments per quarter and see 70% fewer prompt-related incidents. This article is the full workflow.
The five-block prompt skeleton
Every production prompt I ship has the same five blocks, in this order. The order matters — models pay more attention to the top and bottom of the context window, so instructions live at the top and constraints repeat at the bottom.
- Role — who the model is, whose interests it serves, what it optimizes for.
- Context — retrieved data, user profile, tool schemas, conversation history.
- Task — the concrete request, one imperative sentence.
- Constraints — output schema, forbidden actions, tone, refusal policy.
- Examples — 2–4 few-shots covering easy, hard, and refusal cases.
<role>
You are a support triage assistant for Acme SaaS. Optimize for correctness
and safe escalation over speed. You never invent policy.
</role>
<context>
{{retrieved_policy_chunks}}
Customer: {{customer.name}} · Plan: {{customer.plan}} · Region: {{customer.region}}
</context>
<task>
Classify the ticket and draft a one-paragraph reply.
</task>
<constraints>
- Output ONLY the JSON matching the tool schema. No prose outside it.
- If unsure, set escalate=true and copy the exact policy passage into reason.
- Never mention prices or refunds not present in <context>.
</constraints>
<examples>
{{fewshots}}
</examples>Blocks are separated by clear markers. This makes diffs readable and lets you swap one block without rewriting the rest — critical for A/B testing constraint tweaks or few-shot changes independently.
Structured output, always
Never parse free text if you can help it. Use JSON mode, function calling, or grammar-constrained decoding with a schema. The schema is not just runtime validation — it's the API contract, the eval spec, and the documentation, all in one file.
{
"name": "classify_ticket",
"parameters": {
"type": "object",
"required": ["intent", "urgency", "escalate"],
"properties": {
"intent": { "enum": ["refund", "bug", "how_to", "sales", "unknown"] },
"urgency": { "enum": ["low", "med", "high"] },
"escalate": { "type": "boolean" },
"reason": { "type": "string", "maxLength": 200 },
"citations": {
"type": "array",
"items": { "type": "string" },
"description": "IDs of chunks from <context> that support the classification."
}
}
}
}Validate every response against the schema before it leaves the agent. On validation failure, do one automatic repair pass (re-ask the model with the schema errors) and DLQ anything that still fails. This single pattern eliminates ~90% of downstream JSON parsing bugs.
Few-shot examples: pick them like a teacher, not a photocopier
The average team throws in 8 random examples and hopes. The good ones pick 3–4 examples that maximize diversity of decision boundary: one obviously easy, one hard-but-decidable, and one that must be refused. If two examples teach the same lesson, drop one.
- Cover every enum value in the output schema at least once across your few-shots.
- Include one near-miss where the naïve model would pick the wrong enum — this is where few-shots earn their keep.
- Always include one abstention example (unknown intent, missing data) so the model knows refusal is allowed.
- Rotate few-shots based on the retrieved context if you can — dynamic few-shots outperform static ones on domain-specific tasks.
applies_to filter. The prompt renderer picks the top-K matching examples at runtime.The prompt registry
A prompt registry is a tiny Postgres table plus a 50-line rendering helper. It replaces the entire class of 'we redeployed the app to change one word' incidents.
create table prompts (
id uuid primary key default gen_random_uuid(),
name text not null,
version int not null,
template text not null,
schema jsonb not null,
model text not null,
temperature real not null default 0,
is_active boolean not null default false,
created_at timestamptz not null default now(),
unique (name, version)
);
create unique index one_active_per_name
on prompts (name) where is_active;The one_active_per_name partial unique index enforces the golden rule: exactly one active version per prompt name. Rollback is a single SQL statement: flip is_active from the bad version to the previous good one. No redeploy, no rebuild.
The runtime helper fetches the active prompt by name, caches it for 60 seconds, renders the template with the current context, and logs the version used with every request. Reviewing an incident becomes trivial: filter logs by prompt_name = X and version = Y.
Version prompts like code
- Store the template as a Git-committed file — the registry mirrors from Git via CI, not the other way around. Git is the source of truth; the DB is the runtime.
- PRs must include the diff plus the eval delta. If unit or regression evals drop, the PR is blocked, not debated.
- Tag releases (
v1.4.0) and record which release served each production request in your logs. - Ship behind a feature flag: 5% traffic → 25% → 100% over 24–48h. Monitor grounding, refusal rate, tool-call error rate, and cost per request during each stage.
Evals: the missing discipline
The reason most prompt changes ship late and break is that nobody has an eval. Build three suites — small, cheap, and progressively slower — and gate merges on them.
| Suite | Size | Runs | Pass bar | Purpose |
|---|---|---|---|---|
| Unit | 20–50 examples | Every PR | 100% | Catch obvious regressions in seconds |
| Regression | 100–500 real traces | Nightly | > baseline – 2% | Catch subtle drift on realistic traffic |
| Adversarial | 50–200 attacks | Weekly | Growing over time | Jailbreaks, injections, off-topic questions |
Unit tests are hand-written assertions on structured output (intent = refund, urgency = high). Regression is a sample of real requests replayed with the new prompt, scored by an LLM judge. Adversarial is a growing library — every incident postmortem adds a case.
LLM-as-judge — the how, not just the what
A judge prompt is itself a prompt: it deserves the same five-block skeleton, structured output, and evals against human labels. Rules that work in practice:
- Score one dimension at a time (grounding, tone, refusal-correctness). Multi-dimensional single-shot scores are noisy.
- Use a small, fast model (Haiku, gpt-4o-mini) for scale, and reserve the strongest model for spot audits.
- Compare against a human-labeled gold set quarterly; if agreement drops below 85%, retire the judge and retrain.
- Always store the judge's raw output alongside the score — future you will need it to debug drift.
<role>
You are a strict evaluator scoring answer GROUNDING only. You do not care
about tone, style, or completeness. You care about one thing: does every
factual claim in the answer appear in the provided context?
</role>
<constraints>
Output JSON: { "grounded": bool, "unsupported_claims": string[] }.
</constraints>Cost and latency — designed, not discovered
Prompt cost is a design output. Bake three levers into every prompt so you can tune cost without rewriting logic:
- Model tier in metadata — swap Haiku ↔ Sonnet ↔ Opus without touching the template.
- Context budget — a hard token cap on
{{retrieved_chunks}}. Reranker + cap beats bigger context windows for both cost and grounding. - Cache key — the deterministic part of the prompt (role, task, constraints, few-shots) hashed as a prefix. Providers like Anthropic and OpenAI charge less for cache hits; use them.
The result: a 10× cost swing between the cheap and premium path for the same prompt, controlled by config not code.
Anti-patterns to unlearn
| Anti-pattern | Why it fails | Do this instead |
|---|---|---|
| Editing prompts in the UI, no Git | No history, no review, no rollback | Git → CI → registry |
| Free-text output, regex parsing | Breaks silently on model updates | JSON schema + validator + repair pass |
| Prompt megafiles (2k tokens) | Instruction conflicts, cost bloat | Split into named prompts, chain via router |
| Random few-shots | Teaches the wrong lesson | Diverse examples on the decision boundary |
| Eyeballing quality | Every 'small' change is a coin flip | Three eval suites, unit gates PRs |
| One temperature for everything | Classification drifts, generation goes stale | Temp 0 for extraction, 0.4–0.7 for drafting |
| No refusal path | Model invents when uncertain | Explicit abstain enum + reward in evals |
A two-week rollout plan for a team new to this
- Day 1–2: pick one prompt, rewrite in the five-block skeleton, add a JSON schema.
- Day 3–4: hand-write a 30-example unit eval set. Get it to 100% on the new prompt.
- Day 5: stand up the
promptsregistry table + a 50-line renderer. - Day 6–7: move the prompt into the registry, ship behind a 5% flag, monitor 48h.
- Week 2 · Day 1–2: sample 300 production traces into a regression set, score with an LLM judge you validated against 50 human labels.
- Week 2 · Day 3–4: build the adversarial set from your last 20 incident postmortems.
- Week 2 · Day 5: wire evals into CI — unit blocks merge, regression comments on PR, adversarial runs weekly.
- By day 10: every new prompt change ships through the same pipeline; rollback is one SQL statement.
Frequently asked
How long should a system prompt be?
As short as the task allows. Above ~1,000 tokens you get diminishing returns and more instruction conflicts. If you need more, move context into retrieved chunks and let RAG do the work — a well-designed retriever beats a bigger prompt every time.
Should I use function calling or JSON mode?
Function calling when your provider supports it — the schema is enforced at decode time, not post-hoc. JSON mode is a fine fallback for older models. Never rely on 'please respond in JSON' without either.
How do I A/B test a prompt change safely?
Ship the new version to the registry with <code>is_active = false</code>, route 5% of traffic via a feature flag, monitor the same three metrics (grounding, refusal rate, tool-error rate). If the deltas are within noise for 24h, ramp to 25%, then 100%. Rollback is one SQL statement.
Do I really need three eval suites?
For a demo, no. For anything that touches customers, yes. Unit catches obvious regressions instantly; regression catches subtle drift; adversarial catches the class of incident you already had once. Together they cost ~2 days to build and pay back on the first prevented outage.
Can I use the same prompt across multiple models?
Rarely well. Small models need more explicit constraints and shorter few-shots; large models need less scaffolding. Store the model as part of the prompt version and eval each combination separately.
What's the single highest-ROI change if I only have a day?
Move to structured output with a JSON schema and add a validator + one repair pass. It eliminates the whole class of 'the model said something weird and downstream broke' incidents overnight.
Building something similar?
I help teams ship production-grade AI agents, n8n workflows, and data platforms. Let's talk about what you're building.
Work with me
Islam Gamal
AI, Data & Automation Engineer. I design and ship production AI agents, n8n workflows, and cloud data platforms — with a focus on reliability, cost, and measurable business impact. Founder of Tashghil and Tek bil Arabi.
More from Islam Gamal
Building Production AI Agents with n8n: Architecture, Guardrails, Evals, and Cost Control
The full n8n agent playbook — reference architecture, memory design, tool routing, JSON-schema guardrails, retries, DLQs, evals, observability, and the small handful of decisions that keep the token bill sane at scale.
RAG That Actually Works: Chunking, Hybrid Search, Reranking, and Evals
The full production playbook: why naïve RAG demos die on the second question, how to chunk, index, retrieve, rerank, cite, evaluate, and cost-engineer a retrieval-augmented system that survives real users.
Evaluating LLMs and Classical ML: A Practical Metrics Guide
The metric you pick decides the model you ship. A deep field guide to choosing, computing, and defending metrics for classification, ranking, generation, RAG, and agents — with the traps that catch senior teams.
Browse every article by Islam Gamal on the author page.