PromptisePromptise
Why PromptiseUse casesRoadmapDocs
GitHubGet started
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Guides> Testing and evals

Automatic Prompt Optimization: APE, ProTeGi and AMPO

Automatic prompt optimization explained: APE, ProTeGi, OPRO and multi-branch prompts, with one real optimization round on a ticket classifier in Python.

Level
Advanced
Reading time
9 min
Published
Sep 7, 2025
Last updated
Oct 25, 2026
By
Promptise Team
  • Prompt Optimization
  • Evals and Testing
Contents · 9 sections↓
  1. 01What is automatic prompt optimization?
  2. 02How the methods work
  3. 03When to use automatic prompt optimization (and when not to)
  4. 04Worked example: one optimization round on a ticket classifier
  5. 05Multi-branch prompts
  6. 06Running it for real
  7. 07Common mistakes
  8. 08Frequently asked questions
  9. 09Next steps

Automatic prompt optimization uses a model to write and improve prompts for you, scored against labeled examples instead of judged by eye. You give it a task, a small dataset and a metric. It proposes instructions, measures them, studies the failures and edits the prompt. This guide explains the main methods (APE, ProTeGi, OPRO and multi-branch prompts like AMPO), then walks through one real optimization round on a ticket classifier in plain Python. It's for developers who already have a prompt in production and a few dozen labeled examples.

[01]

What is automatic prompt optimization?

Automatic prompt optimization treats the prompt as a parameter you search over. A model generates candidate instructions, each candidate is scored on labeled examples, and the best ones are kept or edited further. APE generates and selects candidates. ProTeGi critiques a prompt using its errors and edits it. OPRO shows the optimizer past prompts with their scores. The common requirement is a dataset and a score you trust.

Key takeaways:

  • No dataset, no optimization. You need labeled examples and a metric; 20 to 50 examples is a workable start.

  • Hold out a test set. Optimized prompts overfit the examples they were tuned on.

  • Learning from errors (ProTeGi-style) is the most practical loop for an existing prompt.

  • Read the winning prompt. It may encode rules you didn't intend.

  • Use a library at scale. DSPy and similar tools run these searches for you.


[02]

How the methods work

Method

Paper

How it works

What it's good at

APE (Automatic Prompt Engineer)

Zhou et al., 2022, arXiv:2211.01910

Shows the model input-output pairs, asks it to infer the instruction, generates many candidates, scores each, keeps the best

Writing a first instruction from examples

ProTeGi / APO

Pryzant et al., 2023, EMNLP, arXiv:2305.03495

Uses a minibatch of errors to write a natural-language "gradient" (a critique), edits the prompt "in the opposite semantic direction", beam search and bandit selection pick candidates

Improving an existing prompt

OPRO

Yang et al., 2023, ICLR 2024, arXiv:2309.03409

Puts previous prompts and their scores in the optimizer's prompt; the model proposes better ones each step

Searching when you can run many iterations

AMPO

Yang et al., 2024, arXiv:2410.08696

Builds a multi-branched prompt from failure cases, with pattern recognition, branch adjustment and branch pruning

Tasks where different inputs need different handling

DSPy

Khattab et al., 2023, arXiv:2310.03714

Declares the pipeline in code; optimizers tune instructions and few-shot demonstrations against a metric

Multi-step pipelines, production use

The reported gains are on the papers' benchmarks: APE matched or beat human-written instructions on 19 of 24 tasks; ProTeGi improved initial prompts by up to 31%; OPRO's prompts beat human-designed ones by up to 8% on GSM8K and up to 50% on Big-Bench Hard tasks. Your gain depends on how weak the starting prompt is and how clean your labels are.


[03]

When to use automatic prompt optimization (and when not to)

Use it when:

  • The task has a clear right answer per example: classification, extraction, routing, short-form QA.

  • You have, or can label, at least a few dozen examples that look like production traffic.

  • You'll re-run it: when the model changes, when the label set changes, when new failures appear.

Skip it when:

  • You can't score outputs automatically. For open-ended writing, an LLM judge is possible, but you're now optimizing against the judge's taste.

  • The examples are unrepresentative. The optimizer learns your dataset, not your users.

  • A missing rule is obvious. If you already know the fix, write it.


[04]

Worked example: one optimization round on a ticket classifier

The task: label support tickets as billing, account_access, bug, feature_request or other. There are 12 labeled development examples and 8 held-out test examples, kept in DEV and TEST as lists of (ticket, label) pairs. The house rule is that a ticket about money or login goes to billing or account access even if it mentions an error, but the starting prompt doesn't say so.

Step 01

Score the current prompt

Pythonapo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import json
from openai import OpenAI

client = OpenAI()
LABELS = ["billing", "account_access", "bug", "feature_request", "other"]
SCHEMA = {"name": "label", "strict": True, "schema": {
    "type": "object", "additionalProperties": False,
    "properties": {"label": {"type": "string", "enum": LABELS}}, "required": ["label"]}}

def classify(instruction: str, ticket: str) -> str:
    r = client.chat.completions.create(
        model="gpt-5-mini", reasoning_effort="minimal",
        messages=[{"role": "system", "content": instruction}, {"role": "user", "content": ticket}],
        response_format={"type": "json_schema", "json_schema": SCHEMA})
    return json.loads(r.choices[0].message.content)["label"]

def score(instruction: str, data: list[tuple[str, str]]):
    errors = [{"ticket": t, "predicted": p, "expected": gold}
              for t, gold in data if (p := classify(instruction, t)) != gold]
    return len(data) - len(errors), errors

base = "Classify the support ticket as billing, account_access, bug, feature_request or other."
for name, data in [("dev", DEV), ("test", TEST)]:
    ok, errors = score(base, data)
    print(f"baseline {name}: {ok}/{len(data)}")
    if name == "dev":
        for e in errors:
            print(f'- "{e["ticket"]}" -> predicted {e["predicted"]}, expected {e["expected"]}')
Output
baseline dev: 9/12
- "After the SSO update the login page shows error 500." -> predicted bug, expected account_access
- "When I try to downgrade my plan the page freezes." -> predicted bug, expected billing
- "The mobile app logs me out every few minutes." -> predicted bug, expected account_access
baseline test: 7/8

The structured output with an enum matters: the classifier can only return a valid label, so the score measures judgment, not formatting.

Step 02

Ask for a "gradient"

ProTeGi's first step is a critique written from the failures:

Text
1
2
3
4
5
6
7
8
I'm writing a prompt for a support-ticket classifier.
My current prompt is:
"Classify the support ticket as billing, account_access, bug, feature_request or other."

It gets these examples wrong:
<the three errors>

Give 3 short reasons why the prompt could have produced these errors. Be specific about missing rules.
Output
1) No rule to prefer user-facing intent/category over technical symptom — the prompt doesn't say whether to classify by the user's problem domain (e.g., "can't access account") or by the technical failure mode (e.g., "server error"), so "error 500" and "page freezes" get treated as bugs instead of account_access or billing.

2) Missing explicit mappings for common keywords/contexts — there are no rules like "SSO, login, logout, session expiry → account_access" or "plan, downgrade, payment → billing", so the classifier defaults to generic "bug" when it sees errors or failures.

3) No tie-breaker for ambiguous tickets …

That diagnosis is correct. It found the missing rule from three examples.

Step 03

Edit the prompt and score the candidates

Text
1
2
3
4
5
6
7
8
Current prompt:
"<base prompt>"

Problems found:
<the critique>

Write 3 different improved versions of the prompt that fix these problems. Each must still be a
general instruction (no copied example tickets) and under 120 words.

Each candidate is scored on the development set, and the best one on the test set it has never seen:

Output
candidate 0: dev 11/12
candidate 1: dev 12/12
candidate 2: dev 12/12

best candidate on held-out test: 8/8

The winning prompt:

Output
Assign one of: billing, account_access, bug, feature_request, other. Prefer user-facing intent: payment/plan/charge/refund/invoice → billing; login/SSO/session/password/account locked → account_access. Map explicit requests for new functionality to feature_request. Classify technical failures (error codes, crashes, page freezes, missing UI behavior) as bug only when no billing/account intent exists. If both intent and technical symptom occur, pick the user's intent. Use other for unclear or unrelated tickets.

Read it before you ship it. It contains a keyword list ("SSO", "session") lifted from the failures, which is fine here but is how optimized prompts overfit. The held-out 8/8 says it generalized at least to those examples. With 20 examples in total, this is a sanity check, not a measurement; use more examples before trusting small differences.

The full ProTeGi algorithm repeats this with beam search over many candidates and bandit selection to save evaluation calls. One round already fixed the obvious gap.

One optimization round on the ticket classifier: the current prompt scores 9 of 12 on the dev set, a critique of the three errors drives an edit into three candidates scoring 11, 12 and 12 of 12, and the best one gets 8 of 8 on the held-out test set before you read it and ship it.
FigureOne ProTeGi-style optimization round on the ticket classifier, with the scores from the worked example

What APE would have done

APE starts from examples instead of a prompt. Given six input-output pairs, the model inferred three instructions. Scored on the same development set:

Output
candidate 0: dev 8/12
Classify each user message into one of these categories: billing, account_access, bug, or feature_request. Return only the single label for each input.
…
candidate 2: dev 10/12
Given a user complaint or request, decide which of these labels fits best: billing (payment/refund issues), account_access (login/2FA/SSO problems), bug (software defects), feature_request (new feature or improvement). Respond with only the label.

None of the candidates mentions other, because none of the six demonstrations was labeled other. APE can only infer what the examples show. Cover every label in the demonstrations, and generate many more candidates than three; the paper's strength comes from searching a large pool.


[05]

Multi-branch prompts

Look at the winning prompt again: "if both intent and technical symptom occur, pick the user's intent." That's a branch: different inputs follow different rules. AMPO (Yang et al., 2024) builds such branches deliberately. It groups failure cases into patterns, adds or adjusts a branch for each pattern, and prunes branches that don't help. The result reads like a decision procedure:

Text
If the ticket asks for money back, a plan change or an invoice fix: billing, even if a bug caused it.
Else if the user can't sign in or stay signed in: account_access, even if an error code appears.
Else if something that exists doesn't work: bug.
Else if the user asks for something new: feature_request.
Else: other.

Branching helps when your task really has several input types. It also makes the prompt longer and harder to maintain, so prune branches that no longer change any score.


[06]

Running it for real

  • Keep prompts versioned. Store each candidate with its scores and the dataset version. In Promptise Foundry, PromptRegistry stores named, versioned prompts and supports rollback, so you can revert an optimized prompt that misbehaves in production.

  • Re-score on model changes. A prompt optimized for one model may score differently on another.

  • Grow the dataset from production failures. Every misrouted ticket is a new labeled example.

  • Use DSPy or a similar library when you have a pipeline with several steps, or want to optimize few-shot demonstrations together with the instruction.


[07]

Common mistakes

Mistake

Why it fails

Fix

Scoring on the examples you optimized on

You measure memorization

Keep a held-out test set

A handful of examples

One example flips the score by 10 points

20 to 50 or more, matching production

Free-text outputs when scoring classification

Formatting noise counts as errors

Structured output with an enum

Shipping the top-scoring prompt unread

It may contain copied examples or odd rules

Review it like code

Demonstrations that miss labels

APE never learns those labels exist

Cover every label in the examples

Optimizing against an LLM judge without checking the judge

You tune to the judge's bias

Validate the judge on human labels first


[08]

Frequently asked questions

What's the difference between APE and ProTeGi?

APE writes candidate instructions from input-output examples and picks the best by score. ProTeGi starts from an existing prompt, writes a critique from its errors, edits the prompt to address the critique, and keeps the best edits. Use APE to bootstrap a prompt and ProTeGi to fix one.

How many examples do I need?

Enough that one example doesn't swing the score much. Twenty to fifty is a practical start for a classifier; more for many labels or subtle rules. Always keep some examples out of the optimization to test the final prompt.

Is DSPy the same as automatic prompt optimization?

DSPy is a framework that includes prompt optimizers. You declare the steps of your pipeline and a metric, and its optimizers tune instructions and few-shot examples. It saves you from writing the loop in this guide yourself.

Can I optimize prompts without labeled data?

Only with a substitute signal: an LLM judge, a code check (valid SQL, passing tests) or user feedback. Code checks are the most reliable. Judges work if you validate them against some human labels first.

Will an optimized prompt work on a different model?

Not necessarily. Prompts can depend on one model's habits. Re-score on the new model and re-run the optimization if the score drops.


[09]

Next steps

  • Build the dataset and checks first: beginner eval loops.

  • Improve prompts by hand first: simple instruction tuning by iteration.

  • Let models revise their outputs instead: self-refine and self-correction.

  • Prompt tests in CI: how to test LLM prompts in Python.

  • Choosing examples: few-shot prompting.

Contents

  1. 01What is automatic prompt optimization?
  2. 02How the methods work
  3. 03When to use automatic prompt optimization (and when not to)
  4. 04Worked example: one optimization round on a ticket classifier
  5. 05Multi-branch prompts
  6. 06Running it for real
  7. 07Common mistakes
  8. 08Frequently asked questions
  9. 09Next steps

Learning paths

Want more structure? Paths put guides in order, like a short course.

See the paths →

> Testing and evals

Related guides

All guides in this topic →
  • // 01Advanced · 7 minLLM Self-Correction: Self-Refine, Reflexion and CRITICLLM self-correction without fine-tuning: how Self-Refine, Reflexion and CRITIC work, why external feedback matters, and a real self-review vs test example.
  • // 02Intermediate · 20 minHow to Evaluate AI Agents: Evals, LLM Judges and pass^kHow to evaluate AI agents: write golden and refusal tasks, grade tool calls in code, calibrate an LLM judge, measure pass^k and gate CI on the results.
  • // 03Intermediate · 24 minHow to Test LLM Prompts and AI Agents in PythonUnit test LLM prompts with pytest, catch prompt regressions in CI, grade outputs with an LLM judge and check which tools an AI agent calls, in Python.
  • // 04Intermediate · 16 minTesting MCP Servers in Python: pytest Without a Live ServerTest an MCP server in Python without running it: auth, guards, middleware, resources, prompts, error envelopes and job queues in 35 pytest tests, plus CI.
  • // 05Intermediate · 18 minAI Agent Evals in CI: Run Them on Every Pull RequestRun AI agent evals in CI on every pull request: test tool choice, arguments, answers and safety, keep cost and flakiness down, with GitHub Actions.
  • // 06Intermediate · 16 minWho Decides When an AI Agent Is Done? Checks and JudgesWho decides when an AI agent is done? Stop conditions, self-checks, success criteria in code, a separate LLM judge and budgets as a backstop, with code.

Keep going.

Browse every guide by topic and level, or follow a learning path that puts them in order.

All guidesLearning paths