Optimizing Prompts Automatically
Minimal evolutionary prompt optimizer loop:
Pythonimport random def evaluate_prompt(prompt, model_call, test_cases, score_fn): """Run prompt against test cases, return average score.""" scores = [] for case in test_cases: output = model_call(prompt.format(**case["inputs"])) scores.append(score_fn(output, case["expected"])) return sum(scores) / len(scores) def mutate(prompt, mutation_bank): """Apply one random mutation operator.""" return random.choice(mutation_bank)(prompt) def crossover(p1, p2): split = len(p1) // 2 return p1[:split] + p2[split:] def optimize(seed_prompts, model_call, test_cases, score_fn, mutation_bank, generations=10, population_size=6, top_k=2): population = seed_prompts best = (None, -1) for gen in range(generations): scored = [(p, evaluate_prompt(p, model_call, test_cases, score_fn)) for p in population] scored.sort(key=lambda x: x[1], reverse=True) if scored[0][1] > best[1]: best = scored[0] parents = [p for p, _ in scored[:top_k]] population = [ mutate(crossover(random.choice(parents), random.choice(parents)), mutation_bank) for _ in range(population_size) ] print(f"Gen {gen}: best={scored[0][1]:.3f} | {scored[0][0][:60]}") return best
Run with concrete test cases and a scoring function (exact match, embedding similarity, LLM-as-judge) — never optimize without a measurable objective.
Progress:
- Define the task and success metric (accuracy, exact-match, BLEU, LLM-judge score, latency, cost)
- Build a labeled test/validation set (10-50+ representative examples with expected outputs)
- Write 2-5 seed prompts using manual best practices (few-shot, CoT, explicit instructions)
- Choose an optimization strategy based on budget/scale (see Best Practices)
- Implement the optimization loop with logging of every candidate + score
- Run optimization, tracking convergence (plot score vs. iteration/generation)
- Validate the winning prompt on a held-out test set (avoid overfitting to validation set)
- Deploy with monitoring; re-run optimization when model version or task distribution shifts
| Technique | When to use | Cost | Complexity |
|---|---|---|---|
| Manual few-shot / CoT scaffolding | Baseline before any automation | Low | Low |
| Evolutionary / genetic algorithms | Discrete prompt text search, no gradient access, black-box APIs | Medium | Medium |
| Self-consistency (sample + vote) | Reduce variance on reasoning tasks, no prompt rewrite needed | Medium (N× calls) | Low |
| Reinforcement learning (RL agent edits prompt) | Large-scale, repeated production task, budget for training | High | High |
| Gradient-based (soft prompts / embeddings) | You control model weights/embeddings (open-source models) | High | High |
| LLM-as-optimizer (meta-prompting: ask an LLM to rewrite the prompt based on failure cases) | Fast iteration, good default for most teams | Low-Medium | Low |
Default recommendation for most dev teams: LLM-as-optimizer + evolutionary selection. Use an LLM to propose prompt mutations conditioned on error analysis, then select via the evolutionary loop above. This avoids RL/gradient infrastructure while still being automated.
Example 1: LLM-as-optimizer mutation
Input: Current prompt scores 62% on sentiment classification; failure cases show it misses sarcasm.
Output: Meta-prompt to optimizer LLM: "This prompt gets these cases wrong: [examples]. Current prompt: '{prompt}'. Rewrite it to better handle sarcasm and ambiguous tone, keeping it concise." → New candidate prompt added to population, scored, kept if it beats parent.
Example 2: Self-consistency for math reasoning Input: CoT prompt on "If a train travels 60mph for 2.5 hours, how far does it go?" Output: Sample 5 completions at temperature 0.7, extract final answers [150, 150, 150, 120, 150], majority vote → 150 (discard the outlier).
Example 3: Evolutionary convergence Input: Seed prompts for a summarization task scored [0.55, 0.61, 0.58]. Output: After 8 generations of crossover+mutation, best prompt scores 0.81; logged lineage shows winning prompt added "Focus on the main action and outcome; omit background details" via mutation in generation 5.
- Always separate validation (for optimization) from test (for final check) sets — optimizing against the same set you report results on silently overfits.
- Make the fitness/score function as close as possible to the real business metric; proxy metrics (e.g., BLEU) can diverge from actual quality.
- Log every candidate prompt, its score, and generation/iteration — enables rollback and auditing of why a prompt was chosen.
- Start with cheap, fast evaluation (small test set, small model) to prune bad candidates before expensive full evaluation.
- Combine self-consistency with chain-of-thought for reasoning tasks — cheap variance reduction before investing in heavier optimization.
- Cap prompt length growth during mutation; unconstrained mutation tends to bloat prompts without improving quality.
- Re-validate optimized prompts whenever the underlying model is updated — prompts are often brittle across model versions.
- Optimizing on too few test cases (< 10) — leads to noisy scores and overfit prompts that fail in production.
- Using a single run's score as ground truth for stochastic models — average over multiple samples per candidate.
- Ignoring cost/latency in the fitness function — a prompt that's 1% more accurate but 5× more tokens may not be worth deploying.
- Letting evolutionary mutation operators be purely random word insertion (as in toy examples) — use semantically meaningful mutations (LLM rewrites, instruction additions/removals) for real gains.
- Deploying the "best" prompt from optimization without a held-out test set check — guaranteed overfitting to validation quirks.
- Treating gradient-based/RL optimization as a default — these require significant infra and are rarely the first tool to reach for; try meta-prompting and evolutionary search first.