Prompt Engineering in Production

Beyond clever phrasing: how production AI teams design, measure, and systematically improve prompts at scale.

Prompt EvaluationContext EngineeringCI/CD for Prompts

Introduction

Lesson 3 covered how to write effective prompts: roles, few-shot examples, chain-of-thought. This lesson is about the next level, using prompts in production systems where reliability, repeatability, and measurability are non-negotiable. The craft has evolved from a standalone job into a software engineering discipline: you need evaluation datasets, metrics, regression prevention, and automated pipelines to confidently change a prompt and ship it to users.

1. When It's Required

When casual prompting breaks down

2. The Evolution

From clever tricks to context engineering

3. Modern Techniques

System messages, XML structure, few-shot

4. Prompt Evaluation

Datasets, model-as-judge, metrics

5. CI/CD for Prompts

Regression prevention and platforms

6. Reference Project

Full evaluation workflow in Python

1. When Prompt Engineering is Required

For everyday use: summarizing a document, brainstorming ideas or answering questions, you rarely need a rigorous engineering process. You iterate in the chat window, it works well enough, and you move on. Prompt engineering as a discipline kicks in when the stakes change.

Casual Use - No Engineering Needed
  • One-off questions and research
  • Personal productivity tasks
  • Exploratory conversations
  • Drafts you will manually review and edit
Production AI - Engineering Required
  • Same task repeated with different inputs (inference pipeline)
  • Outputs consumed programmatically without human review
  • Multi-step AI agents with tool calls
  • Multiple engineers modifying prompts over time
The Litmus Test

Ask yourself: "If I change one sentence in this prompt, can I tell whether it made things better or worse?" If the answer is "I'd have to eyeball a few outputs and guess," you need a formal evaluation process. AI systems are probabilistic, changing "please" to "kindly" might improve some edge cases while silently degrading others. Without measurement, you are flying blind.

2. From Clever Tricks to System Design

Early language models responded dramatically differently to small phrasings. Practitioners discovered "magic" patterns: "Act as a senior engineer...", "Think step by step...", "You will be penalized for..." These were real workarounds for limited model capabilities, not engineering principles.

Modern frontier models understand natural language well. The incantations matter far less. What matters now is what you put into context, not just how you phrase it. This shift from word-level tricks to context-level architecture is the defining change in the discipline.

Context Engineering

Context engineering means deliberately designing everything that enters the model's context window. For a production system, this includes:

System Message

Role definition, behavioral constraints, output format requirements, tone guidelines, and what the model should refuse to do.

Few-Shot Examples

Concrete input/output pairs that demonstrate the expected quality, format, and style. One good example beats a paragraph of description.

Tool Schemas

For AI agents: the tool definitions shape the model's reasoning. Well-named tools with clear descriptions lead to better tool selection and chaining.

Domain Context

Relevant documents, past decisions, code snippets, and repo constraints injected per request. This is what retrieval-augmented generation (RAG) automates at scale.

A Note on Advanced Reasoning Models

Extended thinking models (like Claude with extended thinking enabled) can sometimes over-reason on simple tasks. For straightforward inference pipelines, a standard model with a well-structured prompt often outperforms a reasoning model with a vague one. Match the tool to the task and measure to confirm.

3. Modern Prompt Engineering Techniques

System Message Design

A strong system message does three things: sets the role, defines constraints, and specifies the output format. Vague roles produce vague outputs. Explicit constraints prevent common failure modes before they happen.

import anthropic

client = anthropic.Anthropic()

system_message = """You are a meal planning assistant for competitive athletes.

CONSTRAINTS:
- Always include exact macro breakdowns (protein, fat, carbs in grams)
- Always specify meal timing (e.g., 7:00 AM, 12:00 PM)
- Never suggest foods that violate the athlete's dietary restrictions
- Always list portion sizes in grams

OUTPUT FORMAT:
- Calorie total for the day
- Macro breakdown (protein/fat/carbs)
- Numbered meal list with timing, foods, portions, and per-meal macros"""

response = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=1024,
    system=system_message,
    messages=[
        {"role": "user", "content": "Create a meal plan for a 75kg runner training for a marathon."}
    ]
)

Structured Input with XML Tags

Claude is trained to recognize XML-style tags as semantic separators. Using tags like <athlete_information> or <sample_input> helps the model distinguish between data, instructions, and examples - reducing ambiguity and improving consistency.

Before and After: The Meal Plan Example

The reference project includes two versions of the same meal plan prompt. The v1 version is intentionally naive. The v2 version adds three key improvements: structured XML input, numbered guidelines, and a few-shot example. The average evaluation score jumps from 4.3/10 to 8.7/10.

Prompt v1 - Naive (avg score: 4.3/10)
from evaluator.prompt_evaluator import add_user_message, chat


def run_prompt(prompt_inputs: dict) -> str:
    prompt = f"""
    Write a one-day meal plan for an athlete.

    Height: {prompt_inputs["height"]}cm
    Weight: {prompt_inputs["weight"]}kg
    Goal: {prompt_inputs["goal"]}
    Dietary restrictions: {prompt_inputs["restrictions"]}
    """
    messages = []
    add_user_message(messages, prompt)
    return chat(messages)
Prompt v2 - Improved (avg score: 8.7/10)
from evaluator.prompt_evaluator import add_user_message, chat


def run_prompt(prompt_inputs: dict) -> str:
    prompt = f"""
    Generate a one-day meal plan for an athlete that meets their dietary restrictions.

    <athlete_information>
    - Height: {prompt_inputs["height"]}
    - Weight: {prompt_inputs["weight"]}
    - Goal: {prompt_inputs["goal"]}
    - Dietary restrictions: {prompt_inputs["restrictions"]}
    </athlete_information>

    Guidelines:
    1. Include accurate daily calorie amount
    2. Show protein, fat, and carb amounts
    3. Specify when to eat each meal
    4. Use only foods that fit restrictions
    5. List all portion sizes in grams
    6. Keep budget-friendly if mentioned

    Here is an example with a sample input and an ideal output:
    <sample_input>
    height: 170, weight: 70
    goal: Maintain fitness and improve cholesterol levels
    restrictions: High cholesterol
    </sample_input>
    <ideal_output>
    Calorie Target: approximately 2500 calories
    Macros: Protein (140g), Fat (70g), Carbs (340g)

    Breakfast (7:00 AM): Oatmeal (80g) with berries (100g) and walnuts (15g). Skim milk (240g).
      Protein: 15g, Fat: 15g, Carbs: 60g
    Mid-Morning Snack (10:00 AM): Apple (150g) with almond butter (30g).
      Protein: 7g, Fat: 18g, Carbs: 25g
    Lunch (1:00 PM): Grilled chicken breast (120g) salad with mixed greens (150g) and vinaigrette (30g).
      Protein: 40g, Fat: 15g, Carbs: 70g
    Dinner (7:00 PM): Baked salmon (140g) with steamed broccoli (200g) and quinoa (75g dry).
      Protein: 40g, Fat: 20g, Carbs: 80g
    </ideal_output>
    """
    messages = []
    add_user_message(messages, prompt)
    return chat(messages)
XML Structure

Separates data from instructions, reducing parser ambiguity

Numbered Guidelines

Explicit requirements prevent common omissions

Few-Shot Example

One ideal output anchors format and quality expectations

4. Prompt Evaluation: The Core Discipline

Evaluation is what separates prompt engineering from prompt guessing. Without it, every change is a bet. With it, you can prove a change improved quality, quantify the gain, and catch regressions before they reach users. This is the most important skill in the discipline today.

The Evaluation Pipeline

Prompt Improvement Cycle

Write PromptGenerate DatasetRun EvaluationAnalyze ReportImprove Promptnext iteration

Figure 1: Every prompt change moves through this loop, measure first, then improve

Building an Evaluation Dataset

A good dataset covers the distribution of real inputs your system will see, including edge cases that are rare but important. For the meal plan example, this means athletes with unusual goals, strict dietary restrictions, and conflicting constraints. You can generate this dataset automatically using Claude itself: ask it to brainstorm diverse scenarios, then convert each scenario into a structured test case with inputs and success criteria.

Happy Path

Standard, representative inputs

Edge Cases

Unusual, boundary, or tricky inputs

Diversity

Broad coverage of real-world scenarios

Criteria

Measurable success conditions per case

Model-as-Judge

Manual evaluation does not scale. Instead, use a second Claude call to score each output, this is called model-as-judge. The judge receives the task description, the inputs, the output to evaluate, and the success criteria. It returns a structured score (1-10) with reasoning.

The critical detail: run the judge at temperature=0.0 to ensure deterministic, reproducible grading. The same output should always receive the same score. Using Claude Haiku for judging keeps costs low even across large datasets.

def grade_output(self, test_case: dict, output: str) -> dict:
    """Score a prompt output 1-10 using Claude as the judge."""

    eval_prompt = f"""
    Evaluate the following AI output with extreme rigor.

    Task: {test_case['task_description']}
    Inputs: {test_case['prompt_inputs']}
    Output to evaluate: {output}
    Success criteria: {test_case['solution_criteria']}

    Scoring guide:
    - Score 1-3: fails one or more mandatory requirements
    - Score 4-6: meets requirements but has significant gaps
    - Score 7-8: meets all requirements with minor issues
    - Score 9-10: fully satisfies all criteria

    Return JSON with keys: strengths, weaknesses, reasoning, score (1-10)
    """

    messages = []
    add_user_message(messages, eval_prompt)
    # temperature=0.0 ensures deterministic, reproducible grading
    result = chat(messages, temperature=0.0)
    return json.loads(result)

Metrics to Track

Quality Metrics
  • Average score - mean judge score across all test cases (1-10)
  • Pass rate - percentage of cases scoring 7 or above
  • Worst-case score - lowest score in the dataset (catch catastrophic failures)
Operational Metrics
  • Token count - prompt tokens consumed per call (cost driver)
  • Latency - time-to-first-token and total response time
  • Brand adherence - custom criteria for tone, format, or compliance rules

5. CI/CD for Prompts

A prompt is code. It should be versioned, reviewed, and tested before it ships. The same rigor that prevents a bad function from reaching production should prevent a broken prompt from reaching users. In practice this means running your evaluation suite automatically on every prompt change, and blocking the merge if scores regress beyond a defined threshold.

The Automated Evaluation Flow

Prompt CI/CD Pipeline

DeveloperCI PipelineEvaluatorClaude (Judge)push prompt changerun eval suitegrade output (x N)score 1-10 + reasoningavg score + reportpass / fail gate

Figure 2: Every prompt change triggers the evaluation suite before merging

Regression Prevention

A common pattern: define a minimum acceptable average score (e.g., 7.5/10) and a minimum pass rate (e.g., 80%). A prompt change that drops either metric below the threshold fails the CI check. This enforces the rule that you can only ship a prompt change if you can prove it did not make things worse.

The compare-prompts command in the reference project demonstrates this: it runs both prompt versions against the same dataset and prints the score delta side-by-side.

Evaluation Platforms: Braintrust

At scale, engineering teams use dedicated evaluation platforms rather than rolling their own. Braintrust is the most widely adopted in the industry. It provides:

  • Dataset versioning - track your test cases as they evolve
  • Prompt versioning - compare any two prompt versions on the same dataset
  • Score tracking over time - see quality trends across deployments
  • Multi-provider support - works with Anthropic, OpenAI, Google, and others
  • Human-in-the-loop - mix automated scoring with manual review sessions

Building your own evaluation tooling (like the reference project below) is a great way to understand the mechanics. For production systems handling thousands of prompt iterations, a platform like Braintrust pays for itself quickly.

6. Reference Project: Meal Plan Evaluator

The reference project at gitlab.com/bytecode-solutions/examples/prompt-engineering implements the full evaluation workflow using the Anthropic Python SDK. It is structured as a reusable evaluator/ package with a concrete meal plan example in examples/meal_plan/.

Project Structure
  • evaluator/prompt_evaluator.py - reusable evaluation engine
  • evaluator/report.py - HTML report generator
  • examples/meal_plan/prompt_v1.py - naive prompt
  • examples/meal_plan/prompt.py - improved prompt
  • examples/meal_plan/cli.py - Click CLI commands
  • manager.py - entry point
What It Demonstrates
  • Dataset generation using Claude as the scenario designer
  • Parallel evaluation with ThreadPoolExecutor
  • Model-as-judge at temperature=0.0
  • HTML report with per-case reasoning and score coloring
  • Side-by-side v1 vs v2 comparison with score delta
Three-Step Workflow
# Step 1: Generate a diverse dataset of test cases using Claude
python manager.py generate-dataset --num-cases 10

# Step 2: Run the improved prompt against the dataset and generate an HTML report
python manager.py run-evaluation

# Step 3: Compare the naive prompt (v1) against the improved prompt (v2) side-by-side
python manager.py compare-prompts
Expected Output
# (example output, actual scores vary by dataset and model run)

Evaluating v1: naive  (no structure, no few-shot)...
  Average score: 4.3/10 --> examples/meal_plan/output_v1.html

Evaluating v2: improved (structured guidelines + few-shot example)...
  Average score: 8.7/10 --> examples/meal_plan/output_v2.html

--- Summary ---
  v1 (naive):    4.3/10
  v2 (improved): 8.7/10
  Improvement:   +4.4 points (+102%)

Open the HTML reports to understand why scores differ:
  v1: examples/meal_plan/output_v1.html
  v2: examples/meal_plan/output_v2.html
The Key Insight

The 102% score improvement is not magic, it is the direct result of adding three structural changes (XML tags, guidelines, few-shot example) and then measuring the impact. Without the evaluation pipeline, you would not know which change drove the improvement or whether any improvement happened at all. Measure first, then improve.

Bonus: core-genai - Evaluation as a Library

The reference project ships its own evaluator/ package built from scratch. If you want the same pipeline without building it yourself, core-genai packages it as a reusable Python library available on PyPI. It wraps the model-as-judge pattern behind a provider-agnostic IAgent interface, so you can swap between Claude, Gemini, ChatGPT, or Grok without changing your evaluation code.

Installation:pip install core-genaioruv pip install core-genai
from core_genai.agents.claude import ClaudeAgent
from core_genai.prompt.evaluator import PromptEvaluator

agent = ClaudeAgent(api_key="your-api-key")
evaluator = PromptEvaluator(
    agent=agent,
    model="claude-haiku-4-5-20251001",
    max_concurrent_tasks=5,
)

# Generate a diverse test dataset using Claude as the scenario designer
dataset = evaluator.generate_dataset(
    task_description="Write a compact 1-day meal plan for an athlete",
    prompt_inputs_spec={
        "height": "Athlete's height in cm",
        "weight": "Athlete's weight in kg",
        "goal": "Goal of the athlete",
        "restrictions": "Dietary restrictions",
    },
    num_cases=10,
    output_file="dataset.json",
)

# Run your prompt against every test case and produce a scored HTML report
results = evaluator.run_evaluation(
    run_prompt_function=run_prompt,
    dataset_file="dataset.json",
    extra_criteria="Must include daily caloric total and macronutrient breakdown",
    html_output_file="report.html",
)
  • Provider-agnostic - swap ClaudeAgent for GeminiAgent or ChatGPTAgent with no other changes to your evaluation code
  • Same pipeline - generate_dataset(), run_evaluation(), and the HTML report are all included out of the box
  • render() helper - lightweight template engine for {placeholder} substitution in prompt strings, with passthrough for unknown keys
  • Concurrent execution - ThreadPoolExecutor runs test cases in parallel; tune throughput via max_concurrent_tasks
  • Normalized metadata - get_cost() and get_metadata() return consistent token and USD cost fields across all providers

Key Takeaways

  • Measure before changing - without an evaluation dataset, every prompt change is a guess.
  • Context engineering over word tricks - what goes into context (examples, constraints, domain data) matters more than exact phrasing.
  • XML structure reduces ambiguity - Claude recognizes semantic tags; use them to separate inputs, instructions, and examples.
  • Model-as-judge at temperature=0 - a separate Claude call can grade outputs reproducibly and at scale, no humans required.
  • Treat prompts like code - version them, review them in PRs, and run automated evaluation on every change.
  • Platforms like Braintrust automate the eval pipeline for teams handling many prompt versions across multiple models.
Software Engineering in AI EraLesson 12