Versioning and Managing Prompts
The support bot started refusing refund requests on Tuesday. The prompt hadn't changed — according to the engineer who edited a string literal in chat_handler.py at 4pm Monday without telling anyone. Prompts scattered across code, Notion docs, and Slack messages aren't configuration — they're liabilities. Versioning prompts like code means every change is tracked, tested, attributable, and reversible.
Prompt registry structure
prompts/
support_chat/
system/
v1.0.0.yaml
v2.0.0.yaml
v2.1.0.yaml # current
classify_intent/
v1.0.0.yaml
CHANGELOG.md
eval_results/
support_chat_v2.1.0.json
Each prompt file:
# prompts/support_chat/system/v2.1.0.yaml
id: support_chat/system
version: "2.1.0"
model_compatibility: ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4-20250514"]
author: "[email protected]"
created: "2024-12-27"
parent_version: "2.0.0"
change_summary: "Added citation requirement for policy questions"
eval_pass_rate: 0.94
template: |
You are a support agent for {{company_name}}.
Answer using ONLY the provided context.
Cite sources as [doc_id].
If context is insufficient, say so — do not guess.
Context: {{retrieved_context}}
variables:
- company_name
- retrieved_context
Loading at runtime
class PromptRegistry:
def __init__(self, path: str):
self.prompts = self._load_all(path)
def get(self, prompt_id: str, version: str | None = None) -> Prompt:
versions = self.prompts[prompt_id]
ver = version or versions.latest
return versions[ver]
def render(self, prompt_id: str, variables: dict, version: str | None = None) -> str:
prompt = self.get(prompt_id, version)
return Template(prompt.template).render(**variables)
Log prompt_id and version on every LLM call — non-negotiable for debugging.
Deployment workflow
Edit prompt YAML → Run eval suite → PR review → Merge → Deploy → Monitor
CI gate:
- name: Prompt eval regression
run: |
python -m evals.run --prompt-version ${{ changed_version }} --threshold 0.92
PR shows eval diff: "v2.1.0 passes 94/100 (+2 vs v2.0.0)."
Feature flag integration
Roll out prompt versions gradually:
def resolve_prompt_version(prompt_id: str, tenant_id: str) -> str:
variant = flags.get(f"prompt_{prompt_id}", tenant_id)
return VARIANT_MAP.get(variant, DEFAULT_VERSIONS[prompt_id])
Start at 5% traffic. Ramp if guardrail metrics hold.
Rollback
When production metrics degrade:
# Instant rollback — no deploy needed if registry supports runtime reload
registry.set_active("support_chat/system", "2.0.0") # revert from 2.1.0
Rollback should take seconds, not a full deploy cycle. Keep N-1 and N-2 versions hot-swappable.
Template variables
Strict variable validation:
def render(self, prompt_id: str, variables: dict) -> str:
prompt = self.get(prompt_id)
missing = set(prompt.variables) - set(variables.keys())
if missing:
raise MissingPromptVariable(missing)
return Template(prompt.template).render(**variables)
Silent empty variables ({{company_name}} → "") cause bizarre model behavior.
Prompt management tools
When to adopt LangSmith/Humanloop/PromptLayer:
- Non-engineers editing prompts
- Frequent A/B experiments
- Need visual diff and annotation
- Prompts update without app redeploy
Integration pattern: git remains canonical; tool syncs on merge to main.
Changelog discipline
## support_chat/system
### v2.1.0 (2024-12-27)
- Added citation requirement for policy questions
- Eval: 94/100 (+2 vs v2.0.0)
- Author: [email protected]
### v2.0.0 (2024-12-15)
- BREAKING: switched from free-form to structured response
- Eval: 92/100 (-3 on creative queries, +8 on factual)
Every version has a human-readable reason, not just a diff.
Prompt registry architecture
Central registry with environment separation:
prompts/
├── support_chat/
│ ├── system/v2.1.0.jinja2
│ ├── system/v2.0.0.jinja2
│ └── user/v1.0.0.jinja2
├── code_review/
│ └── system/v1.3.0.jinja2
└── registry.yaml
# registry.yaml
prompts:
support_chat/system:
production: v2.1.0
staging: v2.2.0-rc1
canary: v2.2.0-rc1 # 5% traffic
code_review/system:
production: v1.3.0
Runtime loads version from registry — not hardcoded in application code. Deploy prompt change = update registry pointer, not redeploy app.
A/B testing prompts in production
Route traffic by user hash to compare prompt versions:
def get_prompt_version(prompt_id: str, user_id: str) -> str:
registry = load_registry()
config = registry[prompt_id]
if hash(user_id) % 100 < config.get("canary_pct", 0):
return config["canary"]
return config["production"]
def render_prompt(prompt_id: str, user_id: str, **vars) -> str:
version = get_prompt_version(prompt_id, user_id)
template = load_template(f"{prompt_id}/{version}.jinja2")
return template.render(**vars)
Track metrics per version: task completion rate, user thumbs-up, token usage, latency. Promote canary to production when metrics beat baseline for 48+ hours.
Eval-driven prompt iteration workflow
1. Identify failure mode from production logs (e.g., "model ignores citation requirement")
2. Draft prompt change addressing failure
3. Run eval suite against new version
4. If eval score improves → deploy to canary (5% traffic)
5. Monitor production metrics for 48 hours
6. Promote to production or rollback
Never deploy prompt changes without eval — even small wording changes can shift behavior significantly. Keep eval suite aligned with production failure modes, not just academic benchmarks.
Failure modes
- Prompt hardcoded in application — requires app redeploy for every prompt change
- No version tracking — can't reproduce or rollback production behavior
- Eval suite stale — prompt passes eval but fails on new failure modes
- Canary without metrics — prompt change deployed without measurement
- Template variables undocumented — runtime error on missing variable
Production checklist
- Prompts in version-controlled registry, not application code
- Environment separation (production/staging/canary) in registry
- Eval suite run before every prompt version promotion
- Canary deployment with traffic percentage control
- Changelog with human-readable reason per version
- Template variables documented with types and examples
Common production mistakes
Teams get prompt versioning management wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
LLM features around prompt versioning management break in production when prompts assume deterministic output, context windows are sized for dev datasets, or token costs are never budgeted per user session. Always log prompt hash, model version, and latency—not raw prompts with PII.
Resources
- LangSmith prompt hub
- Humanloop prompt management
- PromptLayer versioning
- Jinja2 template engine
- Semantic versioning specification
Frequently asked questions
Should prompts live in code or a prompt management tool?
Git for prompts that change with code deploys (same PR, same review). Prompt management tools (LangSmith, Humanloop, PromptLayer) when non-engineers iterate on prompts, you A/B test frequently, or prompts update independently of app deploys. Many teams use both: git as source of truth, tool for runtime serving and experiments.
How do I version prompts semantically?
Use semver-ish tags: MAJOR for behavior-breaking changes (new output format), MINOR for instruction additions that change responses, PATCH for typo/clarity fixes that shouldn't affect eval scores. Tag every deploy with version string included in usage logs for attribution.
What metadata should each prompt version carry?
Version ID, author, date, linked eval results, compatible model list, change description, and parent version. When debugging a regression, you need to answer 'what changed between v2.1 and v2.2 and who approved it' in under a minute.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →