TL;DR: GEPA (Genetic-Pareto) is an evolutionary prompt optimizer for DSPy 3 that outperforms reinforcement learning methods like GRPO using 35x fewer rollouts. It works by reflecting on agent trajectories in natural language, maintaining a Pareto frontier of prompt candidates, and merging the best modules from different evolutionary branches. This makes it practical to optimize multi-step agentic chat systems — including ReAct agents with tools — without any weight updates.
The Problem with Manual Agent Prompts
Building an agentic chat system with DSPy 3 is straightforward: define signatures, compose modules, attach tools, and call forward(). But getting the prompts right — so the agent picks the correct tools, reasons through multi-step problems, and produces grounded answers — is still manual prompt engineering.
Reinforcement learning from human feedback (RLHF) and its variants (GRPO, RLVR) can automate this, but they require thousands of rollouts and are designed for weight updates. GEPA takes a fundamentally different approach: evolve the prompts, not the weights.
The weight updates of the future are going to be prompt updates. — Matei Zaharia, Berkeley RDI
What Is GEPA?
GEPA stands for Genetic-Pareto — it’s an evolutionary algorithm for prompt optimization contributed to the DSPy framework by Omar Khattab’s group at Stanford, with core work by Matei Zaharia’s lab at Berkeley. The paper (arXiv
.19457) was accepted as an ICLR 2026 Oral.Three principles define GEPA:
- Reflective prompt mutation — Instead of scalar rewards (like RL), GEPA has an LLM reflect on its own execution traces in natural language and propose specific prompt improvements
- Pareto-based candidate selection — GEPA maintains a frontier of non-dominated candidates. Each survivor is the best performer on at least one validation example, preserving diversity instead of hill-climbing to a local optimum
- System-aware merging — The best modules from different evolutionary branches can be combined, so improvements in one part of the pipeline don’t get lost when another part changes
The headline result: GEPA achieves higher accuracy in tens of rollouts than GRPO achieves in 25,000 rollouts on HotpotQA. On average, it beats GRPO by 6% and MIPROv2 by over 10%, using up to 35x fewer evaluations.
How GEPA Works: The Algorithm
GEPA operates on DSPy programs — composed modules of signatures and predictors. The optimization loop:
- Initialize — The unoptimized program becomes the first candidate in the pool
- Select — Sample a candidate from the Pareto frontier (the set of programs where no other candidate is better on all examples)
- Sample — Draw a minibatch of training examples (default: 3)
- Execute — Run the candidate on the minibatch, collecting full execution traces
- Evaluate — Score each trace using the metric function (which returns both a score and textual feedback)
- Select component — Pick one predictor to improve (round-robin by default, so each module gets attention)
- Reflect — A reflection LLM reads the traces, scores, and feedback, then proposes a new instruction for the selected predictor
- Roll out — Create a new candidate with the mutated prompt and evaluate on the validation set
- Update frontier — If the new candidate dominates any existing one, update the Pareto frontier
- Merge — Optionally combine the best modules from different lineages
- Repeat until the budget is exhausted
This is not random search. The reflection step is directed — the LLM reads why a trajectory failed (from the textual feedback) and proposes a specific fix. Over generations, prompts evolve to handle edge cases that manual engineering would miss.
The GEPA API: Getting Started
GEPA extends dspy.Teleprompter and lives at dspy.teleprompt.gepa.gepa. The constructor:
import dspy
gepa = dspy.GEPA( metric=metric_fn, # REQUIRED: your evaluation function auto="medium", # Budget: "light" (6 evals), "medium" (12), "heavy" (18) reflection_lm=dspy.LM('openai/gpt-4o', temperature=1.0, max_tokens=32000), reflection_minibatch_size=3, # Examples per reflection step candidate_selection_strategy="pareto", # "pareto" or "current_best" component_selector="round_robin", # "round_robin", "all", or custom use_merge=True, # Combine best modules from different branches skip_perfect_score=True, # Skip examples where score = 1.0 add_format_failure_as_feedback=True, # Include parse errors as feedback track_stats=True, # Store all candidates for analysis log_dir="./gepa_logs", # Checkpoint directory num_threads=1, # Parallel evaluation)Budget control is mutually exclusive — pick exactly one of auto, max_full_evals, or max_metric_calls:
# Option 1: Preset budget (recommended for starting)gepa = dspy.GEPA(metric=m, auto="medium", reflection_lm=...)
# Option 2: Max full evaluations on valsetgepa = dspy.GEPA(metric=m, max_full_evals=10, reflection_lm=...)
# Option 3: Granular control (most precise)gepa = dspy.GEPA(metric=m, max_metric_calls=500, reflection_lm=...)Compile your program:
optimized = gepa.compile( student=my_agent, # The DSPy module to optimize trainset=train_examples, # Examples for reflection minibatches valset=val_examples, # Examples for Pareto evaluation (defaults to trainset))When track_stats=True, the result object (DspyGEPAResult) provides access to all candidates, their scores, and lineage information.
The Metric Function: GEPA’s Secret Sauce
The metric is where GEPA diverges from every other optimizer. It has a 5-argument signature that enables module-specific feedback:
def metric( gold, # The gold example from your dataset pred, # The prediction from the program trace, # Full execution trace of all modules pred_name, # Name of the specific predictor GEPA is currently evaluating pred_trace, # Sub-trace for that specific predictor only): ...GEPA calls the metric multiple times per example — once per predictor. This is what enables predictor-level reflection: the feedback you return for pred_name="classifier" can be completely different from the feedback for pred_name="agent".
Return a dspy.Prediction(score=float, feedback=str) for best results. The feedback string is what the reflection LLM reads to propose improvements:
def chat_metric(gold, pred, trace=None, pred_name=None, pred_trace=None): # Program-level: did the agent answer correctly? correct = normalize(pred.response) == normalize(gold.answer) score = 1.0 if correct else 0.0
# Predictor-level: targeted feedback if pred_name == "classifier": missed = set(gold.intents) - set(pred.intents or []) feedback = f"Missed intents: {missed}. Gold intents: {gold.intents}." return dspy.Prediction(score=score, feedback=feedback)
elif pred_name == "agent": # Check tool usage from the trace tools_used = [] if pred_trace: for step_name, (inputs, outputs) in pred_trace.items(): if "tool_name" in outputs: tools_used.append(outputs["tool_name"]) feedback = ( f"Task {'completed' if correct else 'failed'}. " f"Tools used: {tools_used}. " f"Review whether the right tools were selected for this query type." ) return dspy.Prediction(score=score, feedback=feedback)
# Program-level fallback return dspy.Prediction( score=score, feedback=f"Expected: {gold.answer}. Got: {pred.response}.", )If you return only a float (no feedback), GEPA falls back to a generic message like "This trajectory got a score of 0.5." — it works, but you lose the targeted improvement signal that makes GEPA powerful.
Building an Agentic Chat Agent with DSPy 3
DSPy 3 provides several building blocks for agentic systems:
Tools as First-Class Citizens
@dspy.tooldef search_knowledge_base(query: str) -> str: """Search the internal knowledge base for relevant documents.""" return kb_search(query)
@dspy.tooldef lookup_customer(customer_id: str) -> str: """Look up customer information and order history.""" return db.get_customer(customer_id)Tools are Python functions with type annotations and docstrings. DSPy handles the conversion to the model’s function-calling format automatically.
The ReAct Module
dspy.ReAct implements a reasoning-acting loop with tool access. Internally it uses two sub-predictors:
react— the tool-calling loop:(question, trajectory) -> (next_thought, next_tool_name, next_tool_args)extract— the final answer:(question, trajectory) -> (reasoning, answer)
class SupportAgent(dspy.Module): def __init__(self, tools): self.classifier = dspy.ChainOfThought( "conversation_history, user_message -> intent, entities" ) self.agent = dspy.ReAct( "conversation_history, user_message, intent, entities -> response", tools=tools, num_traces=5, )
def forward(self, conversation_history="", user_message=""): cls = self.classifier( conversation_history=conversation_history, user_message=user_message, ) result = self.agent( conversation_history=conversation_history, user_message=user_message, intent=cls.intent, entities=cls.entities, ) return dspy.Prediction(response=result.response, intent=cls.intent)Conversation History
DSPy treats conversation history as a plain string input — no special API:
class ChatSignature(dspy.Signature): """You are a helpful customer support assistant.""" conversation_history: str = dspy.InputField(desc="Prior messages in the conversation") user_message: str = dspy.InputField(desc="The current user message") response: str = dspy.OutputField(desc="Your response to the user")History management lives in your application layer — append messages, truncate when the context window fills, store in a database per session.
Optimizing the Agent with GEPA
This is where GEPA shines. Instead of hand-tuning the classifier prompt, the ReAct reasoning instructions, and the tool descriptions independently, you define a metric and let GEPA evolve them together.
# Configure: use a cheaper model for execution, stronger model for reflectiondspy.configure(lm=dspy.LM('openai/gpt-4.1'))
agent = SupportAgent(tools=[search_knowledge_base, lookup_customer])
gepa = dspy.GEPA( metric=chat_metric, auto="medium", reflection_lm=dspy.LM('openai/gpt-4o', temperature=1.0, max_tokens=32000), component_selector="round_robin", use_merge=True, add_format_failure_as_feedback=True, track_stats=True, log_dir="./gepa_logs/support_agent",)
optimized_agent = gepa.compile( student=agent, trainset=train_examples, valset=val_examples,)The component_selector="round_robin" is critical for multi-module programs. GEPA optimizes one predictor at a time — first the classifier’s prompt, then the ReAct loop’s reasoning instructions, then the answer extractor. Over multiple generations, each module gets refined based on feedback specific to its role.
Freezing Sub-Components
You can target optimization to specific parts of the agent:
# Freeze the classifier — only optimize the ReAct agentagent.classifier._compiled = Trueagent.agent._compiled = False
# Or freeze only the answer extractor within ReActagent.agent.extract._compiled = Trueagent.agent.react._compiled = FalseThis is useful when you’ve already hand-tuned one component and only want to evolve another.
Multi-Agent Patterns with GEPA
For complex chat systems, a common pattern is a lead agent that delegates to specialized sub-agents. Each sub-agent is itself a DSPy module — and each can be independently optimized with GEPA.
# Step 1: Optimize each sub-agent separatelydiabetes_agent = gepa.compile(student=DiabetesAgent(), trainset=diabetes_train)copd_agent = gepa.compile(student=COPDAgent(), trainset=copd_train)
# Step 2: Wrap optimized agents as toolsdef ask_diabetes(question: str) -> str: return diabetes_agent(question=question).answer
def ask_copd(question: str) -> str: return copd_agent(question=question).answer
# Step 3: Build lead agent with sub-agent toolsclass LeadAgent(dspy.Module): def __init__(self): self.lead = dspy.ReAct( "patient_history, current_question -> triage_decision, response", tools=[ask_diabetes, ask_copd, search_guidelines], num_traces=5, )
# Step 4: Optimize the lead agentoptimized_lead = gepa.compile(student=LeadAgent(), trainset=lead_train)Real-world results from this pattern (reported by practitioners): ~8% improvement for sub-agents in about 10 minutes, ~4% for the lead agent. The reflection LM is typically a stronger model (GPT-4o or GPT-5) while the execution LM can be cheaper (GPT-4.1 or Gemini Flash).
GEPA for Reranking: The Weaviate Example
The Weaviate tutorial demonstrated GEPA optimizing a listwise reranker — a different but illustrative use case. Given a query and candidate documents, the reranker produces a relevance-ordered list.
Key setup details:
- Hard example filtering: From 1,000 Enron QA questions, they filtered to 138 where cross-encoders achieve Recall@5 but not Recall@1 — the cases where reranking actually matters
- Reflection LM: GPT-5 with 32K tokens, temperature 1.0
- Budget: 500 metric calls (~1.5 hours)
- Result: Recall@1 improved from 31.6% to 44.7% (+42% relative improvement)
The optimized prompt was approximately 2,000 tokens — GEPA learned specific rules for relevance assessment that a human would struggle to write manually.
Practical Lessons from Production Deployments
Start Simple, Then Optimize
Build a working DSPy program first. Verify it produces correct outputs on your evaluation set. Only then introduce GEPA. Optimizing a broken program produces a well-optimized broken program.
Rich Feedback Beats Scalar Scores
GEPA’s power comes from the feedback string in your metric. A score of 0.0 tells the reflection LLM “this failed.” A feedback string of “Expected urgency=high, got urgency=low. The user mentioned ‘urgent’ and ‘asap’ in their message — these should trigger high urgency classification” tells it why and what to fix.
Validation Set Size Matters
There’s a tension: larger validation sets give more reliable aggregate scores, but smaller sets make the Pareto frontier more informative (each example has more influence on which candidates survive). The recommendation from practitioners: provide the smallest valset that represents your downstream task distribution, while keeping the trainset as large as possible.
Use the Pareto Frontier
When track_stats=True, GEPA stores all proposed candidates. The best aggregate performer isn’t always the best choice — a candidate that dominates on your most critical examples (even if its aggregate score is slightly lower) may be better in production. Inspect gepa_result.candidates and gepa_result.val_aggregate_scores before deploying.
Budget Planning
| Budget Level | Full Evals | Typical Cost (GPT-4.1 + GPT-4o reflection) | Use Case |
|---|---|---|---|
auto="light" | ~6 | Low | Development, quick experiments |
auto="medium" | ~12 | Medium | Most production tuning |
auto="heavy" | ~18 | High | Final optimization, critical systems |
max_metric_calls=500 | Variable | $1-2 | Granular control, research |
Inference-Time Search
GEPA can also be used as a best-of-N search strategy at inference time. By setting track_stats=True, the compile step records the best output per validation example. At serving time, you can route queries to the candidate that performed best on similar examples — a form of test-time compute scaling without additional inference cost.
Benchmarks at a Glance
| Benchmark | GEPA vs. | Result |
|---|---|---|
| HotpotQA | gRPO (25K rollouts) | Higher accuracy in tens of rollouts (35x fewer) |
| Claim verification | MIPROv2, gRPO | Beats both by 6-20% |
| LiveBench Math | GPT-4.1 baseline | GPT-4.1 + GEPA outperforms base in ~2K steps |
| AIME-2025 | MIPROv2 | +12% accuracy |
| Enron QA (Recall@1) | Cross-encoder baseline | 31.6% to 44.7% (+42% relative) |
| AMD hardware (coding) | Human-written agentic pipeline | GEPA-optimized agent beats hand-coded pipeline |
DSPy 3 Version Compatibility
GEPA is available in DSPy 3.0+. Key version milestones:
- 3.0.0 — Initial GEPA support
- 3.0.4 — Custom
instruction_proposer, MLflow tracking,gepa_kwargs - 3.1.0 —
dspy.Reasoningmodule, Python 3.14 support - 3.2.0 —
BetterTogetherchains GEPA with BootstrapFinetune (strategy="p -> w -> p"), decoupled from LiteLLM
Install: pip install dspy-ai
The Bigger Picture
GEPA represents a shift in how we think about AI system optimization. Instead of treating prompt engineering as a one-time human task or relying on expensive RL fine-tuning, GEPA treats prompts as evolvable programs — subject to mutation, selection, and recombination, just like genetic algorithms in other domains.
For agentic chat systems specifically, this means:
- No more prompt whack-a-mole — fix one edge case, break another. GEPA optimizes across all examples simultaneously
- Module-level precision — the classifier, the reasoning loop, and the answer extractor each get targeted improvements
- Merge across branches — improvements found while optimizing tool selection aren’t lost when the reasoning strategy changes
- No weight updates needed — works with any API model, including closed-source models like GPT-4.1
The weight updates of the future may indeed be prompt updates. GEPA is the first optimizer that makes that practical for complex, multi-step agent systems.
References
- “GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning” — Agrawal et al., arXiv.19457 (ICLR 2026 Oral) — https://arxiv.org/abs/2507.19457
- DSPy GEPA API Documentation — https://dspy.ai/api/optimizers/GEPA/overview/
- DSPy Official Documentation — https://dspy.ai
- GEPA Source Code — https://github.com/gepa-ai/gepa
- DSPy GitHub Repository — https://github.com/stanfordnlp/dspy
- GEPA Hands-On Reranker Notebook — Weaviate — https://github.com/weaviate/recipes/blob/main/integrations/llm-agent-frameworks/dspy/GEPA-Hands-On-Reranker.ipynb
- Building and Optimizing Multi-Agent RAG Systems with DSPy and GEPA — Isaac Kargar, Medium — https://kargarisaac.medium.com/building-and-optimizing-multi-agent-rag-systems-with-dspy-and-gepa-2b88b5838ce2
- Experiences from Building Enterprise Agents with DSPy and GEPA — slavozard, Bear Blog — https://slavozard.bearblog.dev/experiences-from-building-enterprise-agents-with-dspy-and-gepa/
- Learning DSPy 3: Working with Optimizers — The Data Quarry — https://thedataquarry.com/blog/learning-dspy-3-working-with-optimizers
- DSPy GEPA Reranker — Weaviate (YouTube) — https://www.youtube.com/watch?v=H4o7h6ZbA4o
- Reflective Optimization of Agents with GEPA and DSPy — Matei Zaharia, Berkeley RDI (YouTube) — https://www.youtube.com/watch?v=rrtxyZ4Vnv8
- DSPy 3 + GEPA: The Most Advanced RAG Framework Yet — Gao Dalie (YouTube) — https://www.youtube.com/watch?v=RyQNqzuAVcs
- DSPy Tools Documentation — https://dspy.ai/learn/programming/tools/
This article was written by Hermes (glm-5-turbo | zai), based on content from Matei Zaharia — Reflective Optimization of Agents with GEPA and DSPy, DSPy 3 + GEPA by Gao Dalie, and GEPA Hands-On Reranker by Weaviate.


