Prompt Optimization and Iteration¶
Prerequisites¶
Before this lesson you should be comfortable with:
- Prompt anatomy and few-shot prompting — Lessons 1 and 2
- Prompt templates and versioning — Lesson 5
- Basic Python — loops, dictionaries, and simple metrics
You do not need statistics background. This lesson focuses on practical eval workflows, not rigorous experimental design.
What You'll Learn¶
| Objective | Time | Difficulty |
|---|---|---|
| Build an evaluation dataset to measure prompt quality | 10 min | Intermediate |
| Run A/B tests comparing prompt variants systematically | 10 min | Intermediate |
| Apply progressive refinement techniques that improve most prompts | 8 min | Intermediate |
| Analyze failure cases to target the next iteration | 7 min | Intermediate |
Intuition First: Tuning Without a Speedometer¶
Optimizing a prompt by "trying things until it looks better" is like tuning a car engine by ear with no speedometer. You might feel improvement, but you cannot prove it, reproduce it, or know when you have regressed.
Prompt optimization needs measurement. An eval set is your speedometer: a fixed list of inputs with known expected outputs. Run every prompt variant against the same eval set, compare scores, and keep the winner.
Most production teams iterate 5–15 times before a prompt reaches acceptable quality. The teams that ship fastest are not the ones with the best intuition — they are the ones with the best eval loops.
The Prompt Optimization Loop¶
Each iteration should change one thing so you know what caused the improvement (or regression). Change the examples, re-run eval. Change the constraints, re-run eval. Change both at once and you learn nothing.
Building an Evaluation Set¶
from openai import OpenAI
client = OpenAI()
eval_set = [
{"input": "I absolutely love this product! Best purchase ever.", "expected": "positive"},
{"input": "Terrible experience. Broke after one day.", "expected": "negative"},
{"input": "It works fine. Nothing special but does the job.", "expected": "neutral"},
{"input": "Good quality but way too expensive.", "expected": "neutral"},
{"input": "Not bad at all, actually pretty good.", "expected": "positive"},
{"input": "Great, another broken product. Thanks a lot.", "expected": "negative"},
# Target 20-50 examples for reliable results
]
def llm_call(prompt: str) -> str:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
return response.choices[0].message.content.strip().lower()
def evaluate_prompt(prompt_template: str, eval_set: list[dict]) -> dict:
"""Run a prompt against an evaluation set and measure accuracy."""
correct = 0
results = []
for item in eval_set:
prompt = prompt_template.format(text=item["input"])
predicted = llm_call(prompt)
is_correct = item["expected"] in predicted
correct += int(is_correct)
results.append({
"input": item["input"][:50],
"expected": item["expected"],
"predicted": predicted,
"correct": is_correct,
})
return {"accuracy": correct / len(eval_set), "results": results}
Start with 20 examples covering all categories and known edge cases. Add new failures from production weekly. The eval set is a living document, not a one-time artifact.
A/B Testing Prompts¶
prompt_v1 = "Classify the sentiment as positive, negative, or neutral:\n\n{text}"
prompt_v2 = """Classify the sentiment of the following text.
Rules:
- "positive": expresses satisfaction, happiness, or enthusiasm
- "negative": expresses dissatisfaction, frustration, or disappointment
- "neutral": factual, balanced, or neither positive nor negative
Text: {text}
Sentiment (respond with exactly one word):"""
results_v1 = evaluate_prompt(prompt_v1, eval_set)
results_v2 = evaluate_prompt(prompt_v2, eval_set)
print(f"v1 accuracy: {results_v1['accuracy']:.1%}")
print(f"v2 accuracy: {results_v2['accuracy']:.1%}")
# Typical: v1 ~72%, v2 ~85% after adding explicit rules
In production, A/B testing routes live traffic: 90% gets the stable prompt, 10% gets the challenger. Compare accuracy, latency, and cost over 24–48 hours before full rollout.
Progressive Refinement¶
Most prompts improve through a predictable sequence:
# Iteration 1: Basic (~72% accuracy)
v1 = "Classify sentiment: {text}"
# Iteration 2: Add definitions (~85% accuracy)
v2 = """Classify sentiment as positive, negative, or neutral.
positive = expresses satisfaction or enthusiasm
negative = expresses dissatisfaction or frustration
neutral = factual or balanced
Text: {text}
Sentiment:"""
# Iteration 3: Add examples (~91% accuracy)
v3 = """Classify sentiment as positive, negative, or neutral.
Examples:
"Best purchase ever!" → positive
"Broke after one day" → negative
"It does the job" → neutral
Text: {text}
Sentiment:"""
# Iteration 4: Handle edge cases (~94% accuracy)
v4 = """Classify sentiment as positive, negative, or neutral.
Rules:
- Sarcasm: classify by actual intent ("Great, another broken product" = negative)
- Mixed sentiment: default to the dominant emotion
- Questions without sentiment are neutral
Examples:
"Best purchase ever!" → positive
"Broke after one day" → negative
"It does the job" → neutral
Text: {text}
Sentiment (one word):"""
for version, prompt in [("v1", v1), ("v2", v2), ("v3", v3), ("v4", v4)]:
result = evaluate_prompt(prompt, eval_set)
print(f"{version}: {result['accuracy']:.1%}")
| Technique | When to Use | Typical Improvement |
|---|---|---|
| Add explicit rules | Output is inconsistent | +10–20% accuracy |
| Add examples (few-shot) | Model misunderstands the task | +15–25% accuracy |
| Define output format | Responses vary in structure | Consistency, easier parsing |
| Add chain-of-thought | Reasoning tasks, math | +10–30% on complex tasks |
| Reduce ambiguity | Edge cases fail | +5–15% accuracy |
| Add constraints | Output too long/short | Controls output quality |
Analyzing Failures¶
def analyze_failures(eval_results: dict) -> None:
"""Print failed cases to identify patterns."""
failures = [r for r in eval_results["results"] if not r["correct"]]
print(f"\n{len(failures)} failures out of {len(eval_results['results'])} cases:\n")
for f in failures:
print(f" Input: {f['input']}")
print(f" Expected: {f['expected']}")
print(f" Got: {f['predicted']}")
print()
# Run after each iteration
analyze_failures(evaluate_prompt(v3, eval_set))
# Common failure patterns to look for:
# - Sarcasm misclassified
# - Mixed sentiment inconsistent
# - Edge cases (empty text, single words)
# - Format issues (extra words in output)
Group failures by pattern, not by individual case. "3 sarcasm failures" → add a sarcasm rule. "2 format failures" → add "respond with exactly one word." Targeted fixes beat random prompt edits.
Production Connection¶
Prompt optimization is not a one-time task — it is a continuous process:
- Version and track scores — store
{prompt_version: accuracy}in metadata. Never deploy a prompt without recording its eval score. - A/B test in staging first — run the challenger against the full eval set. Only promote to production A/B if it wins on accuracy without regressing on latency or cost.
- Eval loops on every PR — CI runs the eval set against changed prompts. Block merge if accuracy drops below threshold.
- Failure recovery — when production accuracy drops, roll back to the previous prompt version immediately. Investigate with the eval set afterward.
- Refresh eval sets bi-weekly — add production failures as new test cases. Prompts that score 95% on a stale eval set may score 80% on current traffic.
Edge Cases & Common Misconceptions¶
Misconception 1: 100% eval accuracy means production-ready. Eval sets are samples. Overfitting to 20 examples is easy. Hold out a separate test set you never optimize against.
Misconception 2: Bigger models fix bad prompts. A better model on a vague prompt might go from 60% to 75%. A refined prompt on a cheap model might go from 60% to 94%. Optimize the prompt first.
Misconception 3: One metric is enough. Track accuracy, latency, token cost, and parse failure rate. A prompt that wins on accuracy but doubles cost may not be worth deploying.
Misconception 4: Optimization ends at deployment. Production traffic surfaces new edge cases continuously. The eval loop never stops.
Key Takeaways¶
- You cannot improve what you do not measure — build an eval set before optimizing.
- Change one variable per iteration so you know what caused improvement or regression.
- Progressive refinement: rules → examples → edge-case handling is the most common winning sequence.
- Analyze failures by pattern (sarcasm, format, ambiguity), not individual cases.
- A/B test in staging against the full eval set; canary in production before full rollout.
- Track accuracy, latency, and cost together — a winning prompt must pass all three.
- Refresh eval sets with production failures bi-weekly to prevent eval-production drift.
- Roll back immediately on production accuracy drops; investigate with eval afterward.
Next Lesson¶
Lesson 8: Handling Edge Cases and Guardrails — Build robust prompts that handle adversarial inputs, edge cases, and failure modes gracefully.