Introduction: Production LLM applications need structured prompt management—not ad-hoc string concatenation scattered across code. Prompt templates provide reusable, parameterized prompts with consistent formatting. Versioning enables A/B testing, rollbacks, and tracking which prompts produced which results. This guide covers practical prompt template patterns: template engines and variable substitution, prompt registries, version control strategies, A/B testing frameworks, and migration patterns for evolving prompts without breaking production systems.

Template Engine
from dataclasses import dataclass, field
from typing import Any, Optional
from datetime import datetime
import re
@dataclass
class PromptTemplate:
"""A reusable prompt template."""
name: str
template: str
version: str = "1.0.0"
description: str = ""
variables: list[str] = field(default_factory=list)
metadata: dict = field(default_factory=dict)
created_at: datetime = field(default_factory=datetime.now)
def __post_init__(self):
"""Extract variables from template."""
if not self.variables:
self.variables = self._extract_variables()
def _extract_variables(self) -> list[str]:
"""Extract variable names from template."""
# Match {variable_name} patterns
pattern = r'\{(\w+)\}'
return list(set(re.findall(pattern, self.template)))
def render(self, **kwargs) -> str:
"""Render template with variables."""
# Check for missing variables
missing = set(self.variables) - set(kwargs.keys())
if missing:
raise ValueError(f"Missing variables: {missing}")
result = self.template
for var, value in kwargs.items():
result = result.replace(f"{{{var}}}", str(value))
return result
def validate(self, **kwargs) -> list[str]:
"""Validate variables without rendering."""
errors = []
# Check for missing required variables
missing = set(self.variables) - set(kwargs.keys())
if missing:
errors.append(f"Missing variables: {missing}")
# Check for extra variables
extra = set(kwargs.keys()) - set(self.variables)
if extra:
errors.append(f"Unknown variables: {extra}")
return errors
class Jinja2Template:
"""Template using Jinja2 for advanced features."""
def __init__(self, template_string: str):
from jinja2 import Template, Environment, StrictUndefined
self.env = Environment(undefined=StrictUndefined)
self.template = self.env.from_string(template_string)
def render(self, **kwargs) -> str:
"""Render with Jinja2."""
return self.template.render(**kwargs)
class AdvancedTemplate:
"""Template with conditionals and loops."""
def __init__(self, name: str, template: str):
self.name = name
self.raw_template = template
self.jinja = Jinja2Template(template)
def render(
self,
context: dict = None,
**kwargs
) -> str:
"""Render with full context."""
all_vars = {**(context or {}), **kwargs}
return self.jinja.render(**all_vars)
# Example templates
SUMMARIZATION_TEMPLATE = PromptTemplate(
name="summarization",
template="""Summarize the following {content_type} in {style} style.
Keep the summary under {max_words} words.
Content:
{content}
Summary:""",
version="1.0.0",
description="General-purpose summarization template"
)
EXTRACTION_TEMPLATE = PromptTemplate(
name="extraction",
template="""Extract the following information from the text:
{fields_to_extract}
Text:
{text}
Return as JSON with the field names as keys.""",
version="1.0.0",
description="Information extraction template"
)
QA_TEMPLATE = PromptTemplate(
name="qa_with_context",
template="""Answer the question based on the provided context.
If the answer is not in the context, say "I don't have enough information."
Context:
{context}
Question: {question}
Answer:""",
version="1.0.0",
description="Question answering with context"
)
Prompt Registry
from dataclasses import dataclass
from typing import Optional
from datetime import datetime
@dataclass
class RegisteredPrompt:
"""A prompt registered in the system."""
template: PromptTemplate
is_active: bool = True
usage_count: int = 0
last_used: Optional[datetime] = None
class PromptRegistry:
"""Central registry for prompt templates."""
def __init__(self):
self.prompts: dict[str, dict[str, RegisteredPrompt]] = {}
# prompts[name][version] = RegisteredPrompt
def register(
self,
template: PromptTemplate,
activate: bool = True
):
"""Register a prompt template."""
if template.name not in self.prompts:
self.prompts[template.name] = {}
# Deactivate other versions if activating this one
if activate:
for version, registered in self.prompts[template.name].items():
registered.is_active = False
self.prompts[template.name][template.version] = RegisteredPrompt(
template=template,
is_active=activate
)
def get(
self,
name: str,
version: str = None
) -> Optional[PromptTemplate]:
"""Get prompt template by name and optional version."""
if name not in self.prompts:
return None
versions = self.prompts[name]
if version:
registered = versions.get(version)
return registered.template if registered else None
# Get active version
for registered in versions.values():
if registered.is_active:
return registered.template
return None
def render(
self,
name: str,
version: str = None,
**kwargs
) -> str:
"""Render a registered prompt."""
template = self.get(name, version)
if not template:
raise ValueError(f"Prompt not found: {name}")
# Track usage
registered = self.prompts[name].get(
version or self._get_active_version(name)
)
if registered:
registered.usage_count += 1
registered.last_used = datetime.now()
return template.render(**kwargs)
def _get_active_version(self, name: str) -> Optional[str]:
"""Get active version for prompt."""
for version, registered in self.prompts.get(name, {}).items():
if registered.is_active:
return version
return None
def list_prompts(self) -> list[dict]:
"""List all registered prompts."""
result = []
for name, versions in self.prompts.items():
for version, registered in versions.items():
result.append({
"name": name,
"version": version,
"is_active": registered.is_active,
"usage_count": registered.usage_count,
"description": registered.template.description
})
return result
def activate_version(self, name: str, version: str):
"""Activate a specific version."""
if name not in self.prompts:
raise ValueError(f"Prompt not found: {name}")
if version not in self.prompts[name]:
raise ValueError(f"Version not found: {version}")
# Deactivate all versions
for v, registered in self.prompts[name].items():
registered.is_active = (v == version)
# File-based registry
class FilePromptRegistry:
"""Load prompts from files."""
def __init__(self, prompts_dir: str):
self.prompts_dir = prompts_dir
self.registry = PromptRegistry()
self._load_prompts()
def _load_prompts(self):
"""Load prompts from directory."""
import os
import yaml
for filename in os.listdir(self.prompts_dir):
if filename.endswith(('.yaml', '.yml')):
path = os.path.join(self.prompts_dir, filename)
with open(path) as f:
data = yaml.safe_load(f)
template = PromptTemplate(
name=data['name'],
template=data['template'],
version=data.get('version', '1.0.0'),
description=data.get('description', ''),
metadata=data.get('metadata', {})
)
self.registry.register(template)
def get(self, name: str, version: str = None) -> Optional[PromptTemplate]:
return self.registry.get(name, version)
def render(self, name: str, **kwargs) -> str:
return self.registry.render(name, **kwargs)
Version Control
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime
import hashlib
@dataclass
class PromptVersion:
"""A versioned prompt with history."""
template: PromptTemplate
parent_version: Optional[str] = None
change_description: str = ""
author: str = "system"
created_at: datetime = field(default_factory=datetime.now)
@property
def content_hash(self) -> str:
"""Hash of template content."""
return hashlib.md5(self.template.template.encode()).hexdigest()[:8]
class PromptVersionControl:
"""Version control for prompts."""
def __init__(self):
self.versions: dict[str, list[PromptVersion]] = {}
# versions[name] = [version1, version2, ...]
def commit(
self,
template: PromptTemplate,
change_description: str = "",
author: str = "system"
) -> str:
"""Commit a new version."""
if template.name not in self.versions:
self.versions[template.name] = []
history = self.versions[template.name]
# Determine parent version
parent = history[-1].template.version if history else None
# Auto-increment version if not specified
if not template.version or template.version == "1.0.0":
template.version = self._next_version(template.name)
version = PromptVersion(
template=template,
parent_version=parent,
change_description=change_description,
author=author
)
history.append(version)
return template.version
def _next_version(self, name: str) -> str:
"""Generate next version number."""
history = self.versions.get(name, [])
if not history:
return "1.0.0"
last_version = history[-1].template.version
parts = last_version.split('.')
# Increment patch version
parts[-1] = str(int(parts[-1]) + 1)
return '.'.join(parts)
def get_version(
self,
name: str,
version: str
) -> Optional[PromptTemplate]:
"""Get specific version."""
history = self.versions.get(name, [])
for v in history:
if v.template.version == version:
return v.template
return None
def get_latest(self, name: str) -> Optional[PromptTemplate]:
"""Get latest version."""
history = self.versions.get(name, [])
return history[-1].template if history else None
def get_history(self, name: str) -> list[dict]:
"""Get version history."""
history = self.versions.get(name, [])
return [
{
"version": v.template.version,
"parent": v.parent_version,
"description": v.change_description,
"author": v.author,
"created_at": v.created_at.isoformat(),
"hash": v.content_hash
}
for v in history
]
def diff(
self,
name: str,
version1: str,
version2: str
) -> dict:
"""Compare two versions."""
t1 = self.get_version(name, version1)
t2 = self.get_version(name, version2)
if not t1 or not t2:
raise ValueError("Version not found")
import difflib
diff = list(difflib.unified_diff(
t1.template.splitlines(),
t2.template.splitlines(),
fromfile=f"{name}@{version1}",
tofile=f"{name}@{version2}",
lineterm=""
))
return {
"version1": version1,
"version2": version2,
"diff": "\n".join(diff),
"variables_added": set(t2.variables) - set(t1.variables),
"variables_removed": set(t1.variables) - set(t2.variables)
}
def rollback(self, name: str, to_version: str) -> PromptTemplate:
"""Rollback to previous version."""
template = self.get_version(name, to_version)
if not template:
raise ValueError(f"Version not found: {to_version}")
# Create new version based on old one
new_template = PromptTemplate(
name=template.name,
template=template.template,
description=template.description,
metadata=template.metadata
)
self.commit(
new_template,
change_description=f"Rollback to {to_version}",
author="system"
)
return new_template
A/B Testing
from dataclasses import dataclass, field
from typing import Callable, Optional
from datetime import datetime
import random
@dataclass
class Variant:
"""A variant in an A/B test."""
name: str
template: PromptTemplate
weight: float = 1.0
# Metrics
impressions: int = 0
successes: int = 0
total_latency: float = 0.0
@property
def success_rate(self) -> float:
return self.successes / self.impressions if self.impressions > 0 else 0
@property
def avg_latency(self) -> float:
return self.total_latency / self.impressions if self.impressions > 0 else 0
@dataclass
class ABTest:
"""An A/B test configuration."""
name: str
variants: list[Variant]
start_time: datetime = field(default_factory=datetime.now)
end_time: Optional[datetime] = None
is_active: bool = True
def select_variant(self, user_id: str = None) -> Variant:
"""Select variant for user."""
if user_id:
# Consistent assignment based on user ID
hash_val = hash(f"{self.name}:{user_id}")
random.seed(hash_val)
# Weighted random selection
total_weight = sum(v.weight for v in self.variants)
r = random.random() * total_weight
cumulative = 0
for variant in self.variants:
cumulative += variant.weight
if r <= cumulative:
return variant
return self.variants[-1]
class ABTestManager:
"""Manage A/B tests for prompts."""
def __init__(self):
self.tests: dict[str, ABTest] = {}
def create_test(
self,
name: str,
control: PromptTemplate,
treatment: PromptTemplate,
treatment_weight: float = 0.5
) -> ABTest:
"""Create a new A/B test."""
test = ABTest(
name=name,
variants=[
Variant(name="control", template=control, weight=1 - treatment_weight),
Variant(name="treatment", template=treatment, weight=treatment_weight)
]
)
self.tests[name] = test
return test
def get_prompt(
self,
test_name: str,
user_id: str = None,
**kwargs
) -> tuple[str, str]:
"""Get prompt from A/B test, returns (rendered_prompt, variant_name)."""
test = self.tests.get(test_name)
if not test or not test.is_active:
raise ValueError(f"Test not found or inactive: {test_name}")
variant = test.select_variant(user_id)
variant.impressions += 1
rendered = variant.template.render(**kwargs)
return rendered, variant.name
def record_success(
self,
test_name: str,
variant_name: str,
latency: float = 0.0
):
"""Record successful outcome."""
test = self.tests.get(test_name)
if not test:
return
for variant in test.variants:
if variant.name == variant_name:
variant.successes += 1
variant.total_latency += latency
break
def get_results(self, test_name: str) -> dict:
"""Get test results."""
test = self.tests.get(test_name)
if not test:
return {}
return {
"name": test.name,
"is_active": test.is_active,
"variants": [
{
"name": v.name,
"impressions": v.impressions,
"successes": v.successes,
"success_rate": v.success_rate,
"avg_latency": v.avg_latency
}
for v in test.variants
]
}
def conclude_test(self, test_name: str) -> str:
"""Conclude test and return winner."""
test = self.tests.get(test_name)
if not test:
raise ValueError(f"Test not found: {test_name}")
test.is_active = False
test.end_time = datetime.now()
# Determine winner by success rate
winner = max(test.variants, key=lambda v: v.success_rate)
return winner.name
# Multi-armed bandit for adaptive testing
class BanditTestManager:
"""Adaptive testing using multi-armed bandit."""
def __init__(self, exploration_rate: float = 0.1):
self.tests: dict[str, ABTest] = {}
self.exploration_rate = exploration_rate
def select_variant(self, test: ABTest) -> Variant:
"""Select variant using epsilon-greedy."""
if random.random() < self.exploration_rate:
# Explore: random selection
return random.choice(test.variants)
# Exploit: select best performing
return max(test.variants, key=lambda v: v.success_rate)
Production Template Service
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
# Initialize components
registry = PromptRegistry()
version_control = PromptVersionControl()
ab_manager = ABTestManager()
# Register default templates
registry.register(SUMMARIZATION_TEMPLATE)
registry.register(EXTRACTION_TEMPLATE)
registry.register(QA_TEMPLATE)
class RenderRequest(BaseModel):
name: str
version: Optional[str] = None
variables: dict
class CreateTemplateRequest(BaseModel):
name: str
template: str
version: Optional[str] = None
description: str = ""
class ABTestRequest(BaseModel):
test_name: str
user_id: Optional[str] = None
variables: dict
@app.post("/v1/prompts/render")
async def render_prompt(request: RenderRequest):
"""Render a prompt template."""
try:
rendered = registry.render(
request.name,
request.version,
**request.variables
)
return {
"prompt": rendered,
"name": request.name,
"version": request.version
}
except ValueError as e:
raise HTTPException(400, str(e))
@app.post("/v1/prompts")
async def create_template(request: CreateTemplateRequest):
"""Create or update a prompt template."""
template = PromptTemplate(
name=request.name,
template=request.template,
version=request.version or "1.0.0",
description=request.description
)
# Commit to version control
version = version_control.commit(
template,
change_description="Created via API"
)
# Register in registry
registry.register(template)
return {
"name": template.name,
"version": version,
"variables": template.variables
}
@app.get("/v1/prompts")
async def list_prompts():
"""List all prompts."""
return {"prompts": registry.list_prompts()}
@app.get("/v1/prompts/{name}/history")
async def get_history(name: str):
"""Get version history for prompt."""
history = version_control.get_history(name)
if not history:
raise HTTPException(404, f"Prompt not found: {name}")
return {"name": name, "history": history}
@app.post("/v1/prompts/{name}/rollback/{version}")
async def rollback_prompt(name: str, version: str):
"""Rollback prompt to previous version."""
try:
template = version_control.rollback(name, version)
registry.register(template)
return {
"name": name,
"rolled_back_to": version,
"new_version": template.version
}
except ValueError as e:
raise HTTPException(400, str(e))
@app.post("/v1/ab/render")
async def render_ab_test(request: ABTestRequest):
"""Render prompt from A/B test."""
try:
prompt, variant = ab_manager.get_prompt(
request.test_name,
request.user_id,
**request.variables
)
return {
"prompt": prompt,
"variant": variant,
"test": request.test_name
}
except ValueError as e:
raise HTTPException(400, str(e))
@app.get("/v1/ab/{test_name}/results")
async def get_ab_results(test_name: str):
"""Get A/B test results."""
results = ab_manager.get_results(test_name)
if not results:
raise HTTPException(404, f"Test not found: {test_name}")
return results
@app.get("/health")
async def health():
return {"status": "healthy"}
References
- LangChain Prompts: https://python.langchain.com/docs/modules/model_io/prompts/
- Jinja2 Templates: https://jinja.palletsprojects.com/
- PromptLayer: https://promptlayer.com/
- Weights & Biases Prompts: https://docs.wandb.ai/guides/prompts
Conclusion
Prompt templates bring software engineering discipline to LLM applications. Use parameterized templates instead of string concatenation—they're easier to test, version, and maintain. Implement a prompt registry for centralized management and consistent access patterns. Version control your prompts like code—track changes, enable rollbacks, and maintain history. Use A/B testing to validate prompt improvements with real users before full deployment. Consider adaptive testing (multi-armed bandits) for continuous optimization. The goal is treating prompts as first-class artifacts with proper lifecycle management—because in LLM applications, prompts are your most important code.
Discover more from Code, Cloud & Context
Subscribe to get the latest posts sent to your email.
