LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnApplied LLM EngineeringPrompt Optimization with DSPy
✍️MediumPrompt Engineering

Prompt Optimization with DSPy

Search DSPy instructions and few-shot demos on frozen deploy-answerer cases, then promote only a compiled artifact that beats the baseline on held-out gates.

19 min read
Learning path
Step 80 of 196 in the full curriculum
Experiment Tracking with MLflow and W&BModel Versioning & Deployment

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The last lesson left deploy-answerer-v1.2-fix-b eligible for canary review after it passed a frozen suite. At 09:17, a canary request like req_205 changes the wording of the rollback question; the hand-edited template flips from ABSTAIN to SERVE without a citation while latency and HTTP errors stay green. That is a prompt regression, not a useful signal to an on-call engineer. The next question is whether you can search instructions and few-shot demos against those same frozen cases, instead of rewriting the string until it feels right.

DSPy[1]Reference 1DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines.https://arxiv.org/abs/2310.03714 turns that release problem into a search you can inspect. You declare the task interface, write a metric, and let an optimizer propose instructions and demonstrations for one configured language model. A separate holdout test still decides whether the compiled candidate is worth shipping. Compilation is empirical search: it can overfit its data, and it doesn't prove the program is correct.

Manual templates don't search

Hand-tuned deploy-answerer prompts leave four questions unanswered:

  1. Model coupling. Wording that works on one hosted model can collapse on the next revision of the same alias.
  2. Fragility. A small instruction change can flip a serve/abstain decision on req_205.
  3. No search loop. Experiment tracking can compare two templates you already wrote. It doesn't propose the next instruction for you.
  4. Hidden state. Last Tuesday's release mixed a prompt string, a model alias, a metric, and a dataset. Which combination shipped?

A DSPy program keeps control flow relatively stable and searches prompt state for one LM and one metric. That separation gives you a candidate to inspect, not a release decision. You still own evaluation and promotion.

A program makes three contracts explicit:

  • What you want the LM to do (signatures)
  • How to compose LM calls (modules)
  • What good looks like (metrics)

With those contracts in place, an optimizer can propose instructions and few-shot examples. You then compare that candidate with the uncompiled baseline on held-out tickets.

Signatures, modules, and metrics

A signature is the task interface, not the prompt text. It names inputs, outputs, and field meanings the way a Python function signature names arguments. Ask what should stay stable when req_205 changes wording: evidence and question still enter, and an answer still leaves. DSPy fills in formatting, while later compilation can rewrite instructions and demonstrations around that boundary.

For deploy-answerer, the job is: given admitted ReleaseOps evidence and an engineer's question, either serve a cited claim or abstain. Use that boundary while reading the API snippets below. They need the dspy package, and compilation needs a configured LM. Tests after them stay on the standard library so you can check decisions without calling a model.

deploy-answerer-signature.py
1import dspy 2 3class GenerateAnswer(dspy.Signature): 4 """Answer a deploy question using only admitted evidence, or abstain.""" 5 evidence: str = dspy.InputField(desc="Admitted ReleaseOps excerpts") 6 question: str = dspy.InputField(desc="The engineer's question") 7 answer: str = dspy.OutputField(desc="SERVE with a citation, or ABSTAIN")

A module wraps a signature and holds compiled state such as demonstrations. dspy.ChainOfThought(GenerateAnswer) prepends a reasoning output field, calls the LM, and fills answer.[1]Reference 1DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines.https://arxiv.org/abs/2310.03714 That rationale is model output, not the execution trace. A trace records predictor calls, inputs, and outputs while the program runs, so don't mistake a readable rationale for an audit log.

deploy-answerer-module.py
1import dspy 2 3class DeployAnswerer(dspy.Module): 4 def __init__(self): 5 super().__init__() 6 self.generate_answer = dspy.ChainOfThought(GenerateAnswer) 7 8 def forward(self, question: str, evidence: str) -> dspy.Prediction: 9 return self.generate_answer(evidence=evidence, question=question)

If you later swap the LM or edit a field description, keep the module structure, recompile, and evaluate. A previous artifact can be a useful baseline, but don't assume it transfers.

Why is a DSPy signature more than a prompt template?

Answer

A signature defines the task interface: inputs, outputs, and field meanings. DSPy can then compile prompts and demonstrations for that interface, while the program logic stays stable.

Ask what a green check should mean for req_205: a correct route isn't enough if SERVE lacks a citation. A metric turns that product rule into a score. It takes a labeled example and a prediction, often returns 0.0 or 1.0, and may run hundreds of times, so keep it cheap and aligned with behavior you plan to ship.

Exact string match is fine for a short route label such as SERVE or ABSTAIN. It's the wrong tool for the full answer sentence. The metric below scores two things: did the model pick the right route, and did a SERVE answer reuse a fact from the admitted evidence?

score-route-and-evidence.py
1def route_of(text: str) -> str: 2 return text.split("|", 1)[0].strip().upper() 3 4def route_accuracy(gold: str, pred: str) -> float: 5 return 1.0 if route_of(gold) == route_of(pred) else 0.0 6 7def evidence_support(gold: str, evidence: str, pred: str) -> float: 8 if route_of(pred) == "ABSTAIN": 9 return 1.0 if route_of(gold) == "ABSTAIN" else 0.0 10 return 1.0 if "approval-4412" in pred.lower() and "approval-4412" in evidence.lower() else 0.0 11 12def answer_metric(gold: str, pred: str, evidence: str) -> float: 13 return 0.5 * route_accuracy(gold, pred) + 0.5 * evidence_support(gold, evidence, pred) 14 15def dspy_answer_metric(example, prediction, trace=None) -> float: 16 """Adapt DSPy's example/prediction callback to the product metric.""" 17 return answer_metric(example.answer, prediction.answer, example.evidence) 18 19evidence = "ReleaseOps record approval-4412 authorizes rollback for payment-tests." 20gold = "SERVE|approval-4412 authorizes rollback" 21good = "SERVE|Per approval-4412, rollback is authorized." 22bad = "SERVE|The deploy is approved because tests look quiet." 23abstain = "ABSTAIN|No ReleaseOps record supports this claim." 24 25print(f"good: {answer_metric(gold, good, evidence):.1f}") 26print(f"unsupported serve: {answer_metric(gold, bad, evidence):.1f}") 27print(f"wrong route: {answer_metric(gold, abstain, evidence):.1f}")
Output
1good: 1.0 2unsupported serve: 0.5 3wrong route: 0.0

A cited SERVE scores 1.0. An unsupported SERVE keeps the route half and loses the citation half. A wrong-route ABSTAIN scores 0.0. dspy_answer_metric adapts the product rule to DSPy: the optimizer passes a labeled example, a prediction, and an optional execution trace, not three standalone strings.

For open-ended wording, you can add an LLM-as-a-judge term after you calibrate it on held-out human labels. Judge scores can vary across runs, prefer verbose answers, and shift with candidate order.[2]Reference 2Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena.https://arxiv.org/abs/2306.05685 Lower temperature can cut sampling variance. It doesn't remove those biases. Keep the deterministic route and citation checks for the behaviors you refuse to regress.

FeatureHand-written templatesDSPy compilation
EffortEdit stringsSearch over a program
PortabilityOften model-specificRecompile and re-eval per target LM
EvaluationOnly as good as your trackerExplicit metric and splits
Few-shottingHand-picked demosBootstrapped or searched demos
What shipsA prompt fileA compiled artifact plus its eval contract

Search on validation, promote on holdout

Now the optimizer (older DSPy papers say teleprompter) has enough information to search. It takes the program, labeled examples, and metric, then returns a compiled copy. .compile() deep-copies the student, so the original module stays uncompiled.[1]Reference 1DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines.https://arxiv.org/abs/2310.03714 Current docs center the word optimizer; both names refer to the same search.

This is discrete search over prompt state, not gradient descent on model weights. The parameters ϕ\phiϕ are instruction strings and sets of demonstrations. Hosted LM calls don't expose a gradient through that text, so DSPy uses random search, bootstrap filtering, or a surrogate-guided trial loop depending on the optimizer. A higher validation score means the metric preferred that candidate on observed cases; it doesn't mean model weights moved.

Formally, we want the parameter set ϕ\phiϕ that maximizes the expected metric MMM over a dataset DDD:

ϕ∗=arg⁡max⁡ϕEx∈D[M(x,Programϕ(x))]\phi^* = \arg\max_{\phi} \mathbb{E}_{x \in D} [M(x, \text{Program}_\phi(x))]ϕ∗=argϕmax​Ex∈D​[M(x,Programϕ​(x))]

DDD here is the validation split the optimizer is allowed to see. In practice, the expectation is the sample mean over that split. Promotion still uses a separate test split, so selection and release measure different things.

What does ϕ\phiϕ represent in DSPy optimization?

Answer

It represents prompt-program choices such as instructions and demonstrations. The optimizer searches over those choices to maximize a validation metric, not to update model weights with gradients.

Split train, validation, and test before the optimizer sees any rows. Train supplies traces for bootstrapping, validation picks a candidate, and test makes the promotion decision. MIPROv2 will carve a validation split out of trainset if you omit valset. That's convenient for an experiment, but reusing that pool as holdout leaks the selection signal.

protect-train-validation-test-splits.py
1train_ids = {"req-001", "req-002", "req-003"} 2validation_ids = {"req-101", "req-102"} 3test_ids = {"req-201", "req-202"} 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, {"req-001"}) 16except ValueError as exc: 17 print("leakage_rejected:", "req-001" in str(exc)) 18 19print("clean_split_sizes:", [len(train_ids), len(validation_ids), len(test_ids)])
Output
1leakage_rejected: True 2clean_split_sizes: [3, 2, 2]

Predict before reading the next output: the validation winner needn't be the release winner. With two frozen tickets, the optimizer might try a zero-shot instruction, a few bootstrapped demos, or a rewritten instruction plus those demos. It keeps the candidate with the highest validation mean, then holdout can still favor the uncompiled baseline.

select-on-validation-promote-on-test.py
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)
Output
1validation_winner: compiled_a 2selected_test_score: 0.65 3promote_over_baseline: False

compiled_a wins validation at 0.95 against the baseline's 0.70, then scores 0.65 on holdout against 0.80. The gap is useful evidence: selection found a validation fit, while promotion tested transfer to unseen tickets.

Side-by-side score bars for two deploy-answerer prompt candidates. On validation, compiled_a scores 0.95 and beats the 0.70 baseline, so the optimizer selects it. On the unseen holdout, compiled_a drops to 0.65 while the baseline stays at 0.80, so the release gate blocks promotion.
The optimizer may select compiled_a on validation. The holdout release gate still compares that winner with the baseline before anyone serves it.

BootstrapFewShot: traces that pass become demos

If the metric accepts one evidence trace and rejects another, should the accepted trace become a future prompt example? LabeledFewShot attaches labeled train rows as demos and doesn't call the LM during .compile(). BootstrapFewShot goes one step further: it runs a teacher program (often a copy of the uncompiled student) on the training set, keeps traces the metric accepts, and writes those traces into the candidate's demonstrations.

Diagram showing Teacher run, Metric filter, Accepted traces, and Rejected traces.
Teacher run, Metric filter, Accepted traces, and Rejected traces.
  1. Bootstrap. Run the teacher on training tickets. Extra rounds copy the LM at temperature=1.0 to gather diverse traces.
  2. Filter. Call metric(example, prediction, trace). A truthy score, or a score at or above metric_threshold, accepts the trace.
  3. Assign. Up to max_bootstrapped_demos slots (default 4) get those accepted traces. Remaining slots up to max_labeled_demos (default 16) fill with raw labeled rows.

That loop only proves a trace passed your metric once. Compare the compiled copy with the uncompiled baseline on holdout before you keep it; a polished demo can still encode a training-set accident.

The library call looks like this. Compilation itself needs dspy.configure(lm=...) and spends LM calls, so the lab below simulates only the filter.

configure-bootstrapfewshot.py
1import dspy 2 3optimizer = dspy.BootstrapFewShot( 4 metric=dspy_answer_metric, 5 max_bootstrapped_demos=4, 6 max_labeled_demos=16, 7) 8# compiled = optimizer.compile(DeployAnswerer(), trainset=trainset)
keep-metric-accepted-traces.py
1traces = [ 2 {"id": "req-001", "score": 1.0, "answer": "ABSTAIN|no ReleaseOps record"}, 3 {"id": "req-002", "score": 0.0, "answer": "SERVE|ship it, tests look quiet"}, 4 {"id": "req-003", "score": 1.0, "answer": "SERVE|approval-4412 authorizes rollback"}, 5] 6 7accepted = [trace for trace in traces if trace["score"] >= 1.0] 8print("accepted_ids:", [trace["id"] for trace in accepted]) 9print("rejected_ids:", [trace["id"] for trace in traces if trace["score"] < 1.0])
Output
1accepted_ids: ['req-001', 'req-003'] 2rejected_ids: ['req-002']

BootstrapFewShotWithRandomSearch (alias BootstrapRS) repeats that bootstrap with different seeds, scores each demo set on validation, and keeps the best. Reach for it when one bootstrap pass is too noisy and you can afford more compile budget, then retain the same holdout gate.

MIPROv2: joint instruction and demo search

BootstrapFewShot doesn't rewrite the instruction. If good demos still lead to the wrong route, MIPROv2 jointly searches instruction text and few-shot sets.[3]Reference 3Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programshttps://arxiv.org/abs/2406.11695 Official docs describe three stages:

  1. Bootstrap demo sets, the same rejection-sampling idea as above, producing num_candidates demo bundles.
  2. Propose instructions with a prompt_model. The proposer sees a data summary, the program structure, the bootstrapped demos, and a sampled tip such as "be concise".
  3. Search combinations with Bayesian optimization. The current DSPy implementation drives that loop with Optuna and a multivariate TPE sampler, so compile hosts need dspy[optuna]. Trials can score minibatches, then re-check the leader on the full validation set.

Default auto="light" sets candidate count, trial count, and validation cap for you. Start there. Manual mode is auto=None plus num_candidates on the constructor and num_trials on compile(...). Mixing auto="light" with num_candidates raises, and omitting num_trials in manual mode raises too.

configure-miprov2-light.py
1import dspy 2 3optimizer = dspy.MIPROv2(metric=dspy_answer_metric, auto="light") 4# compiled = optimizer.compile(DeployAnswerer(), trainset=trainset, valset=valset) 5 6zero_shot_optimizer = dspy.MIPROv2(metric=dspy_answer_metric, auto="light") 7# zero_shot = zero_shot_optimizer.compile( 8# DeployAnswerer(), 9# trainset=trainset, 10# valset=valset, 11# max_bootstrapped_demos=0, 12# max_labeled_demos=0, 13# )
reject-manual-mipro-without-trials.py
1def compile_mipro(*, auto: str | None, num_candidates: int | None, num_trials: int | None = None) -> str: 2 if auto is None and num_candidates is not None and num_trials is None: 3 raise ValueError("If auto is None, num_trials must also be provided.") 4 if auto is None and (num_candidates is None or num_trials is None): 5 raise ValueError("If auto is None, num_candidates must also be provided.") 6 if auto is not None and (num_candidates is not None or num_trials is not None): 7 raise ValueError("If auto is not None, num_candidates and num_trials cannot be set.") 8 return "ok" 9 10try: 11 compile_mipro(auto=None, num_candidates=4) 12except ValueError as exc: 13 print("missing_num_trials_rejected:", "num_trials" in str(exc)) 14 15print("light_ok:", compile_mipro(auto="light", num_candidates=None))
Output
1missing_num_trials_rejected: True 2light_ok: ok

The explicit split is a release guardrail. Pass trainset, valset, and a held-out test set you never hand to compile. Current DSPy builds a validation split from trainset when valset is omitted; convenient for exploration, unsafe as a release set.

As a default, instruction search can transfer farther than a large demo set. Demo-heavy candidates can memorize the train tickets you bootstrapped from. If validation is tiny or skewed, prefer instruction-heavy settings and keep a real holdout. A MIPROv2 run is zero-shot only when both demo sources are disabled at compile time: max_bootstrapped_demos=0 and max_labeled_demos=0.[4]Reference 4MIPROv2 optimizer implementationhttps://github.com/stanfordnlp/dspy/blob/3f06959ebbb80fb0bda001e1f802d70c2d5be7f7/dspy/teleprompt/mipro_optimizer_v2.py Setting only the bootstrapped limit to zero still permits labeled demonstrations.

Which optimizer to try first

DSPy doesn't pick an optimizer for you. The auto knobs on MIPROv2 and GEPA set budget inside one algorithm; they don't compare algorithms for you.

OptimizerTunesTypical setupCompile cost
LabeledFewShotDemos from labelsSmall labeled train setNone (no LM calls)
BootstrapFewShotMetric-accepted traces as demosRepresentative train tickets plus a metricMedium
BootstrapRSMany demo sets, pick on valExplicit validation splitHigh
MIPROv2Instructions and demos jointlySeparate val set, dspy[optuna], more metric budgetVery high
GEPAInstructions via reflective rewritesReflection LM plus feedback-rich metricVery high
BootstrapFinetuneModel weights from accepted tracesA tunable LM, after prompt-only plateausTraining plus deploy cost

GEPA maintains a population of programs, reads natural-language feedback from the metric, and samples parents from a Pareto frontier of per-example winners.[5]Reference 5GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learninghttps://arxiv.org/abs/2507.19457 Across its six paper tasks, it reports beating GRPO by 6% on average (up to 20%) with up to 35× fewer rollouts, and beating MIPROv2 by over 10% (including +12% accuracy on AIME-2025). Those numbers select GEPA on that benchmark, not on deploy-answerer.

A GEPA metric can return a float, but the richer contract is dspy.Prediction(score=..., feedback="..."): the reflection LM can turn feedback into its next instruction proposal. A bare float still runs; the proposer then sees a generic "this trajectory got a score of n" caption. Compare measured holdout gain and compile cost with BootstrapFewShot or MIPROv2 before you adopt it.

Save JSON state, not a pickle from the internet

A compiled program is only useful if you can reload the prompt state later. DSPy has two save paths, and mixing them up makes an otherwise reproducible compile hard to deploy.

  1. State-only JSON. program.save("compiled_a.json") writes predictor state: instructions, demonstrations, signature metadata, and sanitized reconstruction state for an attached LM. Current DSPy LM state includes its model identifier and safe configuration, but excludes API keys. Loading recreates that per-predictor LM, and a later global dspy.configure(lm=...) doesn't replace it.[6]Reference 6DSPy program and LM state serializationhttps://github.com/stanfordnlp/dspy/tree/3f06959ebbb80fb0bda001e1f802d70c2d5be7f7/dspy Recreate DeployAnswerer() in code, call .load("compiled_a.json"), and use the loaded LM unless this release deliberately changes the model. For an intentional override, call program.set_lm(runtime_lm) and evaluate that artifact-model pair before promotion.
  2. Whole-program directory. program.save("compiled_a/", save_program=True) writes program.pkl plus dependency metadata. Restore with dspy.load("compiled_a/", allow_pickle=True). That path deserializes cloudpickle, so treat the directory like executable code and only load artifacts you trust.

Prefer JSON for ordinary releases. It's readable, diffable, and doesn't execute code on load. It still isn't a secret store or a complete release manifest. Current DSPy removes API keys when dumping LM state and strips unsafe endpoint fields during ordinary load; restoring custom LM classes or unsafe endpoint state requires an explicit trusted-state opt-in.[6]Reference 6DSPy program and LM state serializationhttps://github.com/stanfordnlp/dspy/tree/3f06959ebbb80fb0bda001e1f802d70c2d5be7f7/dspy

inspect-json-state-boundary.py
1import json 2from pathlib import Path 3from tempfile import TemporaryDirectory 4 5saved = { 6 "instructions": "Cite a ReleaseOps record or abstain.", 7 "demos": [{"question": "Can we rollback payment-tests?", "answer": "SERVE|approval-4412"}], 8 "signature": ["evidence", "question", "answer"], 9 "lm": { 10 "class": "dspy.clients.lm.LM", 11 "model": "openai/gpt-5.4-mini-2026-08-01", 12 "temperature": 0.0, 13 }, 14} 15 16with TemporaryDirectory() as temp_dir: 17 artifact = Path(temp_dir) / "compiled_a.json" 18 artifact.write_text(json.dumps(saved), encoding="utf-8") 19 restored = json.loads(artifact.read_text(encoding="utf-8")) 20 print("state_file_created:", artifact.exists()) 21 print("restored_outputs:", restored["signature"][-1]) 22 print("python_class:", "DeployAnswerer") 23 print("saved_model_id:", restored["lm"]["model"]) 24 print("api_key_in_json:", "api_key" in restored["lm"]) 25 loaded_predictor_lm = restored["lm"]["model"] 26 globally_configured_lm = "openai/gpt-5.4-nano-2026-08-01" 27 print("effective_after_load:", loaded_predictor_lm or globally_configured_lm) 28 explicit_override = globally_configured_lm 29 print("effective_after_set_lm:", explicit_override)
Output
1state_file_created: True 2restored_outputs: answer 3python_class: DeployAnswerer 4saved_model_id: openai/gpt-5.4-mini-2026-08-01 5api_key_in_json: False 6effective_after_load: openai/gpt-5.4-mini-2026-08-01 7effective_after_set_lm: openai/gpt-5.4-nano-2026-08-01

The last two lines capture the precedence trap. A loaded predictor carries its saved LM, so changing the global default doesn't move it. set_lm is the explicit release action that recursively replaces predictor LMs. Treat that call as a new tested pairing, not as harmless runtime wiring.

Compile offline, serve a frozen artifact

.compile() can spend a large LM budget. Run it in an offline job, then let request handling load the tested artifact and answer. If an optimizer loop reaches the request path, latency, cost, and even prompt state now depend on traffic.

Two-lane deploy-answerer lifecycle. The offline lane runs train and validation tickets through optimizer.compile and writes compiled_a.json. The online lane recreates the DeployAnswerer class, loads that JSON, and answers a request with zero optimizer calls.
Search runs on a compile box. Production recreates the module class, loads JSON state, and answers. The optimizer doesn't run on req_205.
Table of a deploy-answerer JSON save. Instructions, few-shot demos, signature metadata, and LM state including its model ID are stored. API keys are excluded; unsafe endpoint fields may appear but ordinary loading ignores them. The Python Module class and evaluation hashes stay outside the file.
compiled_a.json can carry a sanitized predictor LM and model identifier, but not credentials or the source class. Version the artifact with its metric, data split, and release manifest so every loaded or overridden model remains explainable.

Before trusting average holdout lift, inspect critical slices. If rollback guidance drops below its floor, block the candidate even when the mean improved.

block-artifact-promotion-on-critical-slices.py
1baseline = {"unsupported_claim": 0.84, "rollback": 0.91} 2compiled = {"unsupported_claim": 0.95, "rollback": 0.83} 3minimum = {"unsupported_claim": 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)
Output
1improved_average: True 2failed_release_gates: ['rollback'] 3promote: False

What must be versioned after a DSPy compile run?

Answer

Version the compiled artifact, target model, dataset split, metric, optimizer settings, and source program. Without those pieces, you can't explain or roll back a production quality change.

A stable DeployAnswerer interface lets you run a new compile when the underlying model changes. It doesn't make old JSON model-independent. Instruction phrasing and demos can be specific to one snapshot. A provider-side update is a new experiment, even when the alias string didn't change. Keep a compile record keyed by program version and exact model revision.

require-artifact-for-exact-model-revision.py
1artifacts = { 2 ("deploy-answerer-v3", "hosted-model@2026-04-01"): "compiled_a_hosted-2026-04-01.json", 3 ("deploy-answerer-v3", "local-model@rev7"): "compiled_a_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("deploy-answerer-v3", "local-model@rev7")) 13try: 14 artifact_for("deploy-answerer-v3", "local-model@rev8") 15except KeyError: 16 print("uncompiled_revision_rejected:", True)
Output
1known_artifact: compiled_a_local-rev7.json 2uncompiled_revision_rejected: True

That record is what the next lesson packages into a release bundle. The JSON file is one field; compile run id, model pin, eval suite hash, and rollback target complete the release evidence.

When compilation burns budget

When compile burns budget, diagnose the symptom before increasing the budget. Each failure below points to a different contract.

Exact match on long answers

Almost every prediction scoring 0 means bootstrap keeps nothing and MIPROv2 wanders. The usual cause is scoring open-ended text as if it were a class label: "ABSTAIN because no ReleaseOps record exists" and "ABSTAIN|no ReleaseOps record" can express the same decision. Score route and citation deterministically, then add a calibrated judge only for wording rules can't check.

Waiting for thousands of labels

If you postpone .compile() until the dataset looks like a fine-tune set, you're importing weight-training habits into prompt search. Start with a small, representative split, watch slice coverage, and add tickets where evaluation is unstable. Don't invent a required sample count before measuring the task.

Never reading the compiled prompt

If holdout looks fine but production fails and nobody can say which demos shipped, you treated .compile() as a black box. After compilation, inspect recent LM calls with dspy.inspect_history(n=...). Edge-case demos or misleading instructions point back to coverage or metric design, so fix those and rerun the comparison.

Hidden setup mistakes

Finally, check setup before blaming search. Calling modules without a metric and .compile() is prompt formatting, not optimization. MIPROv2 rejects num_candidates while auto is still "light"; manual mode needs auto=None and num_trials. Its compile path imports Optuna, so install dspy[optuna] on compile hosts.

A five-ticket validation set can memorize: tiny val plus a large demo count is how compiled_a beats the baseline in-sample and loses on holdout. JSON state doesn't recreate DeployAnswerer, so load it into a compatible class. It can recreate an attached predictor LM from sanitized state; use set_lm when a reviewed release intentionally replaces that model. dspy.load(..., allow_pickle=True) executes cloudpickle; only load trusted directories. Heavy optimizers belong in batch jobs, never inside a request handler.

Key takeaways

  • DSPy searches instructions and demonstrations for a declared program. It doesn't replace held-out evaluation.
  • Signatures name the interface. Modules own control flow. Optimizers fill prompt state. .compile() returns a new copy.
  • BootstrapFewShot turns metric-accepted traces into demos. MIPROv2 also rewrites instructions. Compare every candidate with the baseline on a split the optimizer never saw.
  • Save JSON state for ordinary releases. Recreate the Python class, then load its predictor and sanitized LM state. Keep credentials outside the artifact and use set_lm only for an evaluated model override.
  • Version the artifact with the model revision, metric, split hashes, optimizer settings, and source program. That's the object the next lesson will pin inside a release bundle.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.In DSPy, what boundary should a task signature define?

Correct answer: The typed input-output contract that modules and optimizers can compose without hard-coding one prompt string.

A signature states the task interface. DSPy modules control execution and optimizers search prompts or demonstrations around that interface, so the signature is more stable than one hand-written prompt.

2.A DSPy metric marks any answer containing the expected keyword as correct, but users report unsupported claims. What is the missing design work?

Correct answer: Score the required behavior and evidence, including refusal or citation conditions, then validate those criteria on held-out examples.

DSPy optimizes the metric it receives. A keyword-only metric rewards unsupported answers, so the metric must encode the contract and be checked on data not used to search.

3.Why should a compiled DSPy program be promoted using a holdout set?

Correct answer: The holdout set estimates whether searched instructions and demonstrations generalize beyond the examples used during optimization.

Prompt optimization can overfit finite examples just like model training. A held-out evaluation reveals whether the compiled artifact improves the intended behavior rather than memorizing the search set.

4.A baseline instruction is useful, but demonstration selection is noisy. The team can afford repeated compile trials and has a validation set. Which optimizer best targets that uncertainty?

Correct answer: BootstrapRS, because it tries multiple bootstrapped demonstration sets and selects among trials on validation.

BootstrapRS targets variance in bootstrapped demonstrations by running multiple candidate sets and selecting with validation. BootstrapFewShot produces one accepted trace set, while MIPROv2 broadens search to instructions as well as demos.

5.A team wants eight MIPROv2 candidates and 24 compile trials under manual budget control. Which configuration preserves that experiment?

Correct answer: Construct MIPROv2 with auto=None and num_candidates=8, then call compile with num_trials=24.

Manual mode disables automatic budget selection. Candidate count belongs in optimizer construction and trial count belongs in compile, keeping the search space and evaluation budget explicit.

6.A team wants MIPROv2 to search instructions without placing any examples in candidate prompts. Which compile settings establish that zero-shot contract?

Correct answer: Set both max_bootstrapped_demos=0 and max_labeled_demos=0; either nonzero source can still place examples in prompts.

MIPROv2 checks both effective demo limits. Zeroing only bootstrapped traces leaves labeled few-shot examples available, so both limits must be zero for instruction-only candidates.

7.A team loads compiled_a.json, then calls dspy.configure(lm=new_lm) and assumes every predictor switched models. What should its release code do?

Correct answer: Call program.set_lm(new_lm) explicitly, evaluate the new artifact-model pair, and keep credentials outside serialized state.

Current JSON state can reconstruct a predictor's sanitized LM, which takes precedence over the global default. set_lm makes an intentional override explicit; API keys remain runtime secrets and the changed pairing needs holdout evidence.

8.A program has been compiled against an evaluation set. What is the safest serving workflow?

Correct answer: Compile offline, record the artifact and metric evidence, then serve the frozen program under the same contract.

Offline compilation makes the search reproducible and reviewable. Serving a frozen artifact avoids request-time drift, cost, and accidental use of production traffic as an uncontrolled optimization set.

9.An aggregate DSPy score improves, but a safety-critical refusal slice regresses. What promotion rule is appropriate?

Correct answer: Treat the critical slice as a binding gate and block promotion until its requirement is met.

Averages can conceal failures in important subpopulations. Critical slices need explicit gates so an optimizer cannot trade away a safety or contract requirement for broad but less relevant gains.

9 questions remaining.

Next Step
Continue to Model Versioning & Deployment

DSPy turns a prompt program into a compiled, evaluated artifact. Model versioning packages that prompt state with its model pin, dataset, evaluation receipt, deployment policy, and rollback target so the tested candidate can move through release without losing lineage.

PreviousExperiment Tracking with MLflow and W&B
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines.

Khattab, O., et al. · 2023 · arXiv preprint

https://arxiv.org/abs/2310.03714

Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena.

Zheng, L., et al. · 2023 · NeurIPS 2023

https://arxiv.org/abs/2306.05685

Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs

Opsahl-Ong, K., et al. · 2024

https://arxiv.org/abs/2406.11695

MIPROv2 optimizer implementation

DSPy Contributors · 2026

https://github.com/stanfordnlp/dspy/blob/3f06959ebbb80fb0bda001e1f802d70c2d5be7f7/dspy/teleprompt/mipro_optimizer_v2.py

GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning

Agrawal, L. A., Tan, S., Soylu, D., et al. · 2025

https://arxiv.org/abs/2507.19457

DSPy program and LM state serialization

DSPy Contributors · 2026

https://github.com/stanfordnlp/dspy/tree/3f06959ebbb80fb0bda001e1f802d70c2d5be7f7/dspy

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.