For data-science workloads, prompts are model artifacts: they have versions, evals, regressions, and rollbacks.
Tier: Core · Workflow area: LLM Engineering Practice
This playbook sets up the minimum discipline that separates "we tweaked the prompt and it seems better" from an auditable iteration loop — a prompt registry in git, a fixed eval set, and a gate that blocks prompt changes which regress metrics.
Externalize every prompt. No prompts inline in application code. One file per prompt with metadata:
# prompts/ticket_extraction/v3.yaml
id: ticket_extraction
version: 3
model: claude-opus-4-8
schema: TicketV2 # schemas are versioned alongside
changelog: "Added competitor-mention field; tightened summary length"
template: |
Extract the ticket fields. Rules: ...
<ticket>{ticket_text}</ticket>
The (prompt-version, schema-version, model) triple is the unit you deploy, log, and roll back. Every pipeline output row records it — that is what makes last quarter's numbers reproducible.
Build the eval set deliberately. Stratify: ~60% representative cases, ~25% known-hard cases (past failures graduate into the set), ~15% adversarial/edge (empty input, wrong language, prompt-injection attempts). Freeze it; additions are append-only with dates so scores stay comparable.
Score automatically. Three scorer types, in order of preference: exact/field match against expected values (extraction, classification); programmatic checks (parses, passes validators, length bounds); LLM-as-judge with a rubric only where the first two cannot express quality (summaries, tone). Judge calls use a cheap tier and a pinned judge model version.
Wire the gate. A script (locally or CI) that runs the candidate prompt vs the incumbent over the eval set and reports per-slice deltas:
ticket_extraction v3 vs v2 (n=180)
field_accuracy 0.94 → 0.96 (+0.02) hard-slice: 0.81 → 0.86
parse_rate 1.00 → 1.00
adversarial_pass 0.93 → 0.87 (-0.06) ← REGRESSION, merge blocked
cost/1k $0.41 → 0.38
The rule: no merge on any slice regression without an explicit, written waiver. Aggregate-only reporting is how regressions ship.
Iterate with hypotheses, not vibes. Each iteration: read 10 failing cases → write one hypothesis ("model confuses severity when multiple issues are listed") → make one targeted change → rerun the gate. One change per version; batched edits make attribution impossible.
Close the loop from production. Sampled production failures (pb-16's QA queue) get minimized into eval cases and appended to the hard slice. The eval set should grow a few cases per week; a static eval set goes stale in a quarter.
The eval set is the asset; prompts are disposable. Teams that invest a day in 150 good eval cases iterate faster forever after — teams that skip it re-litigate "is it better?" on every change.
promptfoo eval --fail-on-regression).parametrize over eval cases hitting the API with cached responses is a legitimate v1 — the gate matters more than the framework.