Move beyond manual prompt editing. Use DSPy to search prompt and few-shot candidates from data, then release only after held-out evaluation.
Model merging showed one way to adapt a model without full retraining: combine existing checkpoints, evaluate the candidate, and deploy only after it clears release gates. DSPy applies the same discipline to prompts. Instead of hand-editing instructions until they feel right, you define a prompt program, measure it against examples, compile it offline, and release only a tested artifact.
A developer assistant that answers CI and deploy-policy questions can start with hand-tuned instructions, but test logs, runbook wording, and model revisions keep changing. A prompt that worked last month may regress after any one of those changes.
DSPy[1] treats prompts as tunable program state. You define modules and a metric, provide examples of good incident-resolution behavior, and let a DSPy optimizer search for instructions and demonstrations that score well on development data. A separate holdout test and production monitoring still determine whether the compiled candidate is worth deploying.
The compiler analogy is useful with a limit: a DSPy program defines a relatively stable interface and control flow, while compilation searches prompt instructions and demonstrations for one configured LM and metric. Unlike a software compiler, DSPy compilation is empirical optimization. It can overfit its data and doesn't prove correctness.
Instead of writing prompts, write programs. A program defines:
Then let an optimizer propose prompt instructions and few-shot examples for your chosen model and metric, and compare the compiled candidate with a baseline on held-out data.
A signature defines the task without specifying how to prompt. It abstracts away the string formatting and acts like a Python function signature for an LLM call: you declare the inputs and outputs, but you don't write the body.
This runnable code uses both inline signatures for quick experiments and class-based typed signatures for production. The examples stay with one deploy-policy assistant so the pieces fit together:
1import dspy
2
3# Simple signatures (inline)
4classify = dspy.Predict("ticket_text -> urgency: str")
5qa = dspy.Predict("policy_context, ticket_text -> resolution: str")
6
7# Typed signatures (recommended for production)
8class GenerateResolution(dspy.Signature):
9 """Generate an incident resolution using retrieved policy documents."""
10 policy_context: str = dspy.InputField(desc="Retrieved policy passages")
11 ticket_text: str = dspy.InputField(desc="The engineer's message")
12 resolution: str = dspy.OutputField(desc="Concise resolution with policy citation")
13
14print("inline inputs:", ", ".join(classify.signature.input_fields.keys()))
15print("inline outputs:", ", ".join(classify.signature.output_fields.keys()))
16print("typed inputs:", ", ".join(GenerateResolution.input_fields.keys()))
17print("typed outputs:", ", ".join(GenerateResolution.output_fields.keys()))1inline inputs: ticket_text
2inline outputs: urgency
3typed inputs: policy_context, ticket_text
4typed outputs: resolutionThe practical shift is that you stop writing the full prompt text by hand. DSPy derives the initial prompt scaffolding from the signature docstring and field descriptions, then optimizers can rewrite instructions and demos during compilation.
Modules compose signatures into larger components. They wrap signatures, hold state such as demonstrations, and define execution flow. This separates program structure from compiled prompt state. If you swap a language model or change a signature, keep the structure where appropriate, then recompile and evaluate rather than assuming the previous artifact transfers.
Build a policy pipeline that takes an engineer's ticket plus policy context and generates a resolution with a Chain-of-Thought signature. In production, a retrieval system can fill policy_context; for the core optimization example, keeping context explicit makes the program easier to test. The logic flows like standard Python code:
1import dspy
2
3class GenerateResolution(dspy.Signature):
4 """Generate an incident resolution using retrieved policy documents."""
5 policy_context: str = dspy.InputField(desc="Retrieved policy passages")
6 ticket_text: str = dspy.InputField(desc="The engineer's message")
7 resolution: str = dspy.OutputField(desc="Concise resolution with policy citation")
8
9class PolicyPipeline(dspy.Module):
10 def __init__(self):
11 super().__init__()
12 self.generate_resolution = dspy.ChainOfThought(GenerateResolution)
13
14 def forward(self, ticket_text: str, policy_context: str) -> dspy.Prediction:
15 return self.generate_resolution(
16 policy_context=policy_context,
17 ticket_text=ticket_text,
18 )
19
20pipeline = PolicyPipeline()
21predictors = dict(pipeline.named_predictors())
22signature = predictors["generate_resolution.predict"].signature
23
24print("predictors:", ", ".join(predictors.keys()))
25print("module inputs:", ", ".join(signature.input_fields.keys()))
26print("module outputs:", ", ".join(signature.output_fields.keys()))1predictors: generate_resolution.predict
2module inputs: policy_context, ticket_text
3module outputs: reasoning, resolutionNotice that GenerateResolution only declares resolution. dspy.ChainOfThought(GenerateResolution) prepends a reasoning field automatically, so you keep the base signature focused on the task interface and let the module add a generated rationale. That rationale isn't the same thing as the DSPy execution trace introduced later: the trace records predictor calls, inputs, and outputs while the program runs.
Optimization requires a clear signal to know if changes improve the pipeline. Metrics evaluate the system's output by returning a numerical score (often 0.0 to 1.0). Unlike traditional machine learning where metrics compare floats, LLM metrics often require semantic evaluation to judge text quality.
A metric function can be as simple as an exact string match or as complex as a separate LLM acting as an automated judge. Because an optimizer can run the metric repeatedly, it must strike a balance between accuracy and computational cost.
This example shows a metric for our policy pipeline. It combines a strict exact-match shortcut for tiny tests with a simple policy-faithfulness check that verifies the answer cites information from the provided context. Both arguments are ordinary DSPy objects: an example from your dataset and a prediction from your program.
1import re
2import dspy
3
4def resolution_accuracy(example: dspy.Example, prediction: dspy.Prediction, trace=None) -> float:
5 """Metric: does the predicted resolution match the gold resolution?"""
6 if prediction.resolution.strip().lower() == example.resolution.strip().lower():
7 return 1.0
8 return 0.0
9
10def policy_faithfulness(example: dspy.Example, prediction: dspy.Prediction, trace=None) -> float:
11 """Metric: does the resolution cite a policy and reuse context facts?"""
12 resolution = prediction.resolution.lower()
13 context = example.policy_context.lower()
14 cites_policy = "policy" in resolution
15 mentions_approval = bool(re.search(r"owner approval|approval from the owner", resolution))
16 context_supports_approval = "owner approval" in context
17 return 1.0 if cites_policy and mentions_approval and context_supports_approval else 0.0
18
19def policy_metric(example: dspy.Example, prediction: dspy.Prediction, trace=None) -> float:
20 return 0.5 * resolution_accuracy(example, prediction) + 0.5 * policy_faithfulness(example, prediction)
21
22example = dspy.Example(
23 ticket_text="The payment tests failed after the deploy candidate.",
24 policy_context="Deploy Policy: failing payment tests block release until rollback owner approval.",
25 resolution="Per the deploy policy, block the release until rollback owner approval.",
26).with_inputs("ticket_text", "policy_context")
27
28good = dspy.Prediction(resolution="Per the deploy policy, block the release until rollback owner approval.")
29bad = dspy.Prediction(resolution="Retry the deploy and watch the dashboard.")
30
31print(f"good score: {policy_metric(example, good):.1f}")
32print(f"bad score: {policy_metric(example, bad):.1f}")1good score: 1.0
2bad score: 0.0For open-ended incident resolutions, you may add an LLM-as-a-judge metric when deterministic checks are too narrow. Judge metrics can vary across runs and can prefer verbose answers or be influenced by output ordering.[2] Lower-temperature decoding may reduce sampling variance but doesn't remove systematic bias. Before optimizing against a judge, calibrate it on held-out human-labeled examples, document an acceptance threshold appropriate to the task, and keep deterministic policy checks for critical requirements.
| Feature | Traditional Prompting | DSPy Optimization |
|---|---|---|
| Effort | Manual string tweaking | Programmatic logic |
| Portability | Hard to move between models | Recompile for each target model |
| Evaluation | Often ad hoc unless instrumented | Explicit metric and dataset split |
| Few-shotting | Hand-picked examples | Optimizer-selected or bootstrapped candidates |
| Versioning | Ad-hoc prompt changes | Compiled, versioned artifacts |
| Iteration | Manual comparisons | Search plus held-out verification |
Moving from manual prompting to programmatic optimization is best understood through one concrete pipeline. The deploy-policy resolver above can be written from signature to compiled artifact.
Start by declaring what the model should do, not how. Our GenerateResolution signature says: "Give me a ticket and some policy context, and I'll produce a resolution." If you want explicit reasoning, choose a module like dspy.ChainOfThought and let it extend the signature for you. Don't write any prompt text yet.
Combine DSPy modules to create your pipeline. Chain together ChainOfThought, Predict, retrieval modules, or ordinary Python control flow like stacking layers in PyTorch. The structure reflects your logic, not your prompt strings. Our PolicyPipeline receives policy passages from the dataset or retrieval layer, then generates a resolution.
Write a function that scores your pipeline's output quality. It should return a number between 0 and 1. Good metrics measure the behavior you care about (accuracy, faithfulness, style alignment), not string similarity alone. For our deploy-policy pipeline, we might combine resolution_accuracy and policy_faithfulness into a single composite score.
Start with representative examples that cover important developer-assistant slices: CI failures, rollback approvals, break-glass access, and adversarial requests. Sample count is an experimental question; expand or rebalance the set when slice metrics show instability or missing coverage.
Keep separate train, validation, and test splits. Use training examples for bootstrapping, validation examples for selecting a compiled candidate, and test examples only for the promotion decision. MIPROv2 can construct a validation split when you omit one, but an explicit split makes leakage and optimization budget easier to audit.
1train_ids = {"ci-001", "rollback-001", "access-001"}
2validation_ids = {"ci-101", "rollback-101"}
3test_ids = {"ci-201", "rollback-201"}
4
5def assert_disjoint_splits(*splits: set[str]) -> None:
6 for index, left in enumerate(splits):
7 for right in splits[index + 1:]:
8 overlap = left & right
9 if overlap:
10 raise ValueError(f"leaked examples: {sorted(overlap)}")
11
12assert_disjoint_splits(train_ids, validation_ids, test_ids)
13
14try:
15 assert_disjoint_splits(train_ids, validation_ids, {"rollback-001"})
16except ValueError as exc:
17 print("leakage_rejected:", "rollback-001" in str(exc))
18
19print("clean_split_sizes:", [len(train_ids), len(validation_ids), len(test_ids)])1leakage_rejected: True
2clean_split_sizes: [3, 2, 2]Run an optimizer such as BootstrapFewShot or MIPROv2 on your development machine or training instance. It searches instruction and demonstration candidates, then returns the best candidate it observed under the selection metric. That candidate may tie or lose to the uncompiled baseline on a held-out test set, so measure both before deployment. Compilation consumes LM calls; rerun it when the model, metric, adapter, or task distribution changes.
Save either the compiled program state to JSON or the full program as a serialized artifact. In production, load the right form and run inference. There's no optimizer loop at runtime; serving uses the optimized prompts and demos.
The optimizer is the engine that searches alternative program state. Older DSPy papers and internals often call this a teleprompter, but current docs center the term optimizer. It takes your program, data, and metric, and outputs a compiled candidate with selected parameters such as instructions and demonstrations.
Prompt optimization behaves like hyperparameter search over prompt programs. Suppose you have 20 deploy-policy tickets in your validation set. The optimizer might try:
It scores candidates on validation tickets and keeps the version with the highest observed validation score. A final test comparison protects against selecting a prompt that happened to fit validation examples unusually well.
Formally, we want to find the parameter set that maximizes the expected metric over a dataset :
Where represents program parameters (instructions and demonstrations), is typically a held-out validation set, and is the metric we optimize (for example, accuracy or F1 score, which balances precision and recall).
This is a discrete optimization problem, not gradient descent. The parameters include natural-language instruction strings and sets of few-shot examples, neither of which expose a gradient through a hosted LM call. DSPy optimizers use methods such as random search, surrogate-guided search, or reflective evolution depending on the optimizer selected. Cost depends on candidate count, evaluation set, metric, and configured LMs.
Try different instruction and demonstration configurations , run the resulting program on examples from the evaluation set , score each output with , and pick , the configuration with the highest average score. In practice, the expectation is estimated with the sample mean over the evaluation set.
1candidates = {
2 "baseline": {"validation": [0.7, 0.7], "test": [0.8, 0.8]},
3 "compiled_a": {"validation": [1.0, 0.9], "test": [0.6, 0.7]},
4}
5
6def mean(values: list[float]) -> float:
7 return sum(values) / len(values)
8
9selected = max(candidates, key=lambda name: mean(candidates[name]["validation"]))
10baseline_test = mean(candidates["baseline"]["test"])
11selected_test = mean(candidates[selected]["test"])
12
13print("validation_winner:", selected)
14print("selected_test_score:", round(selected_test, 2))
15print("promote_over_baseline:", selected_test >= baseline_test)1validation_winner: compiled_a
2selected_test_score: 0.65
3promote_over_baseline: FalseThe optimizer search loop runs from program and examples to a compiled artifact. Validation picks a candidate inside the optimizer; holdout and release gates still decide promotion.
BootstrapFewShot runs a teacher program (often the same unoptimized DSPy program, using the same configured LM) and turns metric-accepted execution traces into demonstrations.
metric.If successful traces exist, BootstrapFewShot can place accepted traces into a candidate program's demonstrations. Compare the compiled result with the uncompiled program; bootstrapping isn't itself a quality guarantee. This runnable snippet prepares the optimizer without making an LM request. The commented compile call is the boundary that requires a configured LM:
1import dspy
2
3def resolution_accuracy(example, prediction, trace=None):
4 return float(example.resolution == prediction.resolution)
5
6trainset = [
7 dspy.Example(
8 ticket_text="The payment tests failed after the deploy candidate.",
9 resolution="Per the deploy policy, block the release until rollback owner approval.",
10 policy_context="Deploy Policy: failing payment tests block release until rollback owner approval.",
11 ).with_inputs("ticket_text", "policy_context"),
12]
13
14optimizer = dspy.BootstrapFewShot(
15 metric=resolution_accuracy,
16 max_bootstrapped_demos=4,
17 max_labeled_demos=16,
18)
19
20# After dspy.configure(lm=...), compilation makes LM calls:
21# compiled = optimizer.compile(PolicyPipeline(), trainset=trainset)
22print("train_examples:", len(trainset))
23print("max_bootstrapped_demos:", optimizer.max_bootstrapped_demos)
24print("max_labeled_demos:", optimizer.max_labeled_demos)1train_examples: 1
2max_bootstrapped_demos: 4
3max_labeled_demos: 16MIPROv2 is DSPy's documented optimizer for jointly searching instruction text and few-shot examples. It uses grounded instruction proposal plus surrogate-model-guided search over candidate prompt configurations.[3]
This runnable example exercises the manual-control guard without performing optimization. In MIPROv2 manual mode, if you provide num_candidates yourself, set auto=None and pass num_trials to compile(...). MIPROv2 also requires configured prompt and task LMs before a real compile run:
1import dspy
2
3def metric(example, prediction, trace=None):
4 return 1.0
5
6# Construction does not send a request; compile would use these configured LMs.
7lm = dspy.LM("openai/not-called-in-this-example", api_key="unused")
8
9optimizer = dspy.MIPROv2(
10 metric=metric,
11 prompt_model=lm,
12 task_model=lm,
13 auto=None,
14 num_candidates=4,
15)
16student = dspy.Predict("ticket_text -> resolution")
17trainset = [
18 dspy.Example(
19 ticket_text="My rollback approval is stuck.",
20 resolution="Check rollback approval status.",
21 ).with_inputs("ticket_text"),
22]
23
24try:
25 optimizer.compile(student, trainset=trainset)
26except ValueError as exc:
27 print("missing_num_trials_rejected:", "num_trials" in str(exc))
28print("manual_candidate_count:", optimizer.num_candidates)1missing_num_trials_rejected: True
2manual_candidate_count: 4MIPROv2 uses optuna during its compile search path. On machines that run this optimizer, install dspy[optuna].
If you leave valset=None, the current implementation constructs one from the provided trainset. That convenience path can change with DSPy releases and can't replace a declared test set. Pass explicit train, validation, and test splits for comparisons you intend to deploy.
In traditional prompt engineering, the evaluation function often only sees the final string output of the language model. If a multi-step prompt outputs a correct answer, the system may assume success even if an intermediate query rewrite or reasoning step was poor.
DSPy supports this through execution traces collected during optimization. Trace-aware optimizers can use successful predictor calls for demonstration bootstrapping, while trace-aware metrics can provide intermediate feedback. For user-facing prompt or response inspection, use LM history rather than depending on private trace object shapes.
Trace collection supports program-level debugging and bootstrapping:
BootstrapFewShot finds a successful execution path, it can reuse that successful trace as few-shot supervision for future runs.That makes LM pipelines more inspectable during optimization, while metrics and held-out tests remain responsible for quality decisions.
Choosing the right optimizer depends on your data availability, latency constraints, and the acceptable cost of compilation. When beginning a new project, you might not have enough examples to run complex optimizers. Starting with LabeledFewShot gives you a baseline from provided labeled examples.
As you collect representative examples, you can compare BootstrapFewShot against your baseline. This optimizer uses your LM and metric to accept useful traces. When joint search over instructions and demos is worth additional compile budget, evaluate MIPROv2.
| Optimizer | Best For | Mechanism | Typical Setup | Cost |
|---|---|---|---|---|
| LabeledFewShot | Baseline with labeled demos | Samples or inserts provided examples | Small labeled training set | Low |
| BootstrapFewShot | Trace-based demo candidate | Bootstraps metric-accepted traces into demos | Representative training examples | Medium |
| BootstrapFewShotWithRandomSearch / BootstrapRS | Better demo search | Scores many candidate demo sets | Explicit validation split helps | High |
| MIPROv2 | Joint instruction and demo search | Grounded proposals plus surrogate-guided search | Separate validation set plus more metric budget | Very High |
| GEPA | Reflection-based instruction search | Reflective rewrites from traces, Pareto candidate selection | Reflection LM plus feedback-aware metric | Very High |
The optimization process itself calls the LLM repeatedly, which incurs API costs and takes time. Balance the compilation cost against the expected improvement in inference accuracy.
Current DSPy docs also list GEPA, a reflective prompt-evolution optimizer, and BootstrapFinetune for adapting model weights rather than only prompt parameters. The examples stay focused on BootstrapFewShot, BootstrapRS, and MIPROv2 because they expose demo bootstrapping and instruction search directly.
GEPA uses a reflection LM and textual feedback from execution outcomes to propose instruction revisions, while its Pareto selection can retain candidates that perform differently across objectives.[4] The paper reports strong results against its evaluated reinforcement-learning baselines, but that result doesn't select an optimizer for a new application. GEPA needs a suitable reflection LM and a feedback-aware metric, so compare its measured gains and compile cost with simpler optimizers.
Current DSPy exposes two save/load patterns, and mixing them up is a common production mistake.
program.save("./optimized_policy.json", save_program=False) writes program state such as signatures, demonstrations, and module settings to a JSON file. You must recreate compatible Python program structure before calling .load(...).program.save("./optimized_policy/", save_program=True) serializes architecture and state together into a directory. You later restore a trusted artifact with dspy.load("./optimized_policy/", allow_pickle=True).State-only JSON is safer and easier to review. Whole-program loading is more convenient, but it uses cloudpickle to serialize the program, so treat the saved directory like executable code and only load trusted artifacts.
1from pathlib import Path
2from tempfile import TemporaryDirectory
3import dspy
4
5program = dspy.Predict("ticket_text -> resolution")
6
7with TemporaryDirectory() as temp_dir:
8 artifact = Path(temp_dir) / "policy_state.json"
9 program.save(str(artifact), save_program=False)
10
11 restored = dspy.Predict("ticket_text -> resolution")
12 restored.load(str(artifact))
13
14 print("state_file_created:", artifact.exists())
15 print("restored_outputs:", ", ".join(restored.signature.output_fields.keys()))1state_file_created: True
2restored_outputs: resolutionFor a trusted whole-program artifact, the corresponding API is compiled_policy.save("./optimized_policy_v3/", save_program=True) followed by dspy.load("./optimized_policy_v3/", allow_pickle=True). Because that path deserializes a cloudpickled program, only load an artifact you trust and version.
Compilation invokes LMs repeatedly and belongs outside the request path. Its cost depends on optimizer settings, models, metric calls, and dataset size. Separate compile time from serving time in production:
Offline compilation and low-latency production execution stay separate:
Use the state-only JSON path from the previous section for ordinary deployments. Reserve whole-program serialization for trusted internal artifacts where convenience is worth the cloudpickle loading risk.
1baseline = {"ci_failure": 0.84, "rollback": 0.91}
2compiled = {"ci_failure": 0.95, "rollback": 0.83}
3minimum = {"ci_failure": 0.84, "rollback": 0.88}
4
5failed = {
6 task: score
7 for task, score in compiled.items()
8 if score < minimum[task]
9}
10improved_average = sum(compiled.values()) > sum(baseline.values())
11
12print("improved_average:", improved_average)
13print("failed_release_gates:", sorted(failed))
14print("promote:", not failed)1improved_average: True
2failed_release_gates: ['rollback']
3promote: FalseA stable DSPy program interface lets you run a new compilation experiment when you swap underlying models without rewriting all orchestration logic. Because instructions and few-shot examples can be model-specific, recompilation is a candidate-generation step, not evidence that a smaller or cheaper LM meets the old release gates.
For example, you might prototype the deploy-policy pipeline on a stronger hosted model, then recompile the same DSPy program for a smaller open-weight model that your serving stack can run.
The optimizer can search new few-shot examples and instruction phrasing for the new model. That's prompt-state optimization, not weight distillation or a substitute for evaluating the target LM.
When swapping models, don't assume an old compiled artifact transfers. Instruction phrasing, adapter behavior, demonstrations, and provider revisions can all change measured output.
Maintain a compilation matrix: for each program version and exact target-model revision, store and evaluate its artifact separately. A provider-side model update is a new comparison, even when the model alias is unchanged.
1artifacts = {
2 ("policy-v3", "hosted-model@2026-04-01"): "policy-v3_hosted-2026-04-01.json",
3 ("policy-v3", "local-model@rev7"): "policy-v3_local-rev7.json",
4}
5
6def artifact_for(program_version: str, model_revision: str) -> str:
7 key = (program_version, model_revision)
8 if key not in artifacts:
9 raise KeyError("compile and evaluate an artifact for this model revision")
10 return artifacts[key]
11
12print("known_artifact:", artifact_for("policy-v3", "local-model@rev7"))
13try:
14 artifact_for("policy-v3", "local-model@rev8")
15except KeyError:
16 print("uncompiled_revision_rejected:", True)1known_artifact: policy-v3_local-rev7.json
2uncompiled_revision_rejected: TrueFor complex deploy-policy tickets that span multiple policies, DSPy's modularity can represent multi-step retrieval. This sketch requires a configured search function and LM, so it's architecture pseudocode rather than an offline runnable example:
1class MultiHopPolicy(dspy.Module):
2 def __init__(self, search, num_hops=3):
3 super().__init__()
4 self.search = search
5 self.generate_query = [dspy.ChainOfThought("context, ticket_text -> search_query") for _ in range(num_hops)]
6 self.generate_resolution = dspy.ChainOfThought(GenerateResolution)
7
8 def forward(self, ticket_text: str) -> dspy.Prediction:
9 context = []
10 for hop_module in self.generate_query:
11 search_query = hop_module(context="\n".join(context), ticket_text=ticket_text).search_query
12 policies = self.search(search_query, k=3)
13 context.extend(policies)
14
15 return self.generate_resolution(policy_context="\n".join(context), ticket_text=ticket_text)Each query-generation module can expose its own prompt state to optimization. Whether optimizing multiple hops improves retrieval and final answers requires trace inspection and held-out evaluation; a larger search surface can also spend more compile budget.
Check these failure modes early because each can consume compile budget without producing a deployable artifact.
Symptom: Your metric uses exact string comparison on long-form text, so the optimizer gives up because almost no output ever scores 1.0.
Cause: You're treating generation like classification. A resolution that says "Run rollback after owner approval" and one that says "Rollback approved: run it now" may mean the same thing in context, but exact match gives them a 0.
Fix: Use an LLM-as-judge metric, or at least a heuristic like "Does the resolution mention the relevant policy and required approval?" Exact match is fine for short labels (urgency levels), but it's the wrong tool for open-ended generation.
Symptom: You delay compiling because you think you need thousands of labeled tickets.
Cause: You're importing habits from traditional deep learning, where more data usually helps. DSPy optimizers search over prompt configurations, not model weights.
Fix: Start with a small, representative split, measure uncertainty and slice coverage, then add examples where evaluation shows gaps. Don't claim a required sample count before measuring the task.
Symptom: The compiled pipeline works on your test set, but fails strangely in production and you can't explain why.
Cause: You didn't inspect the optimizer output. You don't know which few-shot examples it selected or how it rewrote the instructions.
Fix: After compilation, inspect prompt history with the documented dspy.inspect_history(n=...) helper or the configured LM's history method. If selected examples are edge cases or instructions are misleading, revise training coverage or the metric and rerun the comparison.
Some DSPy failures don't look like prompt failures at first:
num_candidates or num_trials to MIPROv2 while leaving auto enabled. Manual mode requires auto=None, num_candidates on the optimizer, and num_trials during compile(...).MIPROv2. The optimizer imports optuna during compilation, so compile hosts should install dspy[optuna]..load(...).dspy.load(...) restores a cloudpickled program, so treat it like executable code.Use these questions to check whether you can reason about DSPy beyond syntax.
How does MIPROv2 differ from simple few-shot selection?
MIPROv2 jointly optimizes instruction text and demonstration sets. BootstrapFewShot mainly bootstraps demos from successful traces. MIPROv2 searches a larger space by proposing grounded instructions, pairing them with demo candidates, and using surrogate-guided search over measured candidate programs.[3]
Why is model coupling a serious issue in traditional prompt engineering? A prompt can become overfit to one model family. When you move to a new provider, checkpoint, tokenizer, adapter, or decoding setup, the old wording may degrade. DSPy doesn't remove that risk, but it makes the migration controlled: keep the program interface and metric, then recompile for the target model.
What does the trace parameter give a metric?
It can expose intermediate predictor calls rather than only the final answer. That lets a metric grade a generated search query, a reasoning field, or a policy-selection step inside a multi-stage program. Exact trace internals can change across DSPy versions, so production metrics should depend on documented behavior and local tests, not private object shapes.
How are state-only JSON artifacts different from whole-program saves?
program.save("./artifact.json", save_program=False) stores state. You recreate the Python module class, then call .load(...). program.save("./artifact_dir/", save_program=True) stores the program with cloudpickle, then dspy.load("./artifact_dir/", allow_pickle=True) restores it. JSON is safer and easier to review. Whole-program loading is convenient but only belongs in trusted environments.
Confirm you can:
Answer every question, then check your score. Score above 75% to mark this lesson complete.
10 questions remaining.
DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines.
Khattab, O., et al. · 2023 · arXiv preprint
Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena.
Zheng, L., et al. · 2023 · NeurIPS 2023
Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs
Opsahl-Ong, K., et al. · 2024
GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning
Agrawal, L. A., Tan, S., Soylu, D., et al. · 2025
Questions and insights from fellow learners.