← All posts
·7 min read

Claude Code with DSPy: Programmatic Prompt Engineering

Claude CodeDSPyPrompt Engineering

DSPy treats prompts as a compiler problem. Instead of hand-tuning strings until something works, you declare what you want, define metrics, and let DSPy optimise the prompts and few-shot examples for you. Claude Code is the right partner for building those programs: it understands the structure DSPy expects, generates clean module definitions, and helps you iterate on signatures, metrics, and evaluators.

This post walks through how to use Claude Code to build real DSPy pipelines, from a minimal RAG program to a multi-step agent. We will cover the integration patterns that hold up under load and the failure modes you should design around.

Why pair Claude Code with DSPy

Most LLM code in the wild is string templating. You concatenate context, instructions, and examples into a prompt, send it to a model, and parse the output. When something breaks, you go back and tweak the strings. DSPy collapses this loop. You write a typed signature, pick a module (ChainOfThought, ReAct, ProgramOfThought), define a metric, and DSPy compiles the prompt for you.

Claude Code is built for this kind of structured codebase. It reads your project, understands the abstractions, and refactors across modules. Writing DSPy by hand is fine. Writing DSPy with Claude Code is faster, more consistent, and the resulting code is easier to ship.

The combo also works the other way: DSPy is a great compile target when Claude Code is building an LLM feature. You ask Claude to "implement X as a DSPy program with this signature and these test cases" and you get a typed, evaluable program instead of a brittle prompt string.

Installing DSPy in a Claude Code project

DSPy is a Python library. From your project root:

uv add dspy-ai

Or with pip:

pip install dspy-ai

Configure the LM. DSPy supports Anthropic, OpenAI, local models via Ollama, and dozens of others:

import dspy

lm = dspy.LM("anthropic/claude-opus-4-5", api_key="sk-ant-...")
dspy.configure(lm=lm)

Now Claude Code can read this file, understand the LM configuration, and generate downstream modules consistently. For more on configuring Anthropic credentials, see our guide to Claude API keys.

Pattern 1: Signatures as the source of truth

DSPy signatures are typed declarations. They tell DSPy what goes in and what comes out, and the module figures out the prompt.

import dspy

class GenerateSummary(dspy.Signature):
    """Summarise a technical document in two sentences."""

    document: str = dspy.InputField(desc="Full text of the document")
    audience: str = dspy.InputField(desc="Target reader, e.g. 'senior engineer'")
    summary: str = dspy.OutputField(desc="Two-sentence summary")

summariser = dspy.ChainOfThought(GenerateSummary)
result = summariser(
    document=open("README.md").read(),
    audience="senior engineer",
)
print(result.summary)

The trick to using Claude Code well here is to keep the signature class as the spec. When you ask Claude to "add an urgency field to the summary output", it edits the signature and everything downstream stays consistent. The signature is a contract, not just code.

In CLAUDE.md, add:

## DSPy Conventions

- Every LLM call goes through a DSPy module. No raw prompt strings.
- Define signatures as classes inheriting from dspy.Signature.
- Include a one-line docstring that becomes the task instruction.
- Use dspy.InputField and dspy.OutputField with descriptive `desc` arguments.
- New LLM features start as a signature + module + at least one test case.

Claude Code will follow this convention across every new module. The codebase stays uniform.

Pattern 2: Compiling with an optimiser

The thing DSPy does that string-based prompting cannot is compile. You give it a metric and a training set, and it searches for the prompt, few-shot examples, or chain-of-thought structure that maximises the metric.

from dspy.teleprompt import BootstrapFewShotWithRandomSearch

# Define a metric (1.0 if good, 0.0 if not)
def exact_match(example, prediction, trace=None):
    return prediction.summary.strip() == example.summary.strip()

# Small labelled set
trainset = [
    dspy.Example(
        document="...",
        audience="senior engineer",
        summary="Expected output here.",
    ).with_inputs("document", "audience"),
    # ... more examples
]

optimiser = BootstrapFewShotWithRandomSearch(
    metric=exact_match,
    max_bootstrapped_demos=4,
    num_candidate_programs=8,
)

compiled = optimiser.compile(summariser, trainset=trainset)
compiled.save("compiled/summariser.json")

The compiled artefact is a JSON file. You commit it to your repo, load it at runtime with summariser.load("compiled/summariser.json"), and ship. No more "this prompt works on my machine".

Claude Code is excellent at building the evaluation loop around the optimiser. Ask it to "write a metric that scores summaries on factuality using these golden answers" and you get a self-contained evaluator. Ask it to "run the optimiser, save the artefact, log the score" and it builds the orchestration script.

Pattern 3: Multi-step programs

Once you have a few DSPy modules, you compose them into programs. This is where Claude Code earns its keep: the programs get long, the state passes through multiple steps, and you need a tool that can hold the whole thing in its head.

class ResearchAndDraft(dspy.Module):
    def __init__(self):
        super().__init__()
        self.retrieve = dspy.Retrieve(k=5)
        self.synthesise = dspy.ChainOfThought("query, passages -> summary")
        self.outline = dspy.ChainOfThought("summary -> outline")
        self.draft = dspy.ChainOfThought("outline, summary -> draft")

    def forward(self, query: str):
        passages = self.retrieve(query).passages
        summary = self.synthesise(query=query, passages=passages).summary
        outline = self.outline(summary=summary).outline
        draft = self.draft(outline=outline, summary=summary).draft
        return dspy.Prediction(draft=draft, summary=summary, outline=outline)

This is a four-step pipeline: retrieve, synthesise, outline, draft. Each step is its own module. DSPy compiles them together, and the optimiser can tune any of them based on the end-to-end metric.

If you are using Claude Code to build something like this, the pattern that works:

  1. Write the forward method first as a comment describing the flow.
  2. Ask Claude to fill in the modules one at a time.
  3. After each module, run one end-to-end call and inspect lm.history to see what DSPy generated.
  4. Refine the signatures based on what came out.

This is much faster than writing prompts from scratch.

Get Claudify. A Claude Code starter kit with DSPy patterns and evaluation scaffolding ready to go.

Pattern 4: Claude Code as DSPy orchestrator

You can also flip the integration. Claude Code runs as the outer orchestrator, calling DSPy programs as tools. This is useful when the outer loop needs reasoning that DSPy is not great at (deciding which program to call, debugging weird inputs) but the inner work benefits from a compiled program.

The flow:

  1. Claude Code reads a user request.
  2. It decides which DSPy program is the right tool.
  3. It calls the program via a CLI wrapper or Python entrypoint.
  4. It reasons over the output and decides whether to call another program or respond.

This is similar to how Claude Code subagents work, except the subagent is a DSPy program. You get the best of both: Claude's reasoning and DSPy's tuned, evaluated, reliable inner loops.

Evaluating in CI

A compiled DSPy program is testable. You should treat it like any other code artefact in your repo.

# tests/test_summariser.py
import dspy
from myapp.modules import GenerateSummary

def test_summariser_baseline():
    summariser = dspy.ChainOfThought(GenerateSummary)
    summariser.load("compiled/summariser.json")
    result = summariser(
        document="Sample doc",
        audience="senior engineer",
    )
    assert len(result.summary.split(".")) >= 2
    assert result.summary.strip() != ""

Run this with pytest in CI on every PR. If a prompt change drops accuracy below threshold, the build fails. See our guide on Claude Code with pytest for the broader pattern.

When DSPy is the right tool

Use DSPy when:

  • You have more than two or three LLM calls that share structure.
  • You can write a metric, even a noisy one.
  • You want to swap models without rewriting prompts.
  • The team is going to maintain this codebase for a year or more.

Skip DSPy when:

  • You have a single one-shot prompt and no evaluation needs.
  • The task is genuinely creative and you have no metric.
  • You are in pure exploration mode and the structure is changing daily.

Claude Code can still be your tool either way. DSPy is the compiler, Claude Code is the editor. They are complementary, not competing.

Failure modes

A few things to watch for:

Optimisation overhead. Compilation runs hundreds of LM calls. If your model is expensive, the compile is expensive. Start with a cheaper model like Haiku or a local Ollama model for compilation, then validate on Opus or Sonnet. Our Claude model comparison walks through the tradeoffs.

Metric gaming. DSPy will faithfully optimise whatever metric you give it. If your metric is "exact string match" and your task is open-ended, the optimiser may find degenerate solutions. Spend more time on the metric than on the program.

Module abstraction leak. DSPy modules look declarative but they still generate prompts under the hood. When something breaks, set dspy.inspect_history(n=1) and read the actual prompts. The abstraction does not absolve you from understanding what the model sees.

Conclusion

DSPy is a compiler for LLM programs. Claude Code is an editor that understands compiled languages. Together they let you ship LLM features that are typed, evaluated, version-controlled, and tunable. The boilerplate goes away. The structure stays.

If you want a Claude Code project that already understands DSPy conventions, with a working CLAUDE.md, example modules, and evaluation harness, that is what Claudify packages.

Get Claudify. The official Claude Code starter kit for developers shipping production LLM features.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame