Back to Blog
Prompt EngineeringLLMsProduction AIBest Practices

Prompt Engineering Patterns That Actually Work in Production

Beyond basic prompting: battle-tested patterns for reliable LLM outputs in production systems. Structured output, chain-of-thought, self-consistency, guardrails, and more.

IQ

TrainIQ Team

AI Engineering

February 28, 202620 min read

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

python
# 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

python
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)   # 32

Production 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

python
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.

python
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.

python
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.

python
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:

python
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.

python
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.

python
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.

python
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:

  1. 1Structured schemas for every input and output
  2. 2Validation layers that catch bad outputs before they reach users
  3. 3Retry logic with model escalation
  4. 4Monitoring and logging for every invocation
  5. 5A/B testing for continuous improvement
  6. 6Guardrails for safety and compliance
  7. 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.

**See the full curriculum →**

The difference between "I can write prompts" and "I can build production AI systems" is exactly what TrainIQ is designed to bridge.

Ready to go deeper? Master AI with TrainIQ.

8 comprehensive modules covering everything from foundations to production deployment. Hands-on labs, real-world projects, and a community of practitioners.