Module 5: ReAct, Self-Consistency, and Advanced Patterns
5. Meta-Prompting and Self-Refine
Overview
The previous techniques (ReAct, Self-Consistency, ToT) focus on how the model reasons to solve a problem. Meta-prompting and Self-Refine tackle a different, fundamental problem: how do we improve prompts and answers systematically?
Meta-prompting uses an LLM to generate, optimize or evaluate prompts for another task. It's like hiring a communication expert to write your system's instructions.
Self-Refine (Madaan et al., 2023) implements an iterative loop: the model generates an answer, then critiques it from the problem's perspective, and finally improves it. It's the equivalent of a student writing an essay, re-reading it with a critical eye, and rewriting it with that perspective.
Meta-Prompting: The LLM That Writes Prompts
The Intuition
Writing good prompts is hard. It requires understanding:
- How the model behaves in the face of certain language patterns
- What level of detail is necessary
- Which examples are most useful
- How to phrase the expected output
What if, instead of doing it by hand, we ask the LLM itself to design the prompt?
Base Implementation
from openai import OpenAI
import json
client = OpenAI()
def meta_prompt_generator(
task: str,
context: str = "",
output_format: str = "",
constraints: list[str] = None,
examples: list[dict] = None
) -> str:
"""
Generates an optimized prompt for a specific task.
Args:
task: Description of the task (e.g. "classify the sentiment of tweets")
context: Extra context about the domain or the users
output_format: How the output should be formatted
constraints: List of constraints or requirements
examples: Examples of the desired input/output
Returns:
A prompt optimized for the task
"""
constraints_str = "\n".join([f"- {c}" for c in (constraints or [])])
examples_str = ""
if examples:
examples_str = "\nExamples of the desired input/output:\n"
for ex in examples:
examples_str += f"Input: {ex.get('input', '')}\nOutput: {ex.get('output', '')}\n\n"
meta_prompt = f"""You are a prompt engineering expert. Your task is to create the OPTIMAL prompt for the following use case.
TASK TO SOLVE: {task}
ADDITIONAL CONTEXT:
{context if context else "None"}
EXPECTED OUTPUT FORMAT:
{output_format if output_format else "Not specified"}
CONSTRAINTS/REQUIREMENTS:
{constraints_str if constraints_str else "None"}
{examples_str}
Create a prompt that:
1. Is clear and specific about the task
2. Includes an explicit output format
3. Anticipates and handles edge cases
4. Uses the right level of detail (neither too vague nor over-specified)
5. If it applies, includes 1-2 examples (few-shot)
IMPORTANT: Return ONLY the prompt, with no explanations or extra metadata.
The prompt has to be ready to use directly."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": meta_prompt}],
temperature=0.3,
max_tokens=800
)
return response.choices[0].message.content.strip()
# Usage example:
if __name__ == "__main__":
generated_prompt = meta_prompt_generator(
task="Classify the sentiment of restaurant reviews",
context="Reviews can mix positive and negative aspects. They're in English.",
output_format="JSON with: sentiment (POSITIVE/NEGATIVE/MIXED/NEUTRAL), score (0-1), main_aspect",
constraints=[
"Consider food as well as service and atmosphere",
"Detect sarcasm and irony",
"Handle very short reviews (1-2 words)"
],
examples=[
{"input": "The food was amazing but it took them 45 minutes to bring it.",
"output": '{"sentiment": "MIXED", "score": 0.5, "main_aspect": "food+service"}'},
{"input": "Meh.",
"output": '{"sentiment": "NEUTRAL", "score": 0.5, "main_aspect": "general"}'}
]
)
print("Prompt generated by meta-prompting:")
print("=" * 50)
print(generated_prompt)
Advanced Meta-Prompting: Multiple Candidates
def meta_prompt_with_evaluation(
task: str,
n_candidates: int = 3,
golden_set: list[dict] = None
) -> dict:
"""
Generates N candidate prompts and evaluates which one is best
using a golden set of examples with correct answers.
Args:
task: The task to solve
n_candidates: Number of candidate prompts to generate
golden_set: List of {"input": ..., "expected": ...}
Returns:
dict with the best prompt and its metrics
"""
# Step 1: Generate N candidates with different styles
styles = [
"very concise and direct",
"detailed with few-shot examples",
"with step-by-step reasoning instructions"
]
candidates = []
for i, style in enumerate(styles[:n_candidates]):
meta = f"""Create a prompt for this task: {task}
The prompt has to be: {style}
Return ONLY the prompt."""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": meta}],
temperature=0.4,
max_tokens=500
)
candidates.append({
"style": style,
"prompt": resp.choices[0].message.content.strip()
})
# If there's no golden set, return all the candidates
if not golden_set:
return {"candidates": candidates, "best": candidates[0], "no_evaluation": True}
# Step 2: Evaluate each candidate on the golden set
for candidate in candidates:
hits = 0
for example in golden_set:
# Use the candidate prompt to solve the example
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"{candidate['prompt']}\n\nInput: {example['input']}"
}],
temperature=0,
max_tokens=200
)
prediction = resp.choices[0].message.content.strip()
# Simplified evaluation: check whether expected is inside prediction
if example.get("expected", "").lower() in prediction.lower():
hits += 1
candidate["accuracy"] = hits / len(golden_set)
candidate["hits"] = hits
# Step 3: Select the best one
best = max(candidates, key=lambda c: c["accuracy"])
return {
"best_prompt": best["prompt"],
"best_accuracy": best["accuracy"],
"all_candidates": candidates
}
Self-Refine: Generate → Critique → Improve
The Refinement Loop
┌─────────────────────────────────────────────────────┐
│ SELF-REFINE LOOP │
│ │
│ Input → [GENERATE] → Initial answer │
│ ↓ │
│ [CRITIQUE] → "Problems: X, Y, Z" │
│ ↓ │
│ [REFINE] → Improved answer │
│ ↓ │
│ Good enough? ─── YES ──→ Final output │
│ │ │
│ NO │
│ └──→ [CRITIQUE] → ... │
└─────────────────────────────────────────────────────┘
Full Implementation
from dataclasses import dataclass
from typing import Callable, Optional
@dataclass
class RefineIteration:
"""Records one iteration of the self-refine loop."""
number: int
answer: str
critique: str
score_before: float
score_after: float
def critique_answer(
problem: str,
answer: str,
criteria: list[str] = None
) -> dict:
"""
Critiques an answer, identifying problems and areas for improvement.
Args:
problem: The original problem
answer: The answer to critique
criteria: List of specific criteria to evaluate against
Returns:
dict with 'critique', 'score', 'problems', 'can_improve'
"""
default_criteria = [
"factual accuracy",
"completeness (does it answer everything that was asked?)",
"clarity and structure",
"relevance to the problem"
]
used_criteria = criteria or default_criteria
criteria_str = "\n".join([f"- {c}" for c in used_criteria])
prompt = f"""Original problem: {problem}
Answer to evaluate:
{answer}
Evaluate this answer against these criteria:
{criteria_str}
Reply in JSON:
{{
"score": 0.0-1.0,
"problems": ["problem1", "problem2", ...],
"strengths": ["strength1", ...],
"concrete_improvements": ["specific improvement 1", "specific improvement 2", ...],
"can_improve": true/false
}}
Be specific about the improvements. If score >= 0.85, can_improve = false."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=400,
response_format={"type": "json_object"}
)
try:
critique = json.loads(response.choices[0].message.content)
return {
"score": float(critique.get("score", 0.5)),
"problems": critique.get("problems", []),
"strengths": critique.get("strengths", []),
"improvements": critique.get("concrete_improvements", []),
"can_improve": bool(critique.get("can_improve", True))
}
except (json.JSONDecodeError, KeyError) as e:
return {
"score": 0.5,
"problems": ["Error parsing the critique"],
"strengths": [],
"improvements": ["Reformat the answer"],
"can_improve": True
}
def refine_answer(
problem: str,
current_answer: str,
critique: dict
) -> str:
"""
Improves an answer based on a specific critique.
"""
improvements_str = "\n".join([f"- {m}" for m in critique.get("improvements", [])])
problems_str = "\n".join([f"- {p}" for p in critique.get("problems", [])])
prompt = f"""Original problem: {problem}
Your previous answer:
{current_answer}
Problems identified:
{problems_str if problems_str else "None critical"}
Specific improvements required:
{improvements_str if improvements_str else "Improve overall clarity"}
Generate an IMPROVED answer that:
1. Fixes every problem identified
2. Implements the suggested improvements
3. Keeps the strengths of the previous answer
4. Is more complete and accurate
Improved answer:"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=600
)
return response.choices[0].message.content.strip()
def self_refine(
problem: str,
max_iterations: int = 3,
score_threshold: float = 0.85,
criteria: list[str] = None,
verbose: bool = True
) -> dict:
"""
The full Self-Refine loop.
Args:
problem: The problem to solve
max_iterations: Maximum number of refine cycles
score_threshold: If score >= threshold, stop (it's already good enough)
criteria: Specific evaluation criteria
verbose: If True, prints the process
Returns:
dict with final_answer, iterations, final_score
"""
# Step 1: Generate the initial answer
initial_answer = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"Solve this problem in detail:\n\n{problem}"}],
temperature=0.3,
max_tokens=600
).choices[0].message.content.strip()
current_answer = initial_answer
iterations = []
if verbose:
print(f"[Iteration 0 - Initial]\n{current_answer[:200]}...\n")
# Refinement loop
for i in range(max_iterations):
# Critique the current answer
critique = critique_answer(problem, current_answer, criteria)
current_score = critique["score"]
if verbose:
print(f"[Critique iteration {i+1}]")
print(f" Score: {current_score:.2f}")
print(f" Problems: {critique['problems']}")
print(f" Improvements: {critique['improvements'][:2]}")
# If it's already good enough, stop
if not critique["can_improve"] or current_score >= score_threshold:
if verbose:
print(f" ✓ Score high enough ({current_score:.2f}). Stopping.")
iterations.append(RefineIteration(
number=i+1,
answer=current_answer,
critique=str(critique),
score_before=current_score,
score_after=current_score
))
break
# Refine the answer
improved_answer = refine_answer(problem, current_answer, critique)
post_critique = critique_answer(problem, improved_answer, criteria)
new_score = post_critique["score"]
if verbose:
print(f"[Refined iteration {i+1}]")
print(f" Score: {current_score:.2f} → {new_score:.2f}")
print(f" {improved_answer[:150]}...\n")
iterations.append(RefineIteration(
number=i+1,
answer=improved_answer,
critique=str(critique),
score_before=current_score,
score_after=new_score
))
# If the refinement made things worse, keep the previous version
if new_score < current_score - 0.05:
if verbose:
print(" ⚠ The refinement made the answer worse. Reverting.")
break
current_answer = improved_answer
return {
"final_answer": current_answer,
"initial_answer": initial_answer,
"iterations": iterations,
"n_iterations": len(iterations),
"final_score": critique["score"] if iterations else 0.0,
"total_improvement": critique["score"] - 0.5 # vs an assumed baseline of 0.5
}
Meta-Prompting + Self-Refine: The Combination
def meta_refine_pipeline(
task_description: str,
test_inputs: list[str],
n_meta_iterations: int = 2
) -> dict:
"""
Full pipeline: generate a prompt with meta-prompting,
then evaluate and refine the prompt using self-refine.
"""
# Step 1: Generate the initial prompt with meta-prompting
current_prompt = meta_prompt_generator(task=task_description)
print(f"Initial prompt:\n{current_prompt[:200]}...\n")
improvement_history = []
for iteration in range(n_meta_iterations):
# Step 2: Evaluate the prompt on the test inputs
results = []
for test_input in test_inputs[:3]: # Cap at 3 to save calls
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{current_prompt}\n\nInput: {test_input}"}],
temperature=0,
max_tokens=300
).choices[0].message.content
results.append({"input": test_input, "output": resp})
# Step 3: Use meta-prompting to improve the prompt
critique_prompt = f"""Task: {task_description}
Current prompt:
{current_prompt}
Results of using this prompt on real examples:
{json.dumps(results, ensure_ascii=False, indent=2)}
Identify problems in the prompt and generate an IMPROVED version.
Does the prompt produce consistent results? Is the format right? Is there ambiguity?
Return ONLY the improved prompt, with no explanations."""
improvement_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": critique_prompt}],
temperature=0.3,
max_tokens=600
)
improved_prompt = improvement_response.choices[0].message.content.strip()
improvement_history.append({
"iteration": iteration + 1,
"previous_prompt": current_prompt,
"new_prompt": improved_prompt,
"evaluation_results": results
})
current_prompt = improved_prompt
print(f"Iteration {iteration+1}: Prompt improved")
return {
"final_prompt": current_prompt,
"history": improvement_history,
"n_iterations": n_meta_iterations
}
Real Use Cases
Case 1: Improving entity-extraction prompts
task = "Extract named entities (people, organizations, places, dates) from English text"
texts = [
"Google's CEO, Sundar Pichai, visited Madrid on March 15, 2026.",
"The meeting between Apple and Samsung took place in Seoul yesterday.",
"Elon Musk founded SpaceX in 2002 in California."
]
result = meta_refine_pipeline(
task_description=task,
test_inputs=texts,
n_meta_iterations=2
)
print(f"\nFinal optimized prompt:\n{result['final_prompt']}")
Case 2: Self-Refine for Code
code_problem = """
Write a Python function that:
1. Takes a list of dictionaries with 'name', 'age', 'salary'
2. Filters employees with salary > 50000 and age > 30
3. Returns the list sorted by salary descending
4. Handles the empty-list case and missing keys
"""
result = self_refine(
problem=code_problem,
max_iterations=2,
score_threshold=0.9,
criteria=[
"correctness of the code (no bugs)",
"edge-case handling (empty list, missing keys)",
"efficiency (avoid unnecessary operations)",
"readability and documentation"
],
verbose=True
)
print(f"\n=== Final Code ===")
print(result["final_answer"])
print(f"\nScore improvement: +{result['total_improvement']:.2f}")
Case 3: Self-Refine for Educational Content
educational_problem = """
Explain the concept of 'transformer architecture' to someone who knows basic Python
but has never worked with ML/DL. Include an analogy and a simple code example.
"""
result = self_refine(
problem=educational_problem,
max_iterations=3,
criteria=[
"clarity for an audience that isn't technical in ML",
"an analogy that is understandable and accurate",
"runnable, simple code",
"logical progression of the concept"
],
verbose=True
)
Integration with Anthropic
import anthropic
anthropic_client = anthropic.Anthropic()
def self_refine_claude(problem: str, max_iter: int = 2) -> str:
"""
Self-Refine using Claude. Claude tends to be more self-critical.
"""
# Initial generation
initial_message = anthropic_client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=600,
messages=[{"role": "user", "content": f"Solve: {problem}"}]
)
answer = initial_message.content[0].text
for i in range(max_iter):
# Critique
critique_msg = anthropic_client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=300,
messages=[{
"role": "user",
"content": f"""Problem: {problem}
Your answer: {answer}
Identify 2-3 specific improvements that would make this answer more accurate and complete.
If it's already excellent (nothing significant to improve), just say "OPTIMAL"."""
}]
)
critique = critique_msg.content[0].text
if "OPTIMAL" in critique.upper():
break
# Refinement
refine_msg = anthropic_client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=600,
messages=[{
"role": "user",
"content": f"""Problem: {problem}
Previous answer: {answer}
Critique: {critique}
Generate an improved version that incorporates the critique."""
}]
)
answer = refine_msg.content[0].text
return answer
Troubleshooting
Problem 1: The meta-prompt generates vague or generic prompts
Symptom: The generated prompt could apply to any task, it isn't specific.
Causes:
- A very ambiguous task description
- No constraints or examples provided
Solution:
# BAD: vague description
bad_prompt = meta_prompt_generator("classify text")
# GOOD: specific description with context
good_prompt = meta_prompt_generator(
task="Classify technical support tickets into: CRITICAL_BUG, MINOR_BUG, FEATURE_REQUEST, QUESTION, DUPLICATE",
context="The tickets come from B2B software users, written in mixed English and Spanish (Spanglish)",
output_format="JSON: {category: str, confidence: float 0-1, reason: short str}",
constraints=[
"CRITICAL_BUG: only if it mentions data loss or a system outage",
"Detect whether there's implicit urgency in the language",
"If it's ambiguous, prefer QUESTION over the other categories"
]
)
Problem 2: Self-Refine doesn't improve anything (the critique is too permissive)
Symptom: The critic always gives score > 0.8 and says "can_improve = false" even when the answer is mediocre.
Causes:
- The critic uses the same model, which is "too kind to itself"
- The evaluation criteria are vague
Solution:
# Use a stricter critic with specific criteria and examples of what counts as "bad"
strict_critique = f"""Evaluate this answer CRITICALLY.
Be demanding: actively hunt for errors, omissions and inaccuracies.
Criterion: an answer of 0.9+ has to meet ALL of this:
- Not a single factual error
- Fully answers EVERY part of the question
- Clear and well structured
- No unnecessary redundancy
Problem: {problem}
Answer: {answer}
If there is ANY problem, even a small one, the score must be < 0.85."""
Problem 3: Every Self-Refine iteration makes the answer worse
Symptom: The answer gets longer and more confusing with each iteration.
Solution:
def self_refine_with_rollback(problem: str, max_iter: int = 3) -> dict:
"""Self-Refine that rolls back if an iteration makes things worse."""
current_answer = generate_initial(problem)
current_score = critique_answer(problem, current_answer)["score"]
best = {"answer": current_answer, "score": current_score}
for _ in range(max_iter):
critique = critique_answer(problem, current_answer)
if not critique["can_improve"]:
break
improved = refine_answer(problem, current_answer, critique)
new_score = critique_answer(problem, improved)["score"]
if new_score > best["score"]:
best = {"answer": improved, "score": new_score}
if new_score < current_score - 0.05:
# It got worse: roll back
break
current_answer = improved
current_score = new_score
return best
def generate_initial(problem: str) -> str:
return client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": problem}],
temperature=0.3, max_tokens=500
).choices[0].message.content
Exercises
Exercise 1: Meta-prompt for a spam classifier
Use meta-prompting to generate a prompt that classifies emails as SPAM or NO_SPAM. Then evaluate the generated prompt against 10 known examples. Does it beat 85% accuracy?
See solution
test_emails = [
("YOU WON $1,000,000! Click here to claim your prize.", "SPAM"),
("Team meeting tomorrow at 10am.", "NO_SPAM"),
("Special offer: Viagra at 90% off", "SPAM"),
("Your January invoice is available.", "NO_SPAM"),
("Dear user, your account will be blocked in 24h. Verify now.", "SPAM"),
("Can you review the PR I pushed yesterday?", "NO_SPAM"),
("Crypto trading bot - earn $500 a day effortlessly", "SPAM"),
("Attached is the Q4 quarterly report.", "NO_SPAM"),
("You have been selected for an exclusive offer.", "SPAM"),
("Do you have availability for a call on Friday?", "NO_SPAM"),
]
# Generate the prompt with meta-prompting
spam_prompt = meta_prompt_generator(
task="Classify emails as SPAM or NO_SPAM",
context="Emails can be in English or Spanish",
output_format="Reply only with: SPAM or NO_SPAM",
constraints=[
"Detect artificial urgency ('NOW', '24h', 'IMMEDIATELY')",
"Detect promises of easy money",
"Ignore legitimate work emails"
]
)
# Evaluate
hits = 0
for email, expected in test_emails:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{spam_prompt}\n\nEmail: {email}"}],
temperature=0
).choices[0].message.content.strip().upper()
if expected in resp:
hits += 1
print(f"Accuracy of the generated prompt: {hits/len(test_emails):.0%}")
Exercise 2: Self-Refine for an essay
Apply Self-Refine to improve a short essay (200 words) about "the impact of AI on employment". Define 3 quality criteria relevant to an academic essay. How many iterations does it take to converge?
See solution
essay_problem = """
Write a short essay (200 words) about the impact of AI on employment.
It must include: arguments for and against, data if you know any, and a balanced conclusion.
"""
essay_criteria = [
"Balanced argumentation: presents both perspectives with equal weight",
"Concrete data and examples backing up the claims",
"Clear structure: introduction, development with data, conclusion",
"Nuanced conclusion: neither excessively optimistic nor alarmist"
]
result = self_refine(
problem=essay_problem,
max_iterations=3,
score_threshold=0.88,
criteria=essay_criteria,
verbose=True
)
print(f"\n=== Final Essay (iter {result['n_iterations']}) ===")
print(result["final_answer"])
print(f"\nFinal score: {result['final_score']:.2f}")
Exercise 3: Compare Manual vs Automatic Meta-Prompting
Write, by hand, the best prompt you can for "summarize news articles in 3 key points". Then generate one with meta-prompting. Evaluate both on 5 real articles. Which one has better perceived quality?
See solution
# Manual prompt
manual_prompt = """Summarize the following news article in exactly 3 key points.
Format:
1. [Main fact]
2. [Context or cause]
3. [Impact or consequence]
Be concise (max 20 words per point)."""
# Prompt via meta-prompting
auto_prompt = meta_prompt_generator(
task="Summarize news articles in 3 key points",
output_format="A numbered list with 3 points, max 20 words each",
constraints=["Don't include the author's opinions", "Include numeric data if available"]
)
test_article = """Tesla announced today the construction of a new Gigafactory
in Spain, specifically in Valencia, with an investment of 4 billion euros.
The plant will employ 3,000 direct workers and is expected to start operating
in 2028. The Spanish government has offered tax incentives worth 500 million euros."""
def evaluate_summary(prompt: str, article: str) -> str:
return client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{prompt}\n\nArticle: {article}"}],
temperature=0
).choices[0].message.content
print("=== MANUAL ===")
print(evaluate_summary(manual_prompt, test_article))
print("\n=== META-PROMPTING ===")
print(evaluate_summary(auto_prompt, test_article))
Summary
- Meta-prompting: Use an LLM to generate the optimal prompt for another task. Useful when the prompt space is large or the domain is complex.
- Meta-prompting + evaluation: Generate N candidates, evaluate on a golden set, pick the best. More robust than a single candidate.
- Self-Refine: The iterative Generate → Critique → Improve loop. 1-3 iterations are usually enough before diminishing returns.
- A strict critic: The key to Self-Refine is having a critic that is genuinely demanding. A critic that's too permissive won't improve anything.
- Rollback: If an iteration makes the answer worse, revert to the best version so far.
- When to use it: Meta-prompting for system optimization, Self-Refine for critical answers that must be high quality.
Additional resources
- Self-Refine: Iterative Refinement with Self-Feedback (Madaan et al., 2023) - Original paper
- Large Language Models as Optimizers (Yang et al., 2023) - Prompts as optimization
- Automatic Prompt Engineer (APE) (Zhou et al., 2022) - Systematic meta-prompting
- Constitutional AI (Anthropic) - Self-critique for alignment
- OpenAI Prompt Engineering Guide
- Reflexion: Language Agents with Verbal Reinforcement - A self-refine variant