Which Predictor? How GEPA Optimizes a Multi-Agent DSPy Program | Elicited
Go back<br>Which Predictor? How GEPA Optimizes a Multi-Agent DSPy Program<br>9 Jul, 2026<br>| Suggest Changes<br>A multi-agent program is several prompts in a trench coat.
AI-generated entry. See What & Why for context.
In my last deep dive on GEPA, I traced the search algorithm: per-example Pareto frontiers, specialists evolving into generalists, mini-batch sampling, and how the metric’s feedback teaches the reflection LLM what to fix.
But I skipped a question that only shows up when you optimize a program with more than one predictor.
GEPA talks about optimizing “components.” DSPy talks about “predictors.” When I run GEPA on a three-agent program, how does GEPA know which of my agents it is looking at in a given round? And when it produces a better prompt, how does that prompt get back to the right agent?
For a single-predictor program you never think about this. For a multi-agent program, it is the whole game. So I traced it through the source.
The punchline first: there is no mapping table. DSPy and GEPA share one dictionary, and the predictor’s name is the component’s name. Everything else follows from that.
The example: a three-agent support resolver
Let me make this concrete. Here is a small DSPy program that turns a support ticket into a drafted reply. Three agents, each a dspy.ChainOfThought.
A side note on the word “agent.” These three are not agents in the strict sense: no tool use, no loop, no autonomy. They are just predictors in a fixed pipeline, and I am calling them agents to keep the example readable. But the GEPA story does not depend on that simplification. Swap any of them for a real dspy.ReAct agent and the picture only widens: a ReAct module is itself built from predictors — one that drives the reasoning-and-tool-call loop, and one that extracts the final answer — so it contributes two keys to the candidate dict, not one. GEPA still does the same thing to each: every predictor is its own named component, selected and rewritten one at a time. A multi-agent system is just a program with more predictors, and GEPA sees all of them as keys in the same dict.
import dspy
class TriageTicket(dspy.Signature):<br>"""Classify a support ticket.""" # ← agent 1's instruction<br>ticket = dspy.InputField()<br>category = dspy.OutputField(desc="one of: billing, bug, how-to, account")<br>priority = dspy.OutputField(desc="P0, P1, or P2")
class WriteKBQuery(dspy.Signature):<br>"""Write a search query for the help-center knowledge base.""" # ← agent 2<br>ticket = dspy.InputField()<br>category = dspy.InputField()<br>query = dspy.OutputField()
class DraftReply(dspy.Signature):<br>"""Draft a customer-facing reply.""" # ← agent 3's instruction<br>ticket = dspy.InputField()<br>category = dspy.InputField()<br>articles = dspy.InputField(desc="retrieved KB articles")<br>reply = dspy.OutputField()
class SupportResolver(dspy.Module):<br>def __init__(self, kb):<br>super().__init__()<br>self.kb = kb<br>self.triage = dspy.ChainOfThought(TriageTicket)<br>self.write_query = dspy.ChainOfThought(WriteKBQuery)<br>self.compose = dspy.ChainOfThought(DraftReply)
def forward(self, ticket):<br>t = self.triage(ticket=ticket)<br>q = self.write_query(ticket=ticket, category=t.category)<br>articles = self.kb.search(q.query, k=3)<br>r = self.compose(ticket=ticket, category=t.category, articles=articles)<br>return dspy.Prediction(category=t.category, priority=t.priority, reply=r.reply)<br>The thing to notice: each agent’s instruction is just its signature’s docstring. That string is the only thing GEPA will ever change.
What GEPA actually starts from
When you call compile(), the first thing GEPA does is ask the module for its predictors and snapshot their instructions into a plain dict. This is the seed candidate .
# dspy/teleprompt/gepa/gepa.py<br>seed_candidate = {name: pred.signature.instructions<br>for name, pred in student.named_predictors()}
# {<br># "triage.predict": "Classify a support ticket.",<br># "write_query.predict": "Write a search query for the help-center knowledge base.",<br># "compose.predict": "Draft a customer-facing reply.",<br># }<br>That dictionary is the entire interface between DSPy and GEPA. Its keys are the predictor names straight from named_predictors(). GEPA calls each key a component and treats it as an opaque label. It never inspects the string, never parses the name, never builds a lookup table.
On its side, GEPA just records the keys once:
# gepa/src/gepa/core/state.py<br>self.list_of_named_predictors = list(seed_candidate.keys())<br># ["triage.predict", "write_query.predict", "compose.predict"]<br>That list is GEPA’s complete idea of “the things I am allowed to edit.” Everything downstream refers to agents by these strings.
The metric does double duty
GEPA cannot improve a prompt from a bare number. It needs to know why an output was bad, in words. So a GEPA metric returns a dspy.Prediction carrying both a score and a feedback string. (I wrote more about why the feedback matters as much as the score in the previous GEPA...