The Gap Between Demo Prompts and Production Prompts
Everyone can get an LLM to do something impressive in a playground. The real challenge is making it work reliably 10,000 times a day, across edge cases, with consistent quality, and without hallucinating critical information.
After working with dozens of production AI systems, we've identified the patterns that separate fragile demos from robust production systems. This isn't a list of "cool tricks" — it's a practical engineering guide for building prompts that actually hold up under real-world conditions.
Pattern 1: Structured Output Enforcement
The single most impactful thing you can do for production prompts is enforce structured output. Free-form text is unparseable, untestable, and unpredictable.
The Problem
# This works in demos, breaks in production
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Extract the person's name and age from this text: 'John is 32 years old'"}]
)
# Returns: "The person's name is John and they are 32 years old."
# Good luck parsing that reliably across 1000 variations.The Solution: Schema-Enforced Output
from openai import OpenAI
from pydantic import BaseModel, Field
import json
class PersonInfo(BaseModel):
name: str = Field(description="Full name of the person")
age: int = Field(description="Age in years")
confidence: float = Field(description="Confidence score 0.0-1.0")
client = OpenAI()
def extract_person(text: str) -> PersonInfo:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": """Extract person information from text.
Return a JSON object matching this exact schema:
{
"name": "string - full name",
"age": "integer - age in years",
"confidence": "float - your confidence 0.0 to 1.0"
}"""
},
{"role": "user", "content": text}
],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
return PersonInfo(**data) # Pydantic validates the shape
# Now this ALWAYS returns structured, validated data
result = extract_person("John is 32 years old")
print(result.name) # "John"
print(result.age) # 32Production tip: Use OpenAI's native response_format parameter or Anthropic's tool-use/JSON mode. Don't rely on the model "remembering" to output JSON — enforce it at the API level.
Advanced: Nested Structured Output
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
class Severity(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class CodeIssue(BaseModel):
line_number: int
issue_type: str
severity: Severity
description: str
suggested_fix: Optional[str] = None
class CodeReviewResult(BaseModel):
file_name: str
language: str
issues: List[CodeIssue]
overall_quality: int = Field(ge=1, le=10, description="Overall code quality 1-10")
summary: str
def review_code(code: str, filename: str) -> CodeReviewResult:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": f"""You are a senior code reviewer. Analyze the code and return
a structured review as JSON matching this schema:
{{
"file_name": "{filename}",
"language": "detected language",
"issues": [
{{
"line_number": int,
"issue_type": "bug|style|performance|security",
"severity": "low|medium|high|critical",
"description": "what's wrong",
"suggested_fix": "how to fix it (optional)"
}}
],
"overall_quality": 1-10,
"summary": "brief overall assessment"
}}"""
},
{"role": "user", "content": f"Review this code:\n\n{code}"}
],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
return CodeReviewResult(**data)Pattern 2: Chain-of-Thought with Extraction
Chain-of-thought (CoT) prompting is well-known, but most people use it wrong in production. The key insight: let the model think freely, then extract the structured answer from the reasoning.
class ReasonedAnswer(BaseModel):
reasoning: str = Field(description="Step-by-step reasoning process")
answer: str = Field(description="Final concise answer")
confidence: float = Field(description="Confidence 0.0-1.0")
def answer_with_reasoning(question: str, context: str) -> ReasonedAnswer:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": """You are a precise analyst. When given a question and context:
1. THINK through the problem step-by-step in the "reasoning" field
2. Provide a concise ANSWER based on your reasoning
3. Rate your CONFIDENCE (0.0 = guessing, 1.0 = certain)
Return JSON:
{
"reasoning": "your step-by-step analysis",
"answer": "your final answer",
"confidence": 0.0-1.0
}"""
},
{
"role": "user",
"content": f"Context:\n{context}\n\nQuestion: {question}"
}
],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
return ReasonedAnswer(**data)Why this matters: In production, you can log the reasoning for debugging, route low-confidence answers to human review, and improve your prompts by analyzing where the model's logic breaks down.
Pattern 3: Self-Consistency Voting
For high-stakes decisions, don't trust a single LLM call. Run the same prompt multiple times and take the majority answer.
from collections import Counter
from typing import List
class SelfConsistencyClassifier:
"""Run multiple LLM calls and take the majority vote for higher accuracy."""
def __init__(self, num_samples: int = 5, temperature: float = 0.7):
self.num_samples = num_samples
self.temperature = temperature
self.client = OpenAI()
def classify(self, text: str, categories: List[str]) -> dict:
categories_str = ", ".join(categories)
responses = []
for _ in range(self.num_samples):
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": f"""Classify the following text into exactly one category.
Categories: {categories_str}
Return JSON: {{"category": "chosen_category", "reason": "brief justification"}}"""
},
{"role": "user", "content": text}
],
response_format={"type": "json_object"},
temperature=self.temperature
)
data = json.loads(response.choices[0].message.content)
responses.append(data["category"])
# Majority vote
vote_counts = Counter(responses)
winner = vote_counts.most_common(1)[0]
return {
"category": winner[0],
"confidence": winner[1] / self.num_samples,
"vote_distribution": dict(vote_counts),
"num_samples": self.num_samples
}
# Usage
classifier = SelfConsistencyClassifier(num_samples=5)
result = classifier.classify(
"The server is returning 500 errors after the latest deploy",
["bug", "feature_request", "question", "documentation"]
)
# {"category": "bug", "confidence": 1.0, "vote_distribution": {"bug": 5}}When to use this: Classification tasks, content moderation, medical/legal/financial decisions — anywhere a wrong answer has real consequences.
Pattern 4: Prompt Chaining (Decomposition)
Complex tasks fail when crammed into a single prompt. Break them into a pipeline of focused, testable steps.
class ContentPipeline:
"""Multi-step content analysis pipeline with focused prompts."""
def __init__(self):
self.client = OpenAI()
def analyze(self, content: str) -> dict:
# Step 1: Extract key entities
entities = self._extract_entities(content)
# Step 2: Determine sentiment and tone
sentiment = self._analyze_sentiment(content)
# Step 3: Generate summary based on entities + sentiment
summary = self._generate_summary(content, entities, sentiment)
# Step 4: Determine action items
actions = self._extract_actions(content, entities)
return {
"entities": entities,
"sentiment": sentiment,
"summary": summary,
"actions": actions
}
def _extract_entities(self, content: str) -> dict:
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": """Extract named entities from the text.
Return JSON: {"people": [], "organizations": [], "technologies": [], "dates": []}"""
}, {"role": "user", "content": content}],
response_format={"type": "json_object"},
temperature=0
)
return json.loads(response.choices[0].message.content)
def _analyze_sentiment(self, content: str) -> dict:
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": """Analyze sentiment and tone.
Return JSON: {"sentiment": "positive|negative|neutral|mixed", "tone": "formal|casual|urgent|technical", "score": -1.0 to 1.0}"""
}, {"role": "user", "content": content}],
response_format={"type": "json_object"},
temperature=0
)
return json.loads(response.choices[0].message.content)
def _generate_summary(self, content: str, entities: dict, sentiment: dict) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"""Summarize this content in 2-3 sentences.
Key entities: {json.dumps(entities)}
Sentiment: {sentiment['sentiment']} ({sentiment['tone']})
Focus on the most important information and any action-relevant details."""
}, {"role": "user", "content": content}],
temperature=0.3
)
return response.choices[0].message.content
def _extract_actions(self, content: str, entities: dict) -> list:
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": f"""Extract actionable items from this content.
Known entities: {json.dumps(entities)}
Return JSON: {{"actions": [{{"description": "what to do", "owner": "who should do it", "priority": "high|medium|low"}}]}}"""
}, {"role": "user", "content": content}],
response_format={"type": "json_object"},
temperature=0
)
return json.loads(response.choices[0].message.content).get("actions", [])Key advantage: Each step is independently testable, debuggable, and improvable. When the summary is bad, you know exactly which step to fix.
Pattern 5: Guardrails and Validation
Production systems need guardrails. Here's a pattern for input/output validation:
from typing import Callable, List, Optional
class GuardedLLM:
"""LLM wrapper with input/output guardrails."""
def __init__(self):
self.client = OpenAI()
self.input_guards: List[Callable] = []
self.output_guards: List[Callable] = []
def add_input_guard(self, guard: Callable[[str], Optional[str]]):
"""Add input validation. Return error string or None if OK."""
self.input_guards.append(guard)
def add_output_guard(self, guard: Callable[[str], Optional[str]]):
"""Add output validation. Return error string or None if OK."""
self.output_guards.append(guard)
def generate(self, system_prompt: str, user_input: str, **kwargs) -> dict:
# Check input guards
for guard in self.input_guards:
error = guard(user_input)
if error:
return {"status": "blocked", "reason": error, "response": None}
# Generate response
response = self.client.chat.completions.create(
model=kwargs.get("model", "gpt-4o"),
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input}
],
temperature=kwargs.get("temperature", 0.3)
)
output = response.choices[0].message.content
# Check output guards
for guard in self.output_guards:
error = guard(output)
if error:
return {"status": "filtered", "reason": error, "response": None}
return {"status": "ok", "response": output}
# Define specific guardrails
def no_pii_input(text: str) -> Optional[str]:
"""Block inputs containing obvious PII patterns."""
import re
patterns = {
"SSN": r"\b\d{3}-\d{2}-\d{4}\b",
"credit_card": r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b",
"email": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
}
for pii_type, pattern in patterns.items():
if re.search(pattern, text):
return f"Input contains potential {pii_type}. Please remove before processing."
return None
def no_harmful_output(text: str) -> Optional[str]:
"""Block outputs containing specific harmful patterns."""
blocked_patterns = ["how to hack", "exploit this vulnerability", "bypass security"]
text_lower = text.lower()
for pattern in blocked_patterns:
if pattern in text_lower:
return f"Response contained blocked content pattern."
return None
def max_length_output(max_chars: int = 5000):
def guard(text: str) -> Optional[str]:
if len(text) > max_chars:
return f"Response exceeded maximum length ({len(text)} > {max_chars})"
return None
return guard
# Wire it all together
llm = GuardedLLM()
llm.add_input_guard(no_pii_input)
llm.add_output_guard(no_harmful_output)
llm.add_output_guard(max_length_output(5000))
result = llm.generate(
system_prompt="You are a helpful assistant.",
user_input="Explain how RAG systems work."
)Pattern 6: Few-Shot with Dynamic Example Selection
Static few-shot examples are fine for demos. In production, dynamically select the most relevant examples for each query.
class DynamicFewShotPrompt:
"""Select the most relevant few-shot examples per query using embeddings."""
def __init__(self, embedding_service: object):
self.examples = []
self.example_embeddings = []
self.embedding_service = embedding_service
def add_example(self, input_text: str, output_text: str, category: str = ""):
embedding = self.embedding_service.embed_query(input_text)
self.examples.append({
"input": input_text,
"output": output_text,
"category": category
})
self.example_embeddings.append(embedding)
def get_prompt(self, query: str, num_examples: int = 3) -> str:
# Embed the query
query_embedding = self.embedding_service.embed_query(query)
# Find most similar examples
similarities = [
np.dot(query_embedding, ex_emb) / (
np.linalg.norm(query_embedding) * np.linalg.norm(ex_emb)
)
for ex_emb in self.example_embeddings
]
top_indices = np.argsort(similarities)[-num_examples:][::-1]
selected = [self.examples[i] for i in top_indices]
# Build the prompt
examples_text = ""
for ex in selected:
examples_text += f"Input: {ex['input']}\nOutput: {ex['output']}\n\n"
return f"""Here are some examples:
{examples_text}Now handle this new input:
Input: {query}
Output:"""Pattern 7: Retry with Escalation
When a prompt fails validation, don't just retry — escalate to a better model or a different strategy.
class EscalatingRetry:
"""Retry failed LLM calls with escalating model quality."""
def __init__(self):
self.client = OpenAI()
self.escalation_chain = [
{"model": "gpt-4o-mini", "temperature": 0.1, "max_retries": 2},
{"model": "gpt-4o", "temperature": 0.0, "max_retries": 2},
{"model": "gpt-4o", "temperature": 0.0, "max_retries": 1,
"system_suffix": "\n\nIMPORTANT: Previous attempts failed validation. Be extremely precise and follow the schema exactly."},
]
def generate(self, system_prompt: str, user_input: str, validator: Callable) -> dict:
for level in self.escalation_chain:
for attempt in range(level["max_retries"]):
full_system = system_prompt + level.get("system_suffix", "")
response = self.client.chat.completions.create(
model=level["model"],
messages=[
{"role": "system", "content": full_system},
{"role": "user", "content": user_input}
],
temperature=level["temperature"]
)
output = response.choices[0].message.content
is_valid, error = validator(output)
if is_valid:
return {
"status": "success",
"response": output,
"model_used": level["model"],
"escalation_level": self.escalation_chain.index(level),
"attempt": attempt + 1
}
return {"status": "failed", "response": None, "error": "All escalation levels exhausted"}Pattern 8: Prompt Versioning and A/B Testing
Treat prompts like code. Version them, test them, measure them.
import hashlib
import random
from datetime import datetime
class PromptRegistry:
"""Version and A/B test production prompts."""
def __init__(self):
self.prompts = {} # name -> {versions: [...], active_test: {...}}
self.results = [] # log of all prompt invocations
def register(self, name: str, version: str, template: str, weight: float = 1.0):
if name not in self.prompts:
self.prompts[name] = {"versions": [], "active_test": None}
self.prompts[name]["versions"].append({
"version": version,
"template": template,
"weight": weight,
"hash": hashlib.md5(template.encode()).hexdigest()[:8],
"created_at": datetime.now().isoformat()
})
def get_prompt(self, name: str, variables: dict = None) -> dict:
"""Get a prompt, respecting A/B test weights."""
config = self.prompts[name]
versions = config["versions"]
# Weighted random selection for A/B testing
weights = [v["weight"] for v in versions]
total = sum(weights)
r = random.uniform(0, total)
cumulative = 0
selected = versions[-1]
for v in versions:
cumulative += v["weight"]
if r <= cumulative:
selected = v
break
template = selected["template"]
if variables:
for key, value in variables.items():
template = template.replace(f"{{{{{key}}}}}", str(value))
return {
"prompt": template,
"version": selected["version"],
"hash": selected["hash"]
}
def log_result(self, name: str, version: str, input_text: str,
output: str, quality_score: float):
self.results.append({
"prompt_name": name,
"version": version,
"input": input_text[:200],
"output": output[:200],
"quality_score": quality_score,
"timestamp": datetime.now().isoformat()
})
def get_version_stats(self, name: str) -> dict:
"""Compare performance across prompt versions."""
version_scores = {}
for result in self.results:
if result["prompt_name"] == name:
v = result["version"]
if v not in version_scores:
version_scores[v] = []
version_scores[v].append(result["quality_score"])
return {
version: {
"count": len(scores),
"mean_quality": sum(scores) / len(scores),
"min_quality": min(scores),
"max_quality": max(scores)
}
for version, scores in version_scores.items()
}The Meta-Pattern: Think in Systems, Not Prompts
The biggest shift from amateur to professional prompt engineering is this: stop thinking about individual prompts and start thinking about systems.
A production prompt engineering system includes:
- 1Structured schemas for every input and output
- 2Validation layers that catch bad outputs before they reach users
- 3Retry logic with model escalation
- 4Monitoring and logging for every invocation
- 5A/B testing for continuous improvement
- 6Guardrails for safety and compliance
- 7Evaluation pipelines for measuring quality at scale
Go Deeper with TrainIQ
These patterns are a starting point. In the TrainIQ AI training program, Module 2 covers LLMs and prompt engineering in depth — including advanced techniques like constitutional AI prompting, multi-turn conversation management, and building prompt evaluation frameworks from scratch.
But prompt engineering doesn't exist in isolation. You'll also learn how to combine these patterns with RAG systems (Module 5), AI agents (Module 6), and production deployment strategies (Module 7) to build complete, reliable AI applications.
The difference between "I can write prompts" and "I can build production AI systems" is exactly what TrainIQ is designed to bridge.