Active prompting: a complete guide
Most few-shot prompts waste their examples. You grab a handful of cases at random, label them, and hope they cover what matters. Active prompting flips that: it finds the exact questions your model keeps getting wrong, gets a human to answer those, and uses them as your examples. Fewer labels, better results.
That single change — choose examples by how much they confuse the model — is what took standard chain-of-thought on GSM8K from 63.1% to 83.4% in the paper that introduced the technique (Diao et al., 2023, presented at ACL 2024).
See it work
Say you're building a math solver and you can afford to hand-label 4 examples for your prompt. Here's the difference selection makes.
Random few-shot picks whatever's on top of the pile:
Question: John has 5 apples and gives 2 to Mary. How many are left?
Answer: 3
Your model already nails problems like this. The example teaches it nothing.
Active prompting first asks the model the same question five times and watches where its answers scatter:
Question: A train leaves at 2:15 and arrives at 4:05, stopping twice for 8 minutes
each. What was its moving time?
Model's 5 tries: "1h50m", "1h34m", "110 min", "1h50m", "94 minutes" ← all over the place
That disagreement is gold. The model is confused here, so this is the example worth labeling by hand — with the full reasoning, not just the answer. Spend your 4 annotation slots on cases like this and the prompt teaches the model exactly where it's weak.
The mental model
Think about how you'd study for an exam. You don't re-read the chapters you've already mastered — you drill the problems you keep getting wrong. Active prompting does the same thing for a language model: it spends your limited labeling budget on the questions the model fails, not the ones it aces.
It's classic active learning (decades old in machine learning) pointed at prompt construction. The one-line version:
Uncertainty is a signal. Where the model disagrees with itself, there's something worth teaching.
How it works
Active prompting runs as a loop with three phases — estimate uncertainty, annotate the hard cases, then run inference. Some setups go around the loop a few times.
Walking through the steps:
- Assess uncertainty. Run the model over a pool of unlabeled examples (usually 100–1000). For each one, generate k diverse answers (k = 5–10) and measure how much they disagree.
- Select. Rank by uncertainty and keep the top n (n = 4–8) — the cases the model needs the most help with.
- Annotate. A human writes the gold answer and, for reasoning tasks, the step-by-step chain of thought. You're demonstrating the process, not just the result.
- Build the prompt. Assemble those annotated examples into a normal few-shot prompt, ordered simple to complex where you can.
- Run. Inference on your test set. If it's not good enough, loop back and add more.
Why it works
The gains break down into four factors, roughly ranked by how much they matter:
| Factor | Weight | What it buys you |
|---|---|---|
| Uncertainty metric quality | ~40% | High-uncertainty examples carry the most information — boundary cases, subtle distinctions |
| Annotation quality | ~30% | Expert reasoning (not just answers) for exactly the cases that confuse the model |
| Example quantity | ~20% | 4–8 is the sweet spot; returns fade past that |
| Selection diversity | ~10% | Covering different kinds of confusion widens what the prompt addresses |
The causal chain is short: find where the model is uncertain → label those weak spots → the examples hit the model's actual confusion → it learns the boundary conditions → accuracy climbs on similar hard cases. Better examples then let you tackle harder tasks, which surface new uncertainty to mine. It compounds.
Where it shines
Active prompting pays off most on tasks where difficulty varies a lot and reasoning matters — so the model is genuinely uncertain on some inputs and confident on others.
- Mathematical reasoning is the flagship use case: arithmetic word problems, algebra, symbolic reasoning (the original 83.4% GSM8K result).
- Complex question answering — multi-hop and commonsense reasoning that needs an inference chain.
- Code generation — surfacing tricky edge cases and unusual API patterns the model fumbles.
- Logical reasoning — deduction, induction, argument analysis.
- Scientific reasoning — multi-step physics and chemistry problems.
It carries over to specialized domains too, where labels are expensive and worth spending wisely:
- Educational assessment — surface the problems students find hardest, then teach with worked examples.
- Medical diagnosis — pick challenging cases for expert annotation; studies report a 15–20% improvement over random examples on differential-diagnosis tasks.
- Legal analysis — focus annotation on boundary cases and ambiguous statutory language.
- Financial analysis — target the edge cases in risk and fraud reasoning.
When to use it (and when not)
Active prompting earns its keep in a specific spot: few-shot already works, but you want more, and labeling is expensive enough that you can't afford to waste annotations on easy cases.
Reach for it when few-shot lands around 60–85%, you've got 100+ unlabeled examples, experts are available to annotate, the task varies a lot in difficulty, and it benefits from reasoning chains.
Skip it when zero-shot already hits target, few-shot is above 90% (no headroom) or below 40% (you need fine-tuning, not better examples), you have no example pool, every example is equally hard (no uncertainty to exploit), or you need results in real time.
The cost that's unique to this technique is upfront. Scoring uncertainty takes k × pool_size model calls — 500 examples at k=5 is ~$5 just to choose what to label. Then each annotation runs $5–50 of expert time depending on complexity. Serving cost is the same as any few-shot prompt (roughly 2–5× a zero-shot call). The payoff: you typically need 30–50% fewer annotations than random sampling for the same accuracy, so it pays off whenever labeling is costly.
Model fit: any reliable few-shot learner works as a floor (GPT-3.5, Claude 3, Llama 70B+). Stronger models (GPT-4, Claude 3.5 Sonnet) give cleaner uncertainty signals. Models under ~7B and base models without instruction tuning won't cut it. Reasoning models like O1/O3 barely benefit — they're already strong zero-shot, so keep examples minimal (2–4) and focus on format, not reasoning.
When to escalate: if the best examples still leave you under ~70%, or annotation costs more than fine-tuning would, switch to fine-tuning. Need strict output format? Structured outputs or fine-tuning. Knowledge-heavy domain? RAG.
Here's how it stacks up against the alternatives:
| Variant / alternative | When to choose it |
|---|---|
| Standard active-prompt (Diao 2023) | Math, logic, complex QA — CoT annotations, disagreement metric, single round |
| Iterative active-prompt | Budget for 2–3 rounds; refine progressively with early stopping |
| Active-prompt without CoT | Classification, extraction, simple generation — faster to annotate |
| Active + self-consistency | Squeeze out max accuracy when cost is secondary |
| Random few-shot | Labeling is cheap and examples are plentiful |
| Fine-tuning | You have thousands of examples and care more about serving cost |
| RAG | Knowledge-intensive tasks where the knowledge keeps changing |
Picking what's uncertain
Everything hinges on how you measure confusion. Disagreement is the default — the fraction of the k answers that diverge from the majority. It's simple and reliable:
def calculate_disagreement(responses):
answers = [extract_final_answer(r) for r in responses]
most_common = Counter(answers).most_common(1)[0][1]
return 1 - (most_common / len(answers)) # higher = more uncertain
You've got three other options when disagreement isn't a great fit:
- Entropy — Shannon entropy over the answer distribution. Finer-grained when there are many distinct answers.
- Variance — plain statistical variance of numerical outputs, ideal for math.
- Confidence —
1 − mean(model probability)across generations.
Match the metric to the output type. Disagreement shines for discrete answers but falls apart on open-ended text, where you're better off measuring semantic-similarity variance across response embeddings. For code, compare by execution equivalence or AST similarity — raw text disagreement will flag two correct-but-differently-written solutions as "uncertain" when they're not.
Never set temperature to 0 while estimating uncertainty. At temp 0 every sample is identical, disagreement collapses to zero, and you end up selecting essentially at random. Use 0.7–1.0 here. Save temp 0.0 for the final inference once your examples are locked.
A few scenario tweaks worth knowing:
- Classification — use class-probability distributions, pick cases near the decision boundary, and keep the selected set class-balanced.
- Complex reasoning — bump k to 10–15 for a stable signal and weigh reasoning-path diversity, not just answer disagreement.
- Domain-specific — your pool has to reflect the real distribution, and annotators need actual domain expertise.
- Low-resource — start with a 50–100 pool and 2–3 examples per round, trading quantity for annotation quality.
Building the prompt
The output is just a normal few-shot prompt. The selection is the only special part. You'll need an unlabeled pool (100–1000), an uncertainty metric, a way to pick the top-n, a human annotator, a prompt template, and a test set. Optionally: chain-of-thought annotations (do this for any reasoning task), multiple rounds, written annotation guidelines, and a validation set to tune n.
The format is the same Question / Reasoning / Answer pattern you'd use for any few-shot CoT prompt:
Question: [High-uncertainty question 1]
Reasoning: [Expert step-by-step explanation]
Answer: [Correct answer]
Question: [High-uncertainty question 2]
Reasoning: [Expert step-by-step explanation]
Answer: [Correct answer]
[More examples...]
Question: [Test question]
Reasoning:
And the whole technique fits in one loop — assess, select, annotate, construct, run:
def active_prompting(model, pool, test_set, n_examples=8, k_samples=5):
# 1. Estimate uncertainty: k diverse responses per pool example
uncertainties = []
for question in pool:
responses = [model.generate(question, temp=1.0) for _ in range(k_samples)]
uncertainties.append((question, calculate_disagreement(responses)))
# 2. Select the most uncertain examples
selected = sorted(uncertainties, key=lambda x: x[1], reverse=True)[:n_examples]
# 3. Human annotation with chain-of-thought (the only manual step)
annotated = [expert_annotate_with_cot(q) for q, _ in selected]
# 4. Construct the few-shot prompt
prompt = "".join(
f"Question: {ex['question']}\nReasoning: {ex['reasoning']}\nAnswer: {ex['answer']}\n\n"
for ex in annotated
)
# 5. Run on the test set
return [model.generate(prompt + f"Question: {q}\nReasoning:") for q in test_set]
Implementing it end to end
Here's the workflow from a cold start to production:
- Baseline (30 min). Measure zero-shot, then random few-shot. Only continue if few-shot shows promise but has room to grow.
- Prepare the pool (1–2 h). Collect 100–1000 representative examples, clean them, and split into pool / validation / test.
- Estimate uncertainty (1–2 h compute). Pick a metric, set k (5–10), score everything, and sanity-check that scores track real difficulty.
- Select (15 min). Take the top-n. Eyeball the top ~20 and keep the best 8 for quality and diversity.
- Annotate (30 min – 2 h). Experts write CoT against clear guidelines; validate and format consistently.
- Build and evaluate (1–2 h). Assemble the prompt (simple → complex), test on validation, and compare against the random baseline.
- Iterate (optional). Add examples targeting failure patterns; stop when validation gains drop below ~2% per round.
- Deploy. Lock the prompt, set inference parameters, document why each example was chosen, and monitor in production.
A minimal implementation looks like this (OpenAI shown — Anthropic and LangChain differ only in the SDK calls):
import openai
from collections import Counter
class ActivePrompting:
def __init__(self, api_key, model="gpt-4-turbo-preview"):
self.client = openai.OpenAI(api_key=api_key)
self.model = model
def generate_responses(self, question, k=5, temperature=1.0):
return [
self.client.chat.completions.create(
model=self.model, temperature=temperature, max_tokens=500,
messages=[{"role": "user", "content": question}],
).choices[0].message.content
for _ in range(k)
]
def select_uncertain(self, pool, n=8, k=5):
scored = []
for q in pool:
answers = [r.strip().split("\n")[-1] for r in self.generate_responses(q, k)]
disagreement = 1 - (Counter(answers).most_common(1)[0][1] / len(answers))
scored.append({"question": q, "uncertainty": disagreement})
return sorted(scored, key=lambda x: x["uncertainty"], reverse=True)[:n]
def run(self, annotated, test_question, temperature=0.0):
prompt = "".join(
f"Question: {e['question']}\nReasoning: {e['reasoning']}\nAnswer: {e['answer']}\n\n"
for e in annotated
) + f"Question: {test_question}\nReasoning:"
return self.client.chat.completions.create(
model=self.model, temperature=temperature, max_tokens=500,
messages=[{"role": "user", "content": prompt}],
).choices[0].message.content
# ap = ActivePrompting(api_key="...")
# uncertain = ap.select_uncertain(pool, n=8, k=5) # then annotate by hand
# answer = ap.run(annotated_examples, test_question)
Configuration
| Stage | Parameter | Guidance |
|---|---|---|
| Uncertainty estimation | k (samples) | 5–10. Below 3 is unreliable; above 15 wastes money. Start at 5, raise to 10 if scores look unstable. |
| Uncertainty estimation | temperature | 0.7–1.0 for diverse responses (you want the disagreement). |
| Selection | n (examples) | 4–6 for classification, 6–8 for reasoning, 8–12 for complex tasks. Returns fade past 12. |
| Inference | temperature | 0.0–0.2 (0.0 factual, 0.2–0.5 creative). |
| Inference | max_tokens | 50–200 for simple answers, 300–800 for reasoning. |
Do and don't
Do start with the disagreement metric, run estimation at temp 1.0, hand-pick the best 8 from the top-20 (quality beats raw score), require detailed CoT for reasoning tasks, validate before you pay for annotation, version-control prompts and examples, and always compare against a random baseline so you can prove the technique earned its cost.
Don't estimate at temp 0, select purely by score without a human glance (you'll grab outliers), skip the validation set, annotate without guidelines, push past ~12 examples, or reach for active prompting when random few-shot is already great.
When it goes wrong
Most failures show up as one of these symptoms. The fix usually follows from the cause:
- Selected examples don't look hard → metric mismatch, k too small, or temperature too low. Check that humans also find them hard, raise k to 10, push temperature to 1.0, or try entropy instead of disagreement.
- No better than random few-shot → weak annotations, too-narrow selection, or too few examples. Review CoT clarity, check diversity, raise n to 6–8, and confirm the task genuinely varies in difficulty.
- All uncertainty scores look the same → the task is uniformly easy or uniformly hard. If all high, you likely need fine-tuning; if all low, zero-shot is enough. Increase k or change the metric.
- Annotation is slow and expensive → drop n to 3–4 and iterate, write a guidelines template, or let the model draft annotations for a human to correct.
- Model still fails on certain inputs → your selection missed a difficulty pattern. Cluster examples and sample per cluster, run another round on the new frontier, or accept that RAG/fine-tuning fits those inputs better.
- Outputs are inconsistent despite good examples → set inference temperature to 0.0, add a format spec, or layer self-consistency (generate 5, take the majority).
Testing and proving it works
Reserve 10–20% of the pool as a holdout you never touch during estimation, and use it to tune n and k. For a tougher check, cross-validate across 5 folds (5× the compute, but it validates the selection process itself). Throw in a few hand-built adversarial edge cases to confirm active beats random where it counts.
The measurement that actually matters is improvement over a random baseline, with statistical significance. Annotate a random set and an active set, evaluate both on the same test set across several trials, and run a paired t-test:
def compare(self):
baseline = self.evaluate_active(selection="random")
active = self.evaluate_active(selection="uncertain")
improvement = (active - baseline) / baseline * 100
p_value = self.significance_test(baseline, active) # paired t-test / bootstrap
print(f"Random {baseline:.1%} | Active {active:.1%} | +{improvement:.1f}% | p={p_value:.4f}")
return {"baseline": baseline, "active": active,
"improvement": improvement, "p_value": p_value}
To squeeze more out of it: spend the annotation budget in halves (label 4, evaluate, stop early if you've hit target), inject diversity by taking the top-2n uncertain then clustering and keeping the most uncertain per cluster, and combine with self-consistency at inference time. Stop optimizing when validation gains fall under ~2% per round, you hit your budget, or you clear ~90%.
Limitations
Be honest about where it breaks down:
- It needs a pool. No 100+ representative unlabeled examples means no technique. That rules out genuinely novel or rare tasks.
- Annotation is the bottleneck. Gains live or die by annotation quality, and qualified experts in medicine or law are scarce and pricey.
- Estimation is expensive. k × pool_size forward passes (500 × 10 = 5,000 calls) only makes sense when the stakes or labeling costs are high.
- It leans on the metric. Some tasks — open-ended generation especially — have no clean uncertainty signal, so selection barely beats random.
- Returns diminish fast. The first 4–6 examples and the first round capture most of the win (round 1 ≈ 5–10%, round 2 ≈ 2–3%, round 3 under 1%).
- Context fills up. Eight detailed CoT examples (~300 tokens each) plus the question and response push toward ~3,100 tokens — tight on small windows.
- No guarantee. If difficulty is uniform across examples, uncertainty selection buys you nothing. Validate before you commit budget.
You can read most edge cases straight off the uncertainty distribution: a standard deviation under 0.1 means there's no usable signal (probably a fine-tuning job); a max score under 0.2 means the task's too easy (zero-shot will do); examples clustering in one difficulty type call for cluster-based selection; and annotators disagreeing (inter-annotator agreement under 0.7) flags genuinely ambiguous cases that need consensus. The standard fallback chain: flat signal → diverse sampling, estimation crashes → random sampling, active underperforms random on validation → just use the random set.
Three trade-offs you'll constantly balance: annotation budget vs accuracy (stop when marginal gain drops below ~1% per example), uncertainty vs diversity (top-2n then cluster), and context length vs example count (compress CoT when the window's tight).
Going further
Annotation quality is ~30% of your results, so the highest-leverage thing you can do is give annotators a fixed template and a worked example:
## Format
Question: [Original question]
Reasoning: [Detailed thought process, 2–5 sentences]
Answer: [Final answer in the specified format]
## Requirements
1. Break the problem into clear logical steps
2. Show intermediate calculations or inferences
3. Explain WHY each step follows from the last
4. Verify the answer makes sense; keep terminology consistent
## Avoid
- Answers with no reasoning, skipped steps, inconsistent notation, unjustified jumps
A good example earns its slot. It targets real model confusion (selected, not arbitrary), shows a clean reasoning chain with no unexplained leaps, looks like your actual test inputs, is expert-verified, and stays concise and consistently formatted. When a CoT annotation runs long, ask the model to compress it while keeping the key steps, the calculations, and the final check.
For output control, structure annotations as understand → plan → execute → verify, and bake the verification right into the demonstration ($15 spent + $35 left = $50 ✓). The model picks up hard constraints from examples that visibly check them ("that's exactly 3 sentences, as required"). For structured outputs, just show the exact schema in the answer field.
Models differ. GPT-4 handles 8–12 examples well and loves detailed CoT. Claude 3.5 Sonnet often needs fewer (4–6) and mirrors demonstrated format tightly. Llama 70B/405B wants more examples (8–12) and a higher k (8–10). Reasoning models (O1/O3) gain little and prefer minimal, format-focused examples. Deploying across several? Average uncertainty across them and pick examples that confuse all of them.
Adapting to a domain means a pool from the real distribution, domain experts as annotators, and conventions baked into the guidelines — medical (standard terminology, differential-diagnosis reasoning, contraindications), legal (IRAC structure, cite statutes and case law, jurisdiction), or code (edge cases, complexity notes, test cases in the reasoning).
Risks and ethics
Annotation is labor. Experts contributing specialized knowledge deserve fair pay, clear expectations so effort isn't wasted, credit, and clarity on who owns the annotations.
Beyond the generic prompting risks (which active prompting shares with plain few-shot), three are specific to it:
Selection can amplify bias. If the model happens to be more uncertain on a particular demographic, uncertainty sampling will over-represent that group in your examples. Catch it by comparing the attribute distribution of the selected set against the pool (a chi-square test or KL divergence), and fix it with stratified selection — pick the most uncertain example within each group, proportionally.
- Adversarial uncertainty. If users can feed the pool, an attacker can craft inputs designed to maximize disagreement and force their own examples into your prompt. Sanitize user-generated entries, manually review the top-20 before annotating, and cross-check with a second metric.
- Cascading annotation errors. One wrong annotation teaches a wrong pattern that spreads to every similar input. Gate it: have a second expert independently verify a 20% sample and require ≥90% agreement before you proceed.
- Dual-use capability mapping. Active prompting systematically maps where your model is weakest. That data is great for improving the model — and also a roadmap for building adversarial examples. Treat the uncertainty log as sensitive.
Expect a handful of failure modes: poor uncertainty estimation (medium likelihood — validate the metric on a small sample first), low-quality annotations (multi-annotator verification), overfitting to the selected set (holdout test set), and blowing the annotation budget (iterate and measure ROI per example). And be transparent: disclose that examples were chosen by model uncertainty, document the annotation and quality-control process, and make clear the system only knows its annotations plus pre-training.
The ecosystem around it
For the human-in-the-loop step, general annotation platforms with active-learning support fit cleanly — Label Studio, Prodigy, and managed pipelines like AWS SageMaker Ground Truth. For assembling the prompt itself, few-shot templating tools (LangChain's FewShotPromptTemplate, DSPy's bootstrap optimizers, Haystack prompt nodes) take actively-selected examples directly. The selection logic is yours; the downstream wiring is plain few-shot.
It connects naturally to its neighbors. Active prompting is optimized few-shot — same format, uncertainty-selected examples instead of random, for a typical 5–15% lift. It usually carries chain-of-thought inside its annotations (selection and reasoning-format are independent and combine well), and it pairs with self-consistency, which uses multiple samples at inference for voting just as active prompting uses them at selection for measuring confusion.
| Technique | Selection | Annotation | Typical improvement |
|---|---|---|---|
| Zero-shot | none | none | baseline |
| Random few-shot | random | n examples | +10–20% vs zero-shot |
| Active prompting | uncertainty | n examples | +5–15% vs random few-shot |
| Manual curation | expert judgment | n examples | +5–20% vs random (expert-dependent) |
| Auto-CoT | diversity | none (auto) | +5–10% vs zero-shot |
| Fine-tuning | all data | hundreds–thousands | +20–40% vs few-shot |
The hybrids are where it gets fun. Active + RAG retrieves candidate documents, then uses uncertainty to keep the most informative ones as context. Active + self-consistency does active selection for the prompt, then ensembles several inferences with a majority vote. In production, the same machinery runs as a standing loop — log uncertain cases from real traffic, periodically annotate the most uncertain, A/B-test the augmented set, and roll forward only when validation improves (with rollback on regression). Moving from random few-shot, pilot with 3–4 examples and scale only if you beat the baseline by more than 3%. Moving to fine-tuning, reuse the actively-annotated hard examples as high-value training data.
Where it's headed
Two recent findings are worth tracking. Over-prompting: more examples can actually hurt in some models, which means the optimal annotation budget is model- and task-specific, not a fixed number. And Google's Uncertainty-based Sampling Prompting (USP) estimates confidence through self-consistency proxies, skipping the extra model calls that make estimation expensive today. Active research also points at continuous/automated active prompting over live traffic, transferring uncertainty patterns across related tasks, multi-modal selection for vision-language models, and federated selection that ranks examples across organizations without sharing raw data.
The open questions are the interesting part: which uncertainty metrics are best per task type (and can we learn them), can we prove sample-complexity bounds, how should we split budget between annotation quality and quantity, how many rounds before benefits plateau, do examples chosen for one model transfer to another, and can we compress eight examples into four richer ones without losing the effect.
This is exactly how the original result happened. Diao et al. didn't invent a new model — they took the same code-davinci-002 and the same chain-of-thought format everyone else used, and only changed which examples went in the prompt. Selecting the uncertain ones took GSM8K from 63.1% to 83.4%, with gains of 1.0–15.4% across MultiArith, SVAMP, ASDiv, and AQUA, and a 2.1–7.2% edge over self-consistency. Same model, smarter examples.
Summary
- The idea: spend your labeling budget on the examples the model is most confused about, not random ones — like drilling the exam questions you keep failing.
- The loop: sample k answers per example → score disagreement → annotate the top-n uncertain by hand (with reasoning) → build a few-shot prompt → run, and repeat if needed.
- Use it when few-shot lands ~60–85%, you have 100+ examples and expert annotators, and labeling is expensive. Skip it when zero-shot suffices, few-shot is already above 90% or below 40%, or difficulty is uniform.
- What drives results: uncertainty-metric quality (~40%) and annotation quality (~30%) matter far more than how many examples you add.
- Prove it: always benchmark against a random baseline with a significance test — the whole point is fewer, better-chosen labels.
Read Next
Start reading to get personalized recommendations
Explore Unread
Great job! You've read all available articles