Latest Articles

Prompt Templates and Versioning: Building Maintainable LLM Applications

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.

Prompt Templates
Template System: Registry, Rendering, Versioning

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

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.

About the Author

I am a Cloud Architect and Developer passionate about solving complex problems with modern technology. My blog explores the intersection of Cloud Architecture, Artificial Intelligence, and Software Engineering. I share tutorials, deep dives, and insights into building scalable, intelligent systems.

Areas of Expertise

Cloud Architecture (Azure, AWS)
Artificial Intelligence & LLMs
DevOps & Kubernetes
Backend Dev (C#, .NET, Python, Node.js)
© 2025 Code, Cloud & Context | Built by Nithin Mohan TK | Powered by Passion