Verdict: The single most leveraged move you can make with LLM pipelines in 2026 is to separate what the task should do (a stable contract you control) from how the model does it (the volatile, ever-improving region underneath). DSPy — the open-source framework from Stanford NLP — is the tool that codifies this split. Shopify used the same discipline to cut one AI workload's cost by roughly 550× by swapping an expensive model for a cheap one without touching the business logic (dspy.ai/community/use-cases). This guide shows how to apply the same idea to your own pipeline — with the four primitives, three claim layers, four optimizer choices, and a worked example you can run this afternoon.
Last verified: 2026-07-29 · Best for cost-critical, repeated tasks · Saves the most when models change often · 1 pip install (
pip install dspy) · Python 3.10+ (dspy.ai/installation) Pricing/feature facts below are volatile — DSPy moves fast (latest stable: 3.2.1 at time of writing). Re-check the docs before committing to a specific API.
TL;DR — the four-bullet version
- The core insight: treat each LLM task like a Python function — separate the interface (inputs/outputs) from the implementation (the prompt, the model, tool loops).
- The three claim layers you must pin down before optimizing: spec (natural-language instructions), code (hard constraints enforced in Python), and evals (examples that show what "good" looks like).
- DSPy does the search for you — given a signature, training data, and a metric, its optimizers (
BootstrapFewShot,MIPROv2,GEPA,SIMBA) automatically find the prompt + few-shot mix that maximizes your metric. - The payoff is agility, not just accuracy: when a new, cheaper model lands, you re-run the optimizer against the same pinned contract — no prompt rewrite, no integration churn.
What does "separate the task from the model" actually mean?
It means locking the boundary of a task — its inputs, outputs, and constraints — in code, and keeping the internals (the prompt wording, the reasoning strategy, which model is called) completely free to change. When the boundary is fixed, the internals become an optimization target someone else (or an algorithm) can search.
The everyday analogy is a Python function. You give a function a name, a list of inputs, and a return type. Then you get to: reuse it thousands of times, swap the inside, compose it into bigger programs, distribute it as a black box. DSPy brings every one of those properties to LLM programs (dspy.ai, github.com/stanfordnlp/dspy).
This split is valuable for the same reason the function split in ordinary software was valuable. When models improve (or get cheaper) every few weeks, you want to upgrade the inside without touching anything outside. You want to search the inside. You want to compose the inside. And when the next research paper drops a trick — chain-of-thought, retrieval, tool use, agentic loops — you want to try it without rebuilding the system around it.
The three claim layers you must pin down before you optimize
DSPy's core design insight is that a task is fully specified only when you pin three things together. Miss any of the three and you cannot hand the task to an automatic optimizer.
1. Spec — "what should happen" (instructions)
The spec is the natural-language declaration of what the LLM should do. In DSPy, it lives inside a Signature — a class that declares named input and output fields plus an optional docstring. The docstring and field names become the prompt the model sees; you don't write prompt strings by hand.
class ExtractTax(dspy.Signature):
"""Extract the tax value (as a float) from the invoice text.
If the amount is illegible, output zero."""
invoice: str = dspy.InputField()
tax: float = dspy.OutputField()
The signature is the contract — the boundary that does not move. Your integration depends only on invoice -> tax, never on the wording that produced it.
2. Code — "what must happen" (hard constraints)
Specs are natural language; some things you need enforced as code. DSPy ships with a small primitives layer for this — e.g. dspy.Predict (vanilla call), dspy.ChainOfThought (adds reasoning). You compose them in forward(), and you can branch on the result:
class TaxProgram(dspy.Module):
def __init__(self):
self.extract = dspy.Predict(ExtractTax)
self.extract_cot = dspy.ChainOfThought(ExtractTax)
def forward(self, invoice):
pred = self.extract(invoice=invoice).tax
if pred == 0: # fallback: reasoning helps
pred = self.extract_cot(invoice=invoice).tax
if pred < 0: # hard guardrail
raise ValueError("negative tax — hand to a human")
return pred
The if pred == 0 and if pred < 0 checks are constraints you refuse to give up, even if the inside changes. They are the part of the contract the optimizer cannot touch.
3. Evals — "what good looks like" (examples + metric)
The third layer is empirical. Some things cannot be written as instructions or code — they can only be shown by example. A beginner recognizes a maple tree because someone pointed at enough examples over time, not because they memorized a definition. DSPy formalizes this with Examples (your labeled training data) and a metric function that scores a prediction from 0 to 1.
examples = [
dspy.Example(invoice="Invoice 4410, tax 8.20", tax=8.20).with_inputs("invoice"),
dspy.Example(invoice="Invoice 4411, tax 0.00", tax=0.00).with_inputs("invoice"),
]
def tax_metric(example, pred, trace=None):
return float(abs(pred.tax - example.tax) < 0.01)
With spec + code + evals in hand, your task is fully specified. Every new release of any model can now be benchmarked against the same bar; every new technique can be tried as a one-line swap inside the boundary; and automatic optimization becomes possible.
The DSPy primitives you build with
| Primitive | What it is | One-line role |
|---|---|---|
| Signature | Typed input/output spec for one LLM step | The contract. The thing that does not move. |
| Module | A class wrapping signatures with forward() logic |
Where you compose steps and add code guards. |
| Optimizer | A search algorithm that improves your module | The thing that fills in the inside for you. |
| Example | A labeled input/output pair for training/eval | The unit of "what good looks like." |
You configure the model once — fully independent of your signatures — and every module inherits it. This is the literal moment the "task vs model" separation happens in code:
import dspy
lm = dspy.LM("openai/gpt-4o-mini", api_key="...")
dspy.configure(lm=lm) # the model — changeable anytime
program = TaxProgram() # the task — stable contract
To swap models later you change one line (dspy.LM(...)). Nothing else in the codebase moves. (dspy.ai/installation, github.com/stanfordnlp/dspy)
Which DSPy optimizer should you use?
This is the single most common question for new DSPy users. The answer depends on how much data you have, how important accuracy is, and how long you can wait for compilation to finish. DSPy lists all of them under dspy.<OptimizerName> (dspy.ai/optimizers).
| Optimizer | Best for | What it optimizes | Min. data | Time |
|---|---|---|---|---|
BootstrapFewShot |
Quick prototyping, small sets | Few-shot demos | ~20–50 labeled | minutes |
BootstrapFewShotWithRandomSearch |
Mid-size sets, better than baseline | Demos + which to include | ~50–200 labeled | tens of minutes |
MIPROv2 |
Production-grade accuracy | Instructions + demos (Bayesian search) | ~200–500 labeled | hours / overnight |
GEPA |
Multi-objective search (Pareto) | Instructions on the Pareto frontier | labeled + metric | hours |
SIMBA |
Self-reflective rule discovery | Rules + demos from mini-batch failures | labeled + metric | hours |
Decision shortcut: validate your pipeline with BootstrapFewShot first. Graduate to MIPROv2 when you have 200+ examples and need production-grade accuracy. Reach for GEPA when you want a frontier of strong prompts (it maintains a Pareto set diversifying across problem types — see arXiv:2507.19457). Reach for SIMBA when you want the optimizer to write self-reflective rules from its own hard examples (dspy.ai/api/optimizers/SIMBA).
A common mistake is jumping straight to MIPROv2 before validating the signature, module, and metric work. A broken metric produces "optimized but useless" output. Start simple, confirm the pipeline produces something sane with BootstrapFewShot, then scale up the optimizer.
How to put it together: a worked extractor example
Here is a minimal but complete pattern — the kind of thing you can run this afternoon — that orchestrates the separation. Real production versions swap the signature, add more code guards, and pick a bigger optimizer.
import dspy
lm = dspy.LM("openai/gpt-4o-mini", api_key="...")
dspy.configure(lm=lm)
# 1. SPEC — the contract that does not move
class ExtractTax(dspy.Signature):
"""Extract the tax value (as a float) from the invoice text.
If the amount is illegible, output zero."""
invoice: str = dspy.InputField()
tax: float = dspy.OutputField()
# 2. CODE — your hard constraints live in the module
class TaxProgram(dspy.Module):
def __init__(self):
self.extract = dspy.Predict(ExtractTax)
self.extract_cot = dspy.ChainOfThought(ExtractTax)
def forward(self, invoice):
pred = self.extract(invoice=invoice).tax
if pred == 0:
pred = self.extract_cot(invoice=invoice).tax
if pred < 0:
raise ValueError("negative tax — hand to a human")
return pred
program = TaxProgram()
# 3. EVALS — examples + a metric
examples = [
dspy.Example(invoice="Invoice 4410 tax 8.20", tax=8.20).with_inputs("invoice"),
dspy.Example(invoice="Invoice 4411 tax 0.00", tax=0.00).with_inputs("invoice"),
dspy.Example(invoice="Invoice 4412 tax 13.50", tax=13.50).with_inputs("invoice"),
dspy.Example(invoice="Invoice 4413 tax 5.00", tax=5.00).with_inputs("invoice"),
]
def tax_metric(example, pred, trace=None):
return float(abs(pred - example.tax) < 0.01)
# 4. OPTIMIZE — let the optimizer fill in the inside
optimizer = dspy.BootstrapFewShot(metric=tax_metric)
compiled = optimizer.compile(program, trainset=examples)
# 5. USE — model gets swapped in one place; contract never moves
print(compiled(invoice="Invoice 9999 tax 2.40"))
The compiled result is an ordinary Python object — call it, cache it, save it (compiled.save("program.json")), load it later. The optimizer didn't change your code; it filled in the prompts with the best few-shot demonstrations and, for MIPROv2+, tuned the instruction text itself.
If you'd rather not wire an optimizer's harness yourself, you can let an autonomous AI agent loop do the bookkeeping — see our guide on self-running loops with Doer-Judge verification for the broader pattern of letting a system improve itself at runtime.
The Shopify 550× case study in plain terms
The most-cited enterprise result coming out of the DSPy ecosystem is from Shopify. According to DSPy's own use-case list, Shopify applied DSPy + GEPA to structured metadata extraction across all Shopify shops, and reduced yearly costs on that workload by roughly 550× (dspy.ai/community/use-cases; Shopify's own engineering talk is here).
The mechanism is the entire point of this article. Because the task — extract metadata, match a schema, return structured output — was pinned behind a signature and a metric, Shopify's engineers could:
- keep iterating on business logic inside the boundary — no integration churn,
- swap an expensive model for a cheap one using the same email/grader pipeline, and
- let GEPA search the prompt and few-shot space automatically against the same metric the team already trusted.
The 550× number is a vendor-reported metric (per DSPy's community page), so treat it as Vendor claim — Confirmed by DSPy's source list rather than a third-party audited number. We list it because DSPy's own website is the primary source here and the company itself features the result. The deeper principle — that pinning the boundary lets you chase cost dominance without breaking the system — holds regardless of the exact multipler.
If you want more like this from real production systems, Dropbox published optimizing Dash's relevance judge with DSPy for ranking, training data generation, and offline evaluation.
What new techniques are coming to DSPy (and why they all fit inside the same boundary)
DSPy ships new techniques constantly, and the whole point of the task/model split is that every new technique becomes a one-line swap inside a boundary you've already written. None of them touch your integration. Recent examples from the ecosystem:
- Recursive Language Models (RLMs) — a paper-driven approach from the DSPy community for long-context programs. If your task spans a large inbox or a full codebase, you add RLM in one line; your signature stays the same.
- GEPA — prompt optimizer based on Pareto-frontier selection across problem types (arXiv:2507.19457). Good when different prompts excel at different sub-tasks.
- MIPROv2 — instruction + demo co-optimization via Bayesian search (arXiv:2406.11695). The production-grade default.
- BetterTogether — composes optimizers so running MIPROv2 then finetuning gives better results than either alone.
- SIMBA — Stochastic Introspective Mini-Batch Ascent (dspy.ai/api/optimizers/SIMBA). The optimizer analyzes its own failures and writes self-reflective rules.
- DSPy Flex (upcoming in DSPy 4) — instead of optimizing prompts, optimizes the code harness itself: for any function you want to implement, DSPy can learn an entire code harness over time to solve it. Your business measure stays in place; only the learned harness changes.
The throughline is that a well-specified task can be handed to a search process that finds the cheapest implementation clearing the eval bar. Each new optimizer or technique is a better search — not a new contract.
For wider context, two of our earlier deep-dives are worth pairing with this one: how to use an open-source model as the brain of an autonomous agent (the model swap is exactly the kind of inside-change DSPy makes safe) and the broader playbook on self-improving AI agents vs static copilots (DSPy's optimizers are a concrete instance of an agent improving itself against an eval bar).
What does separating the task from the model cost you?
Nothing upfront per LLM call — DSPy is open source (MIT license, github.com/stanfordnlp/dspy) and pip install dspy works in Python 3.10+ (dspy.ai/installation). The real cost is the one-time work of writing a signature, gathering 20-500 labeled examples, and defining a metric — plus whatever time the optimizer spends (which is mostly LLM calls during compilation; you pay per call, hundreds to thousands depending on the optimizer and dataset size).
The optimizer cost is why teams often use a cheaper model during optimization and a more capable model for the compiled, deployed program — BootstrapFewShot and MIPROv2 make hundreds of LLM calls per compile. The same pattern recurs across the field: cheap agent-grade models like Gemma 4 running locally can serve as your "search/draft" model, while a frontier model is invoked only for final answers. DSPy doesn't force this — but the task/model split makes it trivial to try.
When should you NOT bother with DSPy?
DSPy adds real value for optimization-heavy, repeated tasks, but it is not free. From the trade-off literature and DSPy's own docs:
- One-shot creative tasks — if "summarize this paragraph" with a zero-shot prompt already scores 95% on your quality bar, optimization overhead doesn't pay back.
- Tasks with no clear metric — DSPy needs a metric to search against. If your quality is subjective and unwilling to be LLM-as-judge, you can't let the optimizer drive.
- Rapid prototyping — for a working demo in hours, writing a prompt directly is faster than setting up training data and compilation. DSPy pays off in the optimization phase, not the prototyping phase.
- Single-API-call pipelines with surrounding code already factored — if your pipeline is one LLM call and you've already factored that call into a function, you've already done a smaller version of the separation this article argues for.
The moment you have repeated AI tasks and any quality bar — even an approximate one — the discipline wins.
What does separating the task from the model mean for your small business or team?
If you run AI inside a recurring business process (support triage, document extraction, ad copy, content summarization, contract review), the pattern above means a few practical things:
- Stop hand-tuning prompts. Write a signature for each recurring task, gather 20-500 labeled examples, define a metric that proxies the business outcome (not surface-level string match), and let an optimizer find the prompt.
- Lock the contracts you refuse to give up. Business-critical guards — non-negative tax, JSON schema validity, PII redaction, no empty response — live as Python in
forward(). These do not move when the model moves. See our playbook on redacting sensitive data before sending files to ChatGPT for the contract-first mindset. - When a cheaper model drops, swap in one line and re-run. You do not start over. The optimizer re-finds the prompt for the new model against the same metric.
- Pick the optimizer by data size and patience. Validate with
BootstrapFewShot. Go production withMIPROv2when you have 200+ examples. ReachGEPA/SIMBAwhen a single prompt can't fit your whole problem space. - Use the right cheap model for search, the right expensive model for answers. Treat the optimizer's model and the deployed program's model as separate knobs.
The bigger strategic point: the entire industry is splitting into two layers — a stable "what" layer that encodes business context (specs, code, evals) and a fast-changing "how" layer that DSPy and similar tools progressively commoditize. Teams that master the split now gain a durable cost-and-agility advantage as the model wars keep accelerating.
FAQ
Q: What does "separate the task from the model" mean in DSPy?
A: You define the task once as a typed input/output contract (a DSPy Signature) plus constraints in code, and keep the model, prompt wording, and reasoning strategy as swappable internals. The boundary — invoice -> tax, for example — never moves; everything inside is free to be optimized or swapped for a cheaper model.
Q: Is DSPy free and open source?
A: Yes. DSPy is open source under the MIT license, maintained by the Stanford NLP group. Install with pip install dspy in any Python 3.10+ environment (dspy.ai/installation, github.com/stanfordnlp/dspy). You still pay for the LLM API calls your program and its optimizer make.
Q: How many training examples do I need to start with DSPy?
A: DSPy does not require massive datasets. ~20–50 labeled examples is enough for BootstrapFewShot. Plan on ~200–500 for MIPROv2. The bigger the dataset, the more signal the optimizer has to pick good demonstrations and instructions.
Q: Which DSPy optimizer should I use first?
A: Start with BootstrapFewShot to validate your signature, module, and metric produce sane output. Move to MIPROv2 when you have 200+ examples and need production-grade accuracy. Reach GEPA or SIMBA when a single optimal prompt cannot cover all your problem types.
Q: How did Shopify get a 550× cost reduction with DSPy? A: Per DSPy's own use-case list, Shopify applied DSPy + GEPA to structured metadata extraction across all its shops, and moved from an expensive model to a cheap one while keeping the same business logic, examples, and grading metrics. The 550× number is Vendor-confirmed on DSPy's site; the deeper principle — pinning the boundary lets you chase cost dominance without breaking the system — holds regardless of the exact multiplier (dspy.ai/community/use-cases).
Q: Do I need to rewrite my whole pipeline to use DSPy? A: No. You can wrap any individual LLM call with a Signature + Module and optimize just that step first. DSPy modules are plain Python objects — call them, cache results, save and load the compiled program. Use it where it pays off; leave the rest of your codebase as-is.
Q: What's coming next in DSPy? A: DSPy 4 introduces further Flex optimizations (the optimizer learns an entire code harness, not just a prompt) and continues to fold in research innovations like Recursive Language Models (RLMs) for long-context programs. The constant is that each new technique is a one-line swap inside a boundary you've already pinned (dspy.ai).

Discussion
0 comments