Skip to main content
A DSPy optimizer needs a program and a metric. For an agent, the program is a dspy.ReAct module whose instructions contain every tool description, which means that rewriting the instructions rewrites the tool descriptions too. The metric runs one scenario against the candidate and scores the verdict, with a penalty for every step over the budget, where a step is one reply or one tool call, and GEPA additionally takes the judge’s reasoning as text feedback for the next candidate. The runnable example is in the SDK repository: sdks/python/examples/agent-optimization.

What you need

  • Python 3.10 or later. In the example folder, uv sync installs everything. In your own project, pip install "langwatch[dspy]" "dspy[optuna]" langwatch-scenario (MIPROv2 needs the optuna extra), and import scenario before dspy in every module, because in the other order dspy 3.3 fails while loading numpy.
  • OPENAI_API_KEY for the agent, the simulated user and the judge, and LANGWATCH_API_KEY so the optimization run and the simulations appear in LangWatch.
  • Five to ten scenarios. One optimization run executes each of them several times: the MIPROv2 run of the example below took 64 scenario runs, 42 minutes and $0.93, with gpt-5-mini as the agent, the simulated user and the judge, and gpt-5 writing the instruction candidates.

Step 1: Wrap the agent as a DSPy module

The agent is a dspy.ReAct over the tools, and a scenario adapter passes the conversation to it as dspy.History plus the last user message and returns the answer:
trajectory_to_messages turns the ReAct trajectory into tool_calls and tool messages followed by the answer, so the judge sees the tool calls and the rejected arguments as well as the reply; both helpers are in the example’s agent.py. The tool descriptions come from the docstrings: dspy.ReAct writes them into the instructions of its react predictor, and that predictor is what the optimizer rewrites.

Step 2: One example per scenario

Each dspy.Example is one scenario: the situation, the criteria and a step budget, where a rejected tool call that the agent retries counts as an extra step.

Step 3: The program runs the scenario, the metric scores it

The optimizer calls the program on each example and then the metric on the result, so the outer program runs the scenario with the candidate agent inside it and returns the verdict, and the metric only has to score:
A failed scenario scores zero, and a passed scenario scores one minus half a point per budget of extra steps, so the pass gate comes first and the step budget is the objective under it. When a tool rejected its arguments during the conversation, the example’s feedback adds one line that says so, which is the hint GEPA needs to rewrite that tool’s description. scenario.run executes the conversation on its own thread, and GEPA reads the trace of the thread that called the program, so the example’s forward copies the DSPy trace of the scenario thread back into the calling thread.

Step 4: Run the optimizer

max_metric_calls=48 bought four iterations on the example: the first full pass costs six calls, and each accepted candidate costs a minibatch of three for the parent, three for the candidate and a full pass of six. GEPA’s auto="light" budget plans hundreds of metric calls, which is too many when every call is a full simulation.
Run the suite once before and once after with dspy.Evaluate(devset=trainset, metric=scenario_metric, num_threads=3), so the before and after pass rates and steps come from the same scenarios; the example’s evaluate_suite prints that table. To run the example:

Step 5: Read the result in LangWatch

  • Experiments shows the optimization run as a score chart, one point per trial, with the instructions of each predictor at that step. A MIPROv2 run appears through langwatch.dspy.init, and the GEPA example logs each candidate through a callback in optimize_gepa.py. See DSPy visualization.
  • Agent Testing shows every simulation the optimizer ran, as runs of the dspy-optimization set. The example puts the baseline pass and the final pass in their own batches, named <run id>-baseline and <run id>-best, so they appear as two runs of six next to the larger runs from inside the optimizer, and the scripts print both names.

The MIPROv2 run in Experiments: one point per trial, the best trial marked, and the instructions of the selected step below the chart.

The dspy-optimization set in Agent Testing: the baseline and best passes of both runs as runs of six, next to the runs from inside the optimizers.

Compare the instructions before and after: both scripts print every instruction the optimizer changed, and the tool descriptions are part of them.

What the example runs showed

One run of each script on the six scenarios, with the agent, the simulated user and the judge on gpt-5-mini and gpt-5 as the proposer or the reflection model: Neither optimizer saw the tool code. Both saw the rejected calls in the traces, and both wrote the accepted values into the react instructions. MIPROv2, from the proposer’s candidates:
GEPA, from the judge’s reasoning and the tool errors in the feedback, on its first iteration:
That is the same tool description fix as in Fix tool calls, found by search. On both runs the damaged blender return went from failed to passed and from 5 steps to 4. The scenario where the customer asks for a recipe failed on every pass, and the cause was in the example rather than in the agent: its criterion said the agent calls no tool, and the judge counts ReAct’s internal finish step as a tool call. The criterion now names the tools it means. On the MIPROv2 run the angry customer passed before and failed after, and the 83% of the best trial did not repeat on the final pass, which is the non-determinism described in the Limits below.

Limits

  • The optimizer changes text, meaning the instructions and the tool descriptions. A tool that needs a different schema, a missing tool or a harness cap stays a code change: see Fix tool calls.
  • A scenario is not deterministic: the simulated user words the situation differently on every run, and a candidate can pass once and fail once. Evaluate the baseline and the final program twice before you compare them, and use --repeat on the platform for the same reason.
  • The score counts the agent’s steps, so the cost of the simulated user and the judge is not in it.
  • DSPy is Python. A TypeScript agent runs the guided loop instead: see Reduce turns.
Also check: Optimization algorithms (which optimizer to pick), Scenarios in code, DSPy visualization (how a run appears in Experiments).
Last modified on September 4, 2026