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.

01

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.

4 cards
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.

Diagramtext
LM 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 artifact

When 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.

Right order.text
1. 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.

Diagramtext
Explore 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, deployment
04gepa 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
02

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.

6 cards
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.

Key Opportunities of LMtext
lm = 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.

Signature shapespython
Inline: "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.

Minimum data contracttext
x = 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.

The skeleton of the custom programpython
class 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.

AdapterWhen to takeWhat to remember
ChatAdapterA universal start for almost everythingMore boilerplate tokens; docs say it can fallback in JSONAdapter
JSONAdapterstructured output + latency sensitive pathNeed 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.
03

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.

6 cards
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.

What's going on under the hoodtext
signature: "document -> summary"

CoT extends it to an additional reasoning field

response.reasoning + response.summary
13reactReAct — 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.

ReAct skeletontext
agent = 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.

ModuleIdeaWhen you need it.
CodeActtools + code interpretertool agent, where it is necessary not only to name the API, but also to process the result
RLMrecursive code exploration over large contextLong documents, context rot, data exploration through code
RLM caveatExperimental+: Sandbox RuntimeDon’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).

Diagramtext
BestOfN different rollout id choose the best answer
Refine try to get feedback again
MultiChainComparison several reasoning paths
Parallel batch evaluation / batch execution on streams
04

Data & 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.

4 cards
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.

Normal metrics in DSPypython
def 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 demos

Practice

  • 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.

Minimum baseline looptext
evaluator = 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.

PhasePracticeWhy?
Exploration20+ dev examplescatching obvious failure modes
General prompt optimizers20% train / 80% valMore stable validation, less overfit
GEPAMore train, compact but representative valreflection learns on richer training signal
Productionmandatory held-out testThere is no real uplift.
05

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.

5 cards
21optimizer mapFamilies of optimizers in DSPy
FamilyWhat changesTop names in the API
Automatic few-shot learningdemonstrationLabeledFewShot, BootstrapFewShot, BootstrapFewShotWithRandomSearch, BootstrapRS, KNNFewShot
Automatic instruction optimizationinstructions and sometimes demosCOPRO, MIPROv2, SIMBA, GEPA
Automatic finetuningweights of underlying LMsBootstrapFinetune
Program transformationsexecution policyEnsemble
Meta-optimizerssequence of strategiesBetterTogether
Other specialized names in current APIspecial retrieval / rule / similarity helpersInferRules, 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.

Diagramtext
about 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
  → GEPA
06

GEPA 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.

6 cards
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.

Diagramtext
seed 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_best
27feedback 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.

Metric shape for GEPApython
def 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.

What's the real first spin?text
auto = "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 caps
29advancedGEPA 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.

When it's useful.text
Not just compile prompts forever,
but also look for the best outputs on batch tasks,
When the price of additional search is justified by quality
31failure modesWhen GEPA wins and when it breaks
ScriptVerdictWhy
Multi-module system with rich tracesGEPA is very appropriate.You can locally explain which predictor is wrong and how.
enterprise extraction with predictor-level feedbackfitThe official tutorial shows such a regime.
scalar metric without explanationYes, but the value is lower.reflection impoverished; search gets closer to blind optimization
failureGEPA won't.It polishes text components rather than redesigning pipelines.
No representative dataresult will be fragileReflection learns from misallocation of tasks
07

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.

6 cards
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.

Diagramsql
Day 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.
Contractclear input/outputdo-it-yourself
ObservabilityYou can look at the track and see what happened.Error only appears at the end of the program.
OptimizationPredictor-level feedbackYou cannot localize what to improve.
34patternsPattern cookbook – where to start for typical tasks
Task typeStarter moduleFirst metric.First Optimizer
Classification / routingPredictaccuracy / macro-F1BootstrapFewShot or MIPROv2
Structured extractionCoT + class-based signaturefield accuracy / exact matchGEPA if you have text feedback by field
RAG answererModule(retrieve → answer)answer correctness + groundingMIPROv2, then GEPA for multi-stage feedback
Tool agentReActtask success + tool correctnessMIPROv2 or GEPA if trace-level errors are visible
Formal reasoning / mathChainOfThought or ProgramOfThoughtexact answerGEPA / MIPROv2
Huge context explorationRLManswer accuracy + exploration efficiencyonly after baseline; not the first step of the project
Cheap production runtimeFirst, a strong prompted programtask success + latency/costBootstrapFinetune / 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.

Normal readiness checktext
You 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.

Diagramtext
Stage 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 hardening
37pitfallsThe most common architectural errors when implementing DSPy
Don't do that.sql
Write 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-deception
08

Ops & 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.

4 cards
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.

Diagramsql
inspect_history
  + Quickly view text LM calls
  You can't see the whole pipeline.

MLflow tracing / autologging
  + compiles
  + evals
  + traces from program execution
  + explainability under optimization

When 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'.

What to keep as artifactstext
program code
compiled program state
dataset snapshot
metric version
LM / adapter config
optimizer run metadata
40deployDeployment – 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.

Diagramtext
Streaming
  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 exists

No dead end

Keep moving through the map.

Continue in sequence, switch to a related guide, or return to the seven-track learning map.