DSPy Signatures vs Handwritten Prompts in Production

Split design showing modular blocks and handwritten sheets balanced around a glowing center.

A language model can give a flawless answer in testing, then fail after a model update or small prompt edit. That drift is harder to manage when production apps rely on long, handwritten prompt strings. Those strings can leave inputs unclear and outputs unpredictable.

Typed contracts define an LLM task more clearly, while handwritten prompts give you direct control over the exact words sent to a model. Both approaches have a place in production, but they create very different engineering work.

The right choice depends on how often you change models, optimize behavior, test outputs, and maintain the app with other people.

Key Takeaways

  • DSPy signatures define typed input and output contracts, while handwritten prompts provide direct control over the exact wording and provider-specific behavior.
  • Use inline signatures for narrow tasks and class-based signatures when instructions, schemas, reusable types, or shared module contracts need clearer review.
  • Schema validation confirms format, not meaning; production systems still need semantic evaluation, regression tests, logging, and monitoring.
  • DSPy compilation can optimize instructions and demonstrations against measurable metrics, but compiled programs should be versioned and released as artifacts.
  • Handwritten prompts remain a practical choice for small, stable, provider-specific calls, while contract-driven designs suit typed outputs, compound pipelines, and frequent iteration.

How DSPy signatures differ from handwritten prompts

What handwritten prompts control

A handwritten prompt is text that developers write, store, and send to a model. Developers control its wording, message order, examples, formatting rules, and provider-specific features.

This directness helps when you need an exact prompt for a small, stable workflow. A marketer might write one that turns product notes into a landing-page draft. An engineer might write one that maps support requests to a JSON object.

However, prompt strings often combine the task request, output format, sample data, and business rules in one file. When results drift, you must determine whether the issue came from wording, the model, a changed example, or a downstream parser.

A signature declares the contract

A signature separates the task contract and task instructions from generated prompt text, with explicit input and output types recorded as a type annotation. A call such as dspy.Predict("question: str -> answer: str") gives DSPy a clear interface before it creates messages for a language model.

That separation makes the program easier to inspect. The make_signature helper turns a compact definition into a reusable Signature object. You can pass the same contract to different modules, including dspy.Predict and dspy.ChainOfThought, then change framework settings without rewriting call sites.

ApproachYou maintainMain production risk
Handwritten promptWording, formatting, parsing, and examplesHidden coupling between text and downstream code
SignatureTyped contract, evaluation, and framework configurationMore framework and evaluation discipline

The official signature documentation describes signatures as declarations of inputs, outputs, and task behavior rather than fixed prompts.

Choose inline strings or class-based contracts

Start with an inline signature for narrow tasks

An inline signature works well when the task is short and field meanings are obvious. For example, context: list[str], question: str -> answer: str accepts source passages and a question, then returns an answer.

To create the same definition in Python, call make_signature("context: list[str], question: str -> answer: str"). The make_signature helper normalizes this compact definition for use by a predictor.

Fields without a type annotation default to str. An explicit type annotation can request bool, int, list[str], or Optional[float]. This format keeps prototypes readable and suits a single predictor.

Still, a compact definition becomes harder to review when several outputs need a constrained field schema. A selection: int output needs more than a type annotation when valid values depend on a changing product catalog or business rule.

Use a class-based signature when instructions and schemas matter

A class-based signature makes the contract explicit in declarative Python code. Its class docstring supplies task instructions, while the conceptual Input Fields and Output Fields map to annotated dspy.InputField() and dspy.OutputField() declarations. Within DSPy, SignatureMeta collects those declarations into a usable signature.

Classes are the stronger choice when you use nested Pydantic models, reusable validation types, or outputs shared by several components. They make the field schema easier to review because changed output fields and field order appear as code changes, not sentence edits. When several reusable modules share a contract, derive a new signature class with with_instructions(...) or with_updated_fields(...) instead of editing the shared definition in place. Keep that docstring concise and testable.

When a compact definition references custom types, SignatureMeta can use frame introspection to resolve them from caller stack frames. That convenience can fail inside wrappers, notebooks, or dynamic imports. In production code, import those types directly and define them in classes.

Structured outputs depend on adapters and validation

Pydantic-backed validation helps parse declared field types

This typing validates inputs and coerces outputs, with an adapter handling the operational work. Adapters turn instructions, fields, and demonstrations into messages for the language model. They serialize the signature’s “Input Fields” and parse its “Output Fields” into Python values.

After an adapter extracts a raw value, DSPy attempts Pydantic TypeAdapter validation. If that fails, it may try ast.literal_eval, then preserve the raw string when parsing still doesn’t work. DSPy gives custom types, including dspy.Type values, another opportunity to parse the raw output.

The first DSPy program walkthrough shows how an adapter sits between a signature and the configured model. Chat, JSON, XML, and two-step configurations can format the same contract differently.

A declared type is not a business guarantee

A type annotation identifies a Python type, not a business guarantee. A parsed int only proves that a value became an integer. It doesn’t prove that the model chose a valid priority, cited the right source, or respected your policy. Likewise, a valid Pydantic model can contain a plausible but invented product name.

A successful parse confirms format. Your evaluation suite must confirm meaning.

Treat schema validation and quality validation as separate tests. Schema tests should cover parsing, required fields, and valid types. Semantic tests should cover source support, allowed values, policy compliance, and unsafe content. Adapters should use adapter-specific logging to capture the raw model response, adapter name, signature version, and model identifier before retrying or routing the task for review.

Compilation changes the prompt workflow

Optimizers need a measurable target

With handwritten prompts, versioning usually means committing prompt files and recording the model settings beside them. You still need a labeled test set, but prompt optimization remains manual unless you build a search process yourself.

DSPy can compile a program against an evaluation metric and examples. Optimizers can test candidate instructions, demonstrations, prefixes, and related learnable state. GEPA is one feedback-driven option, while a compiled search can compare competing candidates. Other optimizers suit different data and cost constraints.

Compilation doesn’t make your application correct by default. Different optimizers reward what your metric measures. If you score only syntactic JSON validity, it may find prompts that produce tidy but unhelpful answers. The official DSPy tutorials show practical optimizer workflows, but you still need representative development data.

Treat compiled programs as release artifacts

DSPy compilation returns a new program copy rather than changing your source program in place. Store the compiled artifact with its training examples, metric version, model configuration, adapters, date, and evaluation scores. For a GEPA run, preserve its evaluation context and configuration too, and tie everything to the exact versions of the program and its modules.

Signature mutation methods such as append, prepend, insert, and delete let you revise fields dynamically, while with_instructions and with_updated_fields return derived definitions rather than mutating a shared global. SignatureMeta retains each copied class’s class-level contract information, and make_signature can generate a contract before testing and releasing it, preserving immutability between candidates.

Keep that immutability across requests. Don’t mutate a shared global signature during a request; use with_instructions or with_updated_fields to create a derived version, test it, and release it as a versioned artifact.

Build compound AI pipelines around stable contracts

Put signatures at each module boundary

Production apps often need more than one model call. These compound AI pipelines can extract facts from source text, classify user intent, route requests, retrieve records, and generate a final answer. At boundaries between modules, small signatures make each step testable, while a type annotation keeps the contract easy to review.

For example, an extraction step can identify facts from source text, a classifier can infer intent, and a router can select retrieval rules before a generator produces the answer.

The router and retrieval modules should expose narrow inputs and outputs. This separation makes changes easier to isolate and helps teams test each contract independently.

Test and monitor every boundary

Give each module a focused regression set. Your extraction module needs source-grounded cases. Your router needs ambiguous queries and spelling variants. Your generator and downstream modules need cases where they must refuse unsupported claims.

In production, log the signature name, redacted field values, model version, adapters, latency, token use, parse failures, and evaluation outcomes. Compare those signals after a model change or compiled-program release.

The IBM overview of DSPy optimizers also stresses that program parameters can change during optimization. Monitor compiled changes with the same care you apply to application code releases.

When handwritten prompts remain the better choice

Keep direct prompts for small, fixed interactions

A handwritten prompt is often the practical choice for one language model call with a stable provider and no training set for optimization. A reusable typed contract may add little value in that case. It also fits provider features that DSPy may not expose cleanly, such as a proprietary tool-calling option or a tightly controlled system-message arrangement.

You should still version the prompt, test expected outputs, and capture failures. Put output parsing behind a dedicated function rather than mixing it into business logic. That design keeps a future move to a typed contract possible.

Match the approach to your team and risk

If you are building a prototype, start with a prompt and a small regression set. Move to an inline signature when repeated prompt variants create confusion or when a type annotation gives typed outputs a clearer contract than a raw prompt string.

If you own a multi-step application or share production code across models, use a class-based signature early. A representative test set makes prompt optimization more useful during repeated iterations.

If you work in a regulated or high-cost workflow, add explicit validation, release artifacts, and trace logging. Use human review for high-impact failures.

A hybrid approach also works. Keep a specialized handwritten prompt inside one provider-bound component, with adapters isolating the provider feature, while using contract-driven modules for the rest of the pipeline.

Frequently Asked Questions

When should I use a DSPy signature instead of a handwritten prompt?

Use a DSPy signature when you need typed outputs, multiple model calls, frequent iteration, or shared contracts across modules and maintainers. A handwritten prompt is often simpler for a small, stable, provider-specific interaction.

What is the difference between an inline and class-based signature?

An inline signature is concise and works well when the task and fields are simple. A class-based signature is better when you need detailed instructions, nested models, reusable validation types, or a schema shared by several components.

Does a declared type guarantee a correct model answer?

No. A declared type and successful parsing confirm that the output has the expected format, but they do not prove that the content is accurate, policy-compliant, or supported by the source. Semantic evaluation is still required.

What does DSPy compilation change in production?

Compilation searches for instructions, demonstrations, and related program parameters that perform well against an evaluation metric. The resulting program should be stored as a versioned release artifact with its data, metric, model configuration, adapters, and evaluation results.

Can DSPy signatures and handwritten prompts be used together?

Yes. A team can keep a specialized handwritten prompt inside a provider-bound component while using typed signatures for the rest of a compound pipeline. Adapters and narrow module boundaries help isolate provider-specific behavior.

Choose contracts that your team can test

Typed contracts reduce hidden coupling in production prompts. They create clear boundaries for modules that teams can version, test, and monitor. A type annotation makes them easier to maintain than unstructured prompt text.

Handwritten prompts remain useful when direct wording and low operational overhead matter most. For compound AI pipelines, contract-driven designs provide a clearer foundation across multiple model calls and evolving requirements.

However, once your application has typed outputs, frequent iterations, or several maintainers, DSPy signatures give you a clearer contract to version, test, and monitor. Choose handwritten prompts for small, stable, provider-specific calls, and contract-driven designs for typed outputs, multiple calls, frequent iteration, and multiple maintainers.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *