Vol. 34 · Prompting & Adaptation
DSPy & GEPA: Implementation Cookbook
A big volume about how to really build systems on DSPy: all the key abstractions from official documentation, a built-in module map, data design and metrics, the choice of an optimizer, deep dive by GEPA and step-by-step rollout plans from the first baseline to production.
Foundations – What is DSPy?
The official idea of DSPy is simple, but the implications are big: stop encoding the logic of the system in rows of prompts and raise the level of abstraction to software components. The key to quality here is not “magical prompts”, but decomposition, metrics and compile loop.
01foundationDSPy separates task from prompt strings
In conventional prompt engineering, task signature, formatting, reasoning strategy, few-shot examples, and model-specific hacks are mixed into one fragile text. DSPy lays this out in layers and allows you to change the layer without rewriting the entire system. That is why it is useful not only for optimization, but also for normal architecture.
textLM specific model and generation kwargs
Signature What Comes In and What Should Come Out
Module: Predict / CoT / ReAct / PoT
Adapter as a signature turns into messages and back
Metric is considered a good answer.
Optimizer How to Find the Best Instructions / Demos / Weights
Bottom line: code controls flow and prompts become compiled artifactWhen it has the maximum effect
- There are several LM steps that need to be supported.
- The model or provider will change.
- Improved quality through eval loop
- For one throwaway prompt it can be overkill
02design ruleFirst, control flow, then optimization.
DSPy’s official programming overview doesn’t start with optimizers, but with task design and pipeline. The right start: first decide what inputs and outputs the system needs, where retrieval or tools are needed, and then think about how to compile prompt internals.
text1. determine the external contract of the task
2. make the simplest working pipeline
3. collect characteristic dips in dev examples
4. describe metric.
5. Then turn on the optimizer.03lifecycleLifecycle project on DSP
The DSPy documentation clearly suggests a cyclical process: programme, assemble devset, define metrics, optimize, go back and change the pipeline itself if the metric or architecture is weak.
textExplore manual runs, 10-20 cases
↓
Evaluate devset + baseline metric
↓
Optimize few-shot / instruction / weights
↓
Re-design if metric is bad or pipeline is poorly decomposed
↓
Harden observability, cache, save/load, deployment04gepa slotWhere in this picture sits GEPA
GEPA does not replace a DSPy program, but sits on top of it as a reflective optimizer. Its task is not to invent the entire architecture for you, but to improve the text components of an existing program using score, feedback and execution traces.
What should exist before GEPA
- working DSPy program
- train/dev split
- A metric that can explain failure
- GEPA won't save chaotic system without eval loop
Core Abstractions - Basic Bricks
If you look at the official Learn + API, the central abstractions of DSPy are `LM`, `Signature`, `Module`, `Adapter`, `Example` and `Prediction`. Understanding these layers makes all optimizers and tutorials predictable.
05lmLM – One client over model, cache and metadata
dspy.LM specifies model and generation defaults. Documentation shows work with OpenAI, Anthropic, Gemini, Vertex, Databricks, Ollama, local OpenAI-compatible endpoints and other LiteLLM providers. LM is cached by default, stores call history, usage and cost metadata, and can switch via dspy.configure and dspy.context.
textlm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=lm)
with dspy.context(lm=other_lm):
...
generation kwargs: temperature / max_tokens / stop / cache
diverse rollouts: rollout_id + temperature>0
history: prompt / messages / response / usage / cost / model
reasoning models: model_type="responses"Practical conclusions
- Maintain baseline LM globally through 'configure'
- Use 'context' for experiments and A/B
- Rollout id is required for controlled diversity
- Changing 'rollout id' to 'temperature=0' is useless
06signatureSignature – a declarative contract of a task
DSPy: Signature is a declarative specification for input/output behavior. It's about describing what to do, not manually dictating models how to ask.
pythonInline: "question -> answer"
Typed inline: "context: list[str], question -> answer"
Class-based:
class Emotion(dspy.Signature):
"""Classify emotion."""
sentence: str = dspy.InputField()
sentiment: Literal[...] = dspy.OutputField()07fieldsInputField / OutputField / type system
Class-based signatures are needed when you want to clearly define the meaning of the task: docstring, field descriptions, output limitations and complex types. DSPy supports not only 'str' but also 'int', 'bool', 'list[str]', 'dict[...]', 'optional[...]', custom types and special types like 'dspy'. Image' and dspy. History.
When to switch from inline to class-based
- You need "Literal[...]" or structured output.
- You want to clearly describe the inputs/limitations
- Do metric as a DSPy program
- Don't ditch the field name by hand too soon
08dataExample & Prediction: Data and results in one semantics
The basic data container in DSPy is ‘Example’. It is similar to dict but knows which fields are inputs. 'Prediction' is a special subclass 'Example' that returns modules. Metric, eval and optimization work with the same semantics of objects.
textx = dspy.Example(question="Q", answer="A").with_inputs("question")
x.inputs() # only inputs
x.labels() # The rest as labels/metadata
pred = program(**x.inputs())
metric(x, pred)09moduleModule - the basic class of programs in DSPy
dspy.Module is an analogue of nn.Module for LM programs. Inside, you can store sub-modules, predictors, and your control flow. All serious DSPy systems, from multi-hop RAG to tool agents, end up with their own forward() classes.
pythonclass MyProgram(dspy.Module):
def __init__(self):
self.retrieve = dspy.Predict("question -> query")
self.answer = dspy.ChainOfThought("context, question -> answer")
def forward(self, question):
query = self.retrieve(question=question).query
context = search(query)
return self.answer(context=context, question=question)10adaptersAdapter – the layer between the signature and the real LM API
Officially, adapters are responsible for translating signature, demos, input data, and DSPy types into LM messages and parsing the response back to Prediction. They also manage history and function calls. By docs, `ChatAdapter' is used by default; `JSONAdapter' is good for native structured output and lower latency.
| Adapter | When to take | What to remember |
|---|---|---|
| ChatAdapter | A universal start for almost everything | More boilerplate tokens; docs say it can fallback in JSONAdapter |
| JSONAdapter | structured output + latency sensitive path | Need a model with native structured-output support |
| Selection process | `dspy.configure(adapter=...)' or `with dspy.context(adapter=...) * | First stabilize the signature, then change the adapter. |
Modules – How DSP implements behavior
Modules in DSPy abstract prompting technique, but remain compatible with any signature. This is what makes it portable: the same contract can be run through Predict, ChainOfThought, ReAct or ProgramOfThought without changing the external interfaces.
11predictPredict is the most fundamental module
The documentation says plainly: dspy. Predict is the basic module on top of which the rest are built. It simply displays input signature in outputs through the language model and does not extend the signature with additional reasoning fields.
Best start for
- classification
- field extraction
- short, normalized responses
- Hard reasoning is best to start with CoT
12cotChainOfThought - reasoning field over base signature
The API defines ‘ChainOfThought’ as a module that reasoning and step by step before predicting output. Learn docs further emphasize that in many cases, simply replacing 'Predict' with 'ChainOfThought' immediately improves quality.
textsignature: "document -> summary"
↓
CoT extends it to an additional reasoning field
↓
response.reasoning + response.summary13reactReAct — managed tool-using agent
dspy.ReAct implements the classic pattern "Reasoning and Acting": the model sees the list of tools, reason, decides to call the tool or complete the task. In DSPy, this pattern is generalized to arbitrary signatures, not just “question-> answer” chat agents.
textagent = dspy.ReAct(
"question -> answer",
tools=[search_web, get_weather],
max_iters=5,
)14potProgramOfThought: When LM Should Write Code
The official learn page summarises `ProgramOfThought` is as follows: LM learns to output code, and the result of execution of this code determines the answer. The API specifies that the module runs Python programs and requires runtime to be interpreted.
Best suited for
- formalization
- symbolism
- tasks where the answer is more convenient to get through the code
- For conventional extraction, this is too complicated.
15executionCodeAct, RLM and execution-heavy family
This is a family for cases when text reasoning is not enough. CodeAct is inherited from both ReAct and ProgramOfThought: it is essentially a tool agent with code interpreter. RLM is a standalone experimental abstraction for very large contexts, where LM explores data through sandboxed Python REPL and recursive sub-LLM calls instead of directly loading giant context into prompt.
| Module | Idea | When you need it. |
|---|---|---|
| CodeAct | tools + code interpreter | tool agent, where it is necessary not only to name the API, but also to process the result |
| RLM | recursive code exploration over large context | Long documents, context rot, data exploration through code |
| RLM caveat | Experimental+: Sandbox Runtime | Don’t take this first step if conventional RAG is already solving the problem. |
16controlBestOfN, Refine, MultiChainComparison, Parallel — inference-time control
Not all built-in modules are about reasoning style. Some of them manage the compute budget and execution policy. 'BestOfN' repeats the module N times and takes the best one by 'reward fn'. Refine does the same thing, but adds a feedback loop between attempts. MultiChainComparison brings several reasoning attempts into one final answer. Parallel is a utility for multithreaded pair launch (module, example).
textBestOfN different rollout id choose the best answer
Refine try to get feedback again
MultiChainComparison several reasoning paths
Parallel batch evaluation / batch execution on streamsData & Evals – Optimization doesn’t work without it
The official DSPy assessment philosophy is very correct: first devset, then metric, then baseline. Intermediate labels are almost never required. If you don’t know how to reliably say that there is a “good output”, no optimizer will save the situation.
17dataData model — inputs, intermediates, final labels
Learn docs emphasize three types of values on example: inputs, intermediate labels, and final label. Important practical conclusion: DSPy can often be used without intermediate labels and even without final labels, if the metric is able to measure quality by inputs and output.
Implications for Datasets Design
- Do not mark all the internal steps in advance.
- Start with at least a few representative inputs.
- Labels are needed where metrics depend on them.
- Do not confuse metadata with inputs in 'with inputs()'
18metricMetric design – the heart of DSPy and especially GEPA
The DSPy documentation literally says: framework machine-learning-like, so automatic metric is mandatory for both evaluation and optimization. For simple tasks, metric can be an ‘accuracy’/‘exact match’; for most production tasks, metric must be a small DSPy program or a function that checks several properties of long-form output.
pythondef metric(example, pred, trace=None):
score_1 = ...
score_2 = ...
if trace is None:
return (score_1 + score_2) / 2.0 # evaluation / optimization
else:
return score_1 and score_2 # bootstrap demosPractice
- Make metric simple first
- Iteratively improve it with the system
- Long-form output almost always requires composite metrics.
- Don’t optimise on a judge you don’t trust.
19evaluateEvaluate utility and baseline before optimizer
Before running a compile, you need to understand the baseline. Docs offer either a regular Python loop or a built-in dspy.evaluate.Evaluate that helps with multi-thread eval, progress bar and sample predictions table.
textevaluator = Evaluate(
devset=devset,
num_threads=1,
display_progress=True,
display_table=5,
)
evaluator(program, metric=metric)20splitsHow much data is needed and how to divide train / val / test
The official DSPy heuristics are good to start with: already 20 dev examples are useful, 200 go far; for optimizers, you can get value from 30 examples, but it is better to aim for 300+. For many, prompt optimizers docs recommend an atypical split `20% train / 80% val` because prompt optimization is retrained on a small trainset. For GEPA, the more classic ML approach is recommended: maximize the trainset and keep the val large enough to reflect downstream distribution.
| Phase | Practice | Why? |
|---|---|---|
| Exploration | 20+ dev examples | catching obvious failure modes |
| General prompt optimizers | 20% train / 80% val | More stable validation, less overfit |
| GEPA | More train, compact but representative val | reflection learns on richer training signal |
| Production | mandatory held-out test | There is no real uplift. |
Optimizer Map: What DSPy can compile
According to official optimization docs, DSPy optimizers are divided not by brands, but by the type of object being changed: few-shot demos, instructions, model weight, program transformations and meta-sequences. This is a handy map to choose from, not just a list of API names.
21optimizer mapFamilies of optimizers in DSPy
| Family | What changes | Top names in the API |
|---|---|---|
| Automatic few-shot learning | demonstration | LabeledFewShot, BootstrapFewShot, BootstrapFewShotWithRandomSearch, BootstrapRS, KNNFewShot |
| Automatic instruction optimization | instructions and sometimes demos | COPRO, MIPROv2, SIMBA, GEPA |
| Automatic finetuning | weights of underlying LMs | BootstrapFinetune |
| Program transformations | execution policy | Ensemble |
| Meta-optimizers | sequence of strategies | BetterTogether |
| Other specialized names in current API | special retrieval / rule / similarity helpers | InferRules, KNN, KNNFewShot |
22few-shotFew-shot family - when the main problem is good demos
Docs describe this family as an automatic signature extension of optimized examples. LabeledFewShot just picks up labeled demos. BootstrapFewShot uses the teacher program to generate demonstrations and filters them with metrics. 'BootstrapFewShotWithRandomSearch' amplifies search, and 'KNNFewShot' picks up close examples before bootstrap.
When to take
- Dataset is small but representative
- It depends on examples-in-context.
- If you want strict 0-shot, see MIPROv2/GEPA.
23instructionsInstruction family — COPRO, MIPROv2, SIMBA, GEPA
Copro makes coordinate-ascent over instructions. MIPROv2 combines instruction generation and few-shot search through Bayesian Optimization. SIMBA seeks complex examples through stochastic minibatches and generates self-reflective rules. GEPA analyzes trajectories and textual feedback. This family is worth taking when the main problem is not in the raw control flow, but in how the subtasks are formulated.
24weightsBootstrapFinetune, BetterTogether, Ensemble
‘BootstrapFinetune’ distill-it prompt-based DSPy program in weight updates: the program remains the same in structure, but the steps begin to be performed by finetuned models. BetterTogether combines prompt optimization and finetuning in a sequence like ‘prompt → weight → prompt’. Ensemble uses DSPy programs as one program transformation layer.
Where appropriate.
- There is a strong prompt baseline, but you need a cheaper runtime.
- latency/cost at scale
- Do not jump in finetune until metric and prompts are stabilized.
25selectionOfficial heuristics – which optimizer to take first
DSPy docs themselves give the starting heuristics. They are not a substitute for experimentation, but work well as the first decision tree.
textabout 10 examples
→ BootstrapFewShot
50+ examples
→ BootstrapFewShotWithRandomSearch
0-shot instruction optimization
MIPROv2 in 0-shot configuration
200+ examples and more budget optimization
→ MIPROv2
There is already a strong large model and you need an efficient small runtime.
→ BootstrapFinetune
Track-level feedback and rich failure analysis
→ GEPAGEPA Deep Dive – Reflective Optimization as It Is
GEPA is not just another prompt optimizer, but a reflective optimizer. It works on execution traces, can use predictor-level feedback, and has its own data split philosophy. For complex modular programs, this is often a qualitatively different mode of improvement compared to scalar-only search.
26core loopGEPA compile loop — score, traces, reflection, frontier
According to official GEPA docs and paper: the optimizer takes the seed candidate, runs it on the training signal, receives traces and numeric score, reflects what worked and what did not, generates a new instruction and selects candidates through the Pareto-style frontier. This is no longer a blind search for scalar reward, but a search for textual diagnoses.
textseed DSPy program
↓
run on minibatch / trainset
↓
metric(example, pred, trace, pred_name, pred_trace)
↓
score + feedback
↓
reflection LM writes lesson / new instruction
↓
candidate added to frontier
↓
new candidate selected by pareto or current_best27feedback apiMetric contract in GEPA — `trace`, `pred_name`, `pred_trace`
GEPA docs formalize the metric interface more strictly than a conventional eval loop. It can ask for feedback for a specific predictor. To do this, not only "gold" and "pred" arrive in metric, but also "trace", "pred name", "pred trace". If you have a multi-module system, this allows you to give local feedback to the step that is currently being optimized.
pythondef metric(gold, pred, trace=None, pred_name=None, pred_trace=None):
if pred_name is None:
return total_score
if pred_name == "categories_module.predict":
return dspy.Prediction(
score=total_score,
feedback="Missed one facility category and over-predicted another."
)Why is it important?
- multi-head extraction tasks
- RAG with separate retrieval and answer stages
- Repeated local failure modes agents
- If feedback is only global, GEPA will learn rougher
28knobsThe main hyperparameters of GEPA
The official GEPA API is not about the number of parameters, but how they change the search regime.
textauto = "light" | "medium" | "heavy"
reflection minibatch size = 3 # by doc default
candidate_selection_strategy = "pareto" | "current_best"
reflection lm = strong LM # docs directly suggest a strong reflection model
max_full_evals / max_metric_calls = budget caps29advancedGEPA Advanced Extensions
GEPA advanced docs describe custom `instruction proposer` and custom `component selector` separately. That is, you can separately decide which components to update and who will write the new instructions. For most tasks, docs are advised to start with a default proposer, and customization to include only when default is already understood.
When to Really Climb Advanced
- Multiple components are needed and selective update is required.
- non-standard instruction search format
- Default proposer is almost always sufficient
30searchGEPA as a batch inference-time search
GEPA can be used as a batch inference-time search strategy. To do this, transmit “valset=trainset”, include “track stats=True” and “track best outputs=True”, and then use “detailed results” and frontier best outputs.
textNot just compile prompts forever,
but also look for the best outputs on batch tasks,
When the price of additional search is justified by quality31failure modesWhen GEPA wins and when it breaks
| Script | Verdict | Why |
|---|---|---|
| Multi-module system with rich traces | GEPA is very appropriate. | You can locally explain which predictor is wrong and how. |
| enterprise extraction with predictor-level feedback | fit | The official tutorial shows such a regime. |
| scalar metric without explanation | Yes, but the value is lower. | reflection impoverished; search gets closer to blind optimization |
| failure | GEPA won't. | It polishes text components rather than redesigning pipelines. |
| No representative data | result will be fragile | Reflection learns from misallocation of tasks |
Cookbook – How to Start a Project Right
The most common mistake DSPy teams make is arguing too early about optimizers and designing the data/metric layer too late. Below is a plan for how to decompose the system in the beginning so that you can actually get sustainable quality through DSPy and GEPA.
32start planImplementation plan 0: 1: First 2-3 days of the project
The initial stage of DSPy should be super-pragmatic. The goal is not to “turn on compile right away,” but to build a system skeleton and understand its failure surface.
sqlDay 1
select an external contract
Write a 1-module baseline on Predict or CoT
Hands to run 15-20 representative cases
Day 2
devset
3-5 basic failure modes
metric
Day 3
baseline score
Decide whether to decompose 2-3 submodules
Then try the Optimizer.Anti-start pattern
- 7-step agent to baseline
- Build datasets without a list of failure modes
- Run GEPA before the first fair eval
33decompositionHow to Decompose a System in DSPy From the Beginning
Proper DSPy decomposition follows locally measured steps, not “how beautiful it sounds.” Each module must have a clear contract and a clear reason to exist. If a step cannot be separately explained or evaluated, it is usually a poor module boundary.
| A sign. | Good module. | Bad module. |
|---|---|---|
| Contract | clear input/output | do-it-yourself |
| Observability | You can look at the track and see what happened. | Error only appears at the end of the program. |
| Optimization | Predictor-level feedback | You cannot localize what to improve. |
34patternsPattern cookbook – where to start for typical tasks
| Task type | Starter module | First metric. | First Optimizer |
|---|---|---|---|
| Classification / routing | Predict | accuracy / macro-F1 | BootstrapFewShot or MIPROv2 |
| Structured extraction | CoT + class-based signature | field accuracy / exact match | GEPA if you have text feedback by field |
| RAG answerer | Module(retrieve → answer) | answer correctness + grounding | MIPROv2, then GEPA for multi-stage feedback |
| Tool agent | ReAct | task success + tool correctness | MIPROv2 or GEPA if trace-level errors are visible |
| Formal reasoning / math | ChainOfThought or ProgramOfThought | exact answer | GEPA / MIPROv2 |
| Huge context exploration | RLM | answer accuracy + exploration efficiency | only after baseline; not the first step of the project |
| Cheap production runtime | First, a strong prompted program | task success + latency/cost | BootstrapFinetune / BetterTogether |
35adoptionWhen to add GEPA to the roadmap
GEPA should be added not “when you heard about the new paper”, but when the team has already stabilized program boundaries and is able to give useful textual feedback. Prior to this, MIPROv2 or simple few-shot methods usually give more value per engineering hour.
textYou got a baseline?
You got devset?
Does metric correlate with human judgment?
Do you see local failure modes?
Can feedback be expressed in text?
If you say yes everywhere, GEPA is right.36maturityRollout on team maturity
DSPy should be implemented at maturity, not one big leap. If the team is building an LM system for the first time, it’s better to gradually add layers, otherwise complexity will explode before real quality comes along.
textStage 1 Predict / CoT + manual eval
Stage 2 Example / metric / Evaluate / devset
Stage 3 MIPROv2 / bootstrap optimizers
Stage 4 GEPA + predictor-level feedback
Stage 5 finetune / ensemble / prod hardening37pitfallsThe most common architectural errors when implementing DSPy
sqlWrite a huge agent immediately.
First, find out if 1-2 modules and retrieval are enough.
Measure only the aggregate score
not visible, retrieval, reasoning or formatting breaks the task
It's too early to sign words with your hands.
First let metric and optimizer show where the problem is.
Run GEPA without text feedback
The meaning of reflective optimization is lost.
Confuse devset and testset
Optimization becomes self-deceptionOps & Production to bring the system to work
DSPy docs cover not only the modeling layer, but also the real operational contour: tracing, optimizer tracking, save/load, cache, deployment, streaming, async. This is important because without observability, the compile loop quickly turns into a black box.
38observabilityDebugging & Observability – ‘Inspect history’ and Tracing
DSPy is able to show LM history through 'inspect history', but the docs themselves honestly state the limitations: this log covers only LM calls and poorly reflects the role of retrievers, tools and custom modules. For serious work, you need tracing. Official tutorials systemically recommend MLflow autologging for compiles, evals and traces.
sqlinspect_history
+ Quickly view text LM calls
You can't see the whole pipeline.
MLflow tracing / autologging
+ compiles
+ evals
+ traces from program execution
+ explainability under optimizationWhen to include
- From the first day of work optimization
- Required for GEPA and agents
- without traces team argues about quality blindly
39artifactsSave / load / cache - reproducibility of experiments
Compiled DSPy programs can be saved as state-only JSON or as a whole program directory via `save program=True' and then downloaded via `dspy.load'. The cache documentation separately emphasizes: LM calls are cached by default, prompt caching can be enabled from supported providers, and if necessary, you can write your own dspy.clients. Cache'.
textprogram code
compiled program state
dataset snapshot
metric version
LM / adapter config
optimizer run metadata40deployDeployment – FastAPI for simple, MLflow for more adult circuit
The official deployment tutorial describes two main paths: FastAPI for lightweight REST-serving and MLflow for more production-grade deployment with version and artifact management. Practically, this means: while the product is young, FastAPI is enough; as soon as you want to manage lifecycle compiled programs, tracing and versioning, move to MLflow-centered workflow.
Quick heuristics
- prototype / internal tool → FastAPI
- managed experiments / versioning → MLflow
- Recruitment without saved artifacts = pain in a week
41runtimeStreaming, async, usage tracking and production checklist
DSPy covers runtime concerns directly in tutorials. Streamify includes token streaming and intermediate status streaming. Asyncify and async patterns are needed when an I/O-bound system serves many concurrent requests. Usage tracking is available at the prediction level by enabling "track usage=True". This is the final production contour.
textStreaming
streamify + StreamListener
Only string fields are streamed
Async
sync: exploration / research / simpler debugging
async: concurrency / throughput / many requests
Usage
dspy.configure(track_usage=True)
prediction.get_lm_usage()
Before you sell, check:
baseline frozen
metric versioned
traces working
saved compiled artifacts
cache policy decided
deployment path chosen
rollback story existsNo dead end
Keep moving through the map.
Continue in sequence, switch to a related guide, or return to the seven-track learning map.