# Meta-Prompting and Automatic Prompt Optimization: A DSPy Guide

> Source: https://sukruyusufkaya.com/en/blog/meta-prompting-dspy-otomatik-optimizasyon
> Updated: 2026-08-05T08:09:31.783Z
> Type: blog
> Category: yapay-zeka
**TLDR:** Moving beyond brittle hand-written prompts: a practical guide to meta-prompting and metric-driven, programmatic prompt optimization with DSPy.

**TL;DR —** If a hand-written prompt works beautifully on one model and falls apart on another, if you find yourself re-doing "prompt engineering" from scratch for every new use case, and if everyone on your team writes a slightly different prompt for the same task, this piece is for you. Meta-prompting means using an LLM to write, critique, or improve another LLM's prompt. Automatic prompt optimization turns this into something systematic: you search over prompt variants against a small labeled dataset and a metric, and you keep the one that scores best. DSPy, an open-source framework, turns this idea into a programming model: instead of writing a prompt, you declare what you want with a **signature**, you choose how the model should reason with a **module**, and you let an **optimizer** (a "teleprompter") automatically **compile** your program against your data and your metric. In this piece I walk through the conceptual framework, DSPy's concrete building blocks, and how an enterprise team should roll this out step by step — including what to watch for: overfitting, cost, reproducibility, and KVKK (Turkey's personal data protection law) considerations.

## Why I'm writing about this

Over the last two years, in corporate trainings and consulting projects, I keep watching the same scene play out. A team member spends weeks crafting the perfect prompt, the system goes live, everyone is happy. Then the model provider ships an update, or the team decides to switch from a GPT-4-class model to a cheaper one, or simply the distribution of input data shifts a bit — and that "perfect" prompt suddenly starts producing inconsistent answers. The team reopens the prompt, adds a sentence, removes an example, tries again, hopes for the best. Every time I see this cycle, it reminds me of something: hand-writing prompts is essentially eyeballing regression tests. It works, but it doesn't scale and it's fragile.

What I'm going to describe here is not a magic wand. Meta-prompting and automatic prompt optimization are an attempt to take prompt-writing out of the realm of manual craft and move it, at least partially, into a measurable, repeatable engineering discipline. The most mature, most concrete tool for this today is DSPy. I'll try to give equal weight to the "why" and the "how" in this piece, because the biggest mistake I see teams make is adopting these tools "without justification" — simply because they're trending.

## The fundamental problem with hand-written prompts

I don't want to belittle prompt engineering — a good prompt is still valuable, still necessary. But hand-written, "hardcoded" prompts carry a few structural weaknesses:

**Model dependency.** Hints specific to one model ("think step by step," a particular formatting style, particular word choices) may not work on another model, and can even hurt. A "You are an expert..." pattern that one model responds to very well can waste tokens on another. This is a serious maintenance burden for enterprise teams running a multi-model strategy — using different models for different tasks to optimize cost.

**Task dependency.** A prompt structure optimized for a classification task doesn't transfer directly to a summarization task. Every new task means starting from trial and error again.

**Invisible context loss.** Once the person who wrote a prompt leaves, the reasons why the prompt looks the way it does — which attempts failed, which sentence was added for which edge case — are usually lost. The prompt sits there like an "undated commit."

**Scale problem.** Manual prompt improvement is bounded by one person's attention. You can iterate on dozens of examples saying "this looks better," but doing a systematic, statistically meaningful comparison over hundreds of examples by eye simply isn't practical.

**Absence of A/B testing at the prompt level.** In web products we A/B test every button color, yet we often ship prompts to production based on a "this felt right to me" intuition. But a prompt is also a product, and it needs to be measured.

The common thread across these four problems is this: writing a prompt is, fundamentally, an **optimization problem**. We have a search space (the set of possible prompt variants), an objective function (how well it performs the task), and constraints (token cost, latency). The human brain performs this search by hand, with a limited sample, slowly. The question is: can we partially automate this search?

## Meta-prompting: using an LLM on an LLM

Meta-prompting, in its simplest definition, is using a language model to generate, critique, or improve a prompt for another language model (or for itself). The idea isn't new — "writing a prompt to write a prompt" emerged in practice shortly after ChatGPT became widespread, and it has also been formalized academically in various works (for example, studies like "Automatic Prompt Engineer" propose having an LLM generate instructions and then score those instructions against a metric).

In practice, meta-prompting shows up in a few forms:

**1. Generative meta-prompting.** You give the model the task definition (input-output examples, constraints), and the model produces a draft system prompt for that task. For example: "Look at these 10 customer complaints and the ideal short responses given to them, and write a system prompt that would consistently produce responses in this style."

**2. Critical meta-prompting (self-critique / reflexion).** The model first produces an answer, then it (or another model) is asked "where is this answer weak, and what instruction gap caused the mistake?" The resulting critique is fed back into the prompt. This is a pattern seen in "Reflexion" and similar work.

**3. Comparative meta-prompting.** Two or more prompt variants are run on the same inputs, a "judge" (either a model or an automatic metric) decides which one is better, and new variants are generated based on the winner. This is a "mutation + selection" loop, similar to genetic algorithms.

The power of meta-prompting lies here: by partially delegating the writing of prompts to a model, you let the model itself try wording variations (word order, example selection, where an instruction sits) that a human might overlook. But meta-prompting on its own is not a **process** — it's a **technique**. To turn it into a systematic, measurable loop, we need the concept of automatic prompt optimization.

## Automatic prompt optimization: search + metric

Automatic prompt optimization has three core components:

1. **A search space** — the set of prompt/instruction/example combinations you can try.
2. **A metric (reward function)** — an automatically computable function that measures how "good" a prompt variant is (accuracy, F1, an LLM-judge score, a rule-based check, etc.).
3. **A search strategy** — how you traverse this space: random trial, hill climbing, beam search, Bayesian optimization, or guided search based on a meta-LLM's suggestions.

The logic goes like this: assume you have a small but representative **labeled dataset** (a train/dev split) — say, 50-200 examples, each containing an input and the "correct" or "acceptable" output for that input. The automatic optimization process runs different prompt variants against this dataset, scores them with the metric, and selects the best-performing variant(s), or generates new ones based on them. This closely resembles classic hyperparameter search in machine learning — except here, the "hyperparameter" isn't a number, it's a piece of text (an instruction sentence, a few-shot example selection, a reasoning format).

There's a critical point here: **automatic prompt optimization isn't a "magic" replacement for hand-writing prompts — it's a search engine that's only as good as the quality of your evaluation infrastructure.** If your metric is weak (for example, if it doesn't reflect the actual business goal), the optimization process will find a prompt that's "perfect according to the metric but useless in the real world." That's why this topic cannot be separated from evaluation — I'll return to that shortly.

## What DSPy is, and why it proposes a different mindset

DSPy (an open-source Python framework developed by the Stanford NLP group) proposes that we approach prompt engineering not as "string editing" but as "writing and compiling a program." The name itself hints at this ("Declarative Self-improving Python" in some sources; often summarized as "programming, not prompting"). The mindset shift is this:

- **Traditional approach:** "Write this prompt string, try it, manually fix it, try again."
- **DSPy approach:** "Declare the input-output behavior of the task (signature). Choose the reasoning strategy you'll use to accomplish that behavior (module). Using a small labeled example set and a metric you already have, automatically 'compile' this program — meaning, let the optimizer adjust the instructions and few-shot examples for you."

This resembles the analogy in software engineering of thinking of the prompt as source code and the LLM's output as compiled binary. You declare high-level intent; the "compiler" (optimizer) figures out the low-level details (the exact instruction text, which examples to show, in what order) on your behalf.

Now let's go through DSPy's three core building blocks one by one: signature, module, and optimizer (teleprompter).

### 1. Signature: declaring behavior, not how to achieve it

A signature defines the input and output fields of an LLM call, along with a short natural-language description. The goal is to answer "what should this task do" — not "how should it do it."

A simple signature can be written as an inline string:

```python
import dspy

# a simple signature in the form "question -> answer"
qa = dspy.Predict("question -> answer")
```

When you need richer, field-level descriptions, a class-based signature is used:

```python
class CustomerComplaintClassification(dspy.Signature):
    """Classify a customer complaint into predefined categories and determine its urgency level."""

    complaint_text: str = dspy.InputField(desc="The raw complaint text written by the customer")
    category: str = dspy.OutputField(desc="One of: Billing, Technical, Refund, Other")
    urgency: str = dspy.OutputField(desc="Low, Medium, High")
```

Notice that there are no instructions here like "think step by step," "keep the answer short," or "write in JSON format." The signature defines the contract; the exact wording of the instruction, which words get used, is determined by DSPy itself (or by the optimizer). This is the biggest break from traditional prompt writing: you clearly declare "what" you want, and you leave the decision of "how" it gets expressed to the system.

### 2. Module: choosing the reasoning strategy

If the signature answers the "what" question, the module answers the "how should it reason" question. There are a few core modules that stand out in DSPy:

**`dspy.Predict`** — the simplest module. It converts a signature directly into an LLM call, without adding any extra reasoning step. It's suitable for simple classification, short inference, and format conversion tasks.

**`dspy.ChainOfThought`** — automatically adds a "reasoning" (rationale) field to the model before it produces an answer. So even if your signature only defines `question -> answer`, ChainOfThought adds an invisible `rationale` field behind it and asks the model to produce its reasoning first, then its answer. For tasks that require multi-step reasoning (math, logic, multi-step inference), this delivers a noticeable improvement over `Predict`.

```python
answer_module = dspy.ChainOfThought("question -> answer")
result = answer_module(question="Under what condition does the early-termination fee clause in this contract apply?")
print(result.answer)
print(result.rationale)  # the intermediate reasoning the model produced
```

**`dspy.ReAct`** — implements the "Reasoning + Acting" think/act loop: the model produces a thought, calls a tool (a function), observes the result, thinks again, calls a tool again if needed, and finally gives the answer. This is used for agent-style tasks that require access to external tools — search engine queries, database queries, calculators, and the like.

```python
def get_customer_record(customer_id: str) -> str:
    """Connects to the customer record system and returns customer info (example function)."""
    ...

agent = dspy.ReAct("question -> answer", tools=[get_customer_record])
```

Beyond these, there are also modules like `dspy.MultiChainComparison` (which produces multiple reasoning chains and compares them) and `dspy.ProgramOfThought` (which reasons by generating code), but the three core building blocks are the ones above.

You can chain and combine modules in Python like ordinary functions/classes — DSPy programs are generally written as a subclass of `dspy.Module`, where multiple modules are used sequentially or nested:

```python
class ComplaintProcessor(dspy.Module):
    def __init__(self):
        super().__init__()
        self.classify = dspy.Predict(CustomerComplaintClassification)
        self.generate_reply = dspy.ChainOfThought("complaint_text, category -> suggested_reply")

    def forward(self, complaint_text):
        classification = self.classify(complaint_text=complaint_text)
        reply = self.generate_reply(
            complaint_text=complaint_text,
            category=classification.category,
        )
        return dspy.Prediction(
            category=classification.category,
            urgency=classification.urgency,
            suggested_reply=reply.suggested_reply,
        )
```

Notice that at no point did we write any "prompt text" yet. We only defined the behavior and the flow architecture. The actual text of the prompt — the instruction sentences, the examples — will emerge once the optimizer comes into play.

### 3. Optimizer / Teleprompter: "compiling" the program against data

This is where DSPy's real power lies. The "teleprompter" (in newer versions mostly referred to as "optimizer") is the component that takes a small labeled dataset and a metric function, and automatically searches for and improves the instructions and/or the few-shot examples in your program's modules. There are a few important types of optimizers:

**`BootstrapFewShot`** — starting from a handful of labeled examples you have, it runs the program on these examples, and for the runs deemed "successful" according to the metric, it also includes the intermediate steps (for example, the rationale that ChainOfThought produced) to automatically produce few-shot demonstrations. In other words, instead of you hand-writing "here are three examples, this is how it should respond," the system "distills" (bootstraps) examples from its own successful runs and automatically adds them as few-shot examples to the prompt.

**`BootstrapFewShotWithRandomSearch`** — on top of the above, it adds a layer that randomly tries different few-shot example combinations and selects the one that performs best on the dev set.

**`COPRO`** (Coordinate Prompt Optimization) — improves the instruction text itself (independent of the few-shot examples) using a coordinate-descent-like search; it uses an LLM as a "meta-optimizer" to generate variants of the current instruction, scores them on the dev set, and keeps the best one.

**`MIPRO` / `MIPROv2`** (Multi-prompt Instruction Proposal Optimizer) — one of DSPy's most advanced optimizers today, which jointly optimizes both the instruction text and the few-shot example selection using a strategy similar to Bayesian optimization. It analyzes the characteristics of your dataset (the distribution of example inputs and outputs) to produce more informed instruction proposals; this "informed proposal generation" step is itself essentially a use of meta-prompting — an LLM generates instruction candidates for your program.

Here's what concrete usage looks like:

```python
from dspy.teleprompt import BootstrapFewShotWithRandomSearch

def accuracy_metric(example, prediction, trace=None):
    correct_category = example.category == prediction.category
    correct_urgency = example.urgency == prediction.urgency
    return correct_category and correct_urgency

optimizer = BootstrapFewShotWithRandomSearch(
    metric=accuracy_metric,
    max_bootstrapped_demos=4,
    num_candidate_programs=10,
)

compiled_program = optimizer.compile(
    ComplaintProcessor(),
    trainset=train_examples,   # 50-150 labeled examples
    valset=dev_examples,       # a separate validation set
)
```

Behind the scenes, the `compile` call does this: it tries different instruction/few-shot combinations, scores each attempt against `train_examples`/`dev_examples` using `accuracy_metric`, selects the combination that yields the best score, and embeds that into the program. As a result, you now have a prompt/program pair that's automatically tuned to the task and data you defined — one you never hand-wrote. You can save this "compiled" program to disk (`compiled_program.save(...)`), version it, and load it in production.

I want to underline one point here: DSPy's optimizers do not change the underlying LLM's weights (they don't do fine-tuning — although there are some advanced optimizers that support fine-tuning in certain scenarios, the core usage operates at the prompt/demonstration level). What it does is maximize output quality against your metric by giving the same frozen model different instruction texts and different few-shot examples. In other words, you're not doing "model training" — you're doing "prompt/demonstration search and selection."

## Where meta-prompting fits inside DSPy

I want to clarify the link here, because it's often confused. Meta-prompting is a **part of the internal mechanism** of DSPy's optimizers. For example, COPRO and MIPRO, when generating new instruction candidates, send an LLM a meta-prompt along the lines of "here's the current instruction, here are some successful/unsuccessful examples, propose a better version of this instruction." So when you use DSPy, under the hood you're actually using an automated, systematic, and metric-driven version of meta-prompting. DSPy takes meta-prompting from "try it once by hand" up to "generate dozens of variants, score all of them objectively, and select the statistically best one."

## When this approach pays off, and when it's overkill

When I explain this in a training, the most natural question that comes up is: "Should we use DSPy for every prompt?" The answer is definitely no. Below is a rough framework I use when deciding.

| Situation | Recommendation |
|---|---|
| One-off, low-risk task (a simple internal summary) | A simple, hand-written prompt is enough |
| Task is fixed, model is fixed, volume is low | A hand-written prompt plus a few iterations is enough |
| Task runs at high volume in production, cost of errors is high | Automatic optimization is valuable |
| Same task will run across multiple models (switching models for cost/performance optimization) | Automatic optimization is very valuable — you don't need to re-search the prompt for every model |
| Complex multi-step agent (tool calling, multi-module pipeline) | DSPy's module + optimizer combination adds real value |
| You don't have a labeled dataset/eval set and don't want to invest in building one | Automatic optimization is meaningless for now — build the eval set first |
| Task changes frequently (new category, new rule every week) | Be careful: there's a recompilation cost on every change; a simple prompt plus human oversight may be more agile |

In short: automatic prompt optimization is meaningful for tasks with **volume, repeatability, and a measurable definition of success**. For one-off, low-volume work subject to subjective evaluation (like a marketing copy draft), hand-writing the prompt is still the fastest, most practical path.

## No optimization without evaluation

I need to emphasize this under its own heading, because it's the step most often skipped in the field: **the prerequisite for automatic prompt optimization is a good evaluation infrastructure.** The `metric` parameter you pass to the `compile` function in DSPy is the compass for the entire search process. If that compass is wrong, no matter how sophisticated an optimizer you use, the result will optimize in the wrong direction.

In practice, to build a good metric/eval set you need:

- **A representative sample.** A set of at least 50-100 examples that reflects the diversity of inputs you'll encounter in production (different customer profiles, different registers of language, edge cases). A set that's too small (10-15 examples) will cause the optimizer to overfit to "noise."
- **A clear definition of "correct."** For some tasks this is easy (classification accuracy is a clear metric). For others (free-text generation, tone, creativity) it isn't — here you generally use an "LLM-as-a-judge" approach or rule-based partial checks (whether certain key facts appear in the text, whether prohibited phrases are absent, and so on).
- **A train/dev (validation) split.** Running the optimizer only on the `trainset` and measuring success on the same data produces a program that's "perfect" in the lab but weak in the real world. A separate `valset` (and, ideally, a separate, untouched test set) is essential.
- **Regular updates.** Production data changes over time (new product categories, new types of customer complaints). If you leave your eval set static, the program you're optimizing drifts further and further from reality.

That's why the first question I always ask before adopting DSPy is: "Do you have a metric that can objectively score this task, and at least a few dozen labeled examples?" If not, that's where the investment needs to go first — not into DSPy.

## A practical workflow for an enterprise team in Turkey

Here's the step-by-step flow I recommend to the teams I consult with:

**1. Clarify the task and the definition of success.** An abstract definition like "categorize customer emails" isn't enough. Set a concrete, measurable goal, such as "match the category and urgency level with the human annotator's label at above 90%."

**2. Build a small but realistic eval set.** Select 50-150 examples from real production data (anonymized or masked where possible) and have at least one human expert label them. Here's an important note for teams operating in Turkey: if you include real customer data (names, national ID numbers, phone numbers, addresses, and other personal data) in your eval set, this counts as personal data processing under KVKK (Turkey's Personal Data Protection Law). Where possible, mask/anonymize the data, clarify your legal basis if needed (explicit consent, legitimate interest, etc.), and before sending your eval set to a third-party optimization service (especially one hosted abroad), check your data-processing inventory and any applicable KVKK compliance process.

**3. Start with a simple baseline.** In your first version, use `dspy.Predict` or a simple hand-written prompt, and measure current performance on your eval set. This will be your reference point for answering the question "does optimization actually make a difference?"

**4. Choose the module architecture.** If the task is simple, use `Predict`; if it requires reasoning, use `ChainOfThought`; if it needs access to external systems (CRM, knowledge base, calculator), use `ReAct`.

**5. Code the metric.** Prefer a rule-based, objective metric where possible (regex checks, field matching, numerical accuracy). If that's not possible, use an LLM-judge, but also separately validate the judge's own prompt (check on a small sample how well the judge agrees with human evaluation).

**6. Run the optimizer, but start with a cheap one.** Start with a relatively lightweight optimizer like `BootstrapFewShot`; if the results aren't satisfying, move on to a more comprehensive (and more costly) optimizer like `MIPROv2`.

**7. Validate on the test set.** Measure final performance on a test set the optimizer has never seen. If the dev set score is close to the test set score, that's a sign there's no overfitting.

**8. Version the compiled program.** Commit the program written to disk via `save()` to git (or a model/prompt registry) like code, and note which dataset and which optimizer parameters it was produced with.

**9. Monitor in production.** After going live, periodically sample real usage data, add new examples to your eval set, and re-measure performance regularly.

## A concrete end-to-end pseudocode example

Below is a simplified flow that brings all the pieces together (read it as a simplified reflection of the real DSPy API):

```python
import dspy

# 1) Configure the language model (example: an LLM provider endpoint)
lm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=lm)

# 2) Define the task with a signature
class ContractSummarization(dspy.Signature):
    """Summarize a legal contract text in plain language, in 3-5 sentences,
    without losing the parties' key obligations."""

    contract_text: str = dspy.InputField()
    summary: str = dspy.OutputField(desc="A plain-language summary of 3-5 sentences")

# 3) Choose the module architecture
summarizer = dspy.ChainOfThought(ContractSummarization)

# 4) Define the metric (example: a mix of a rule-based check for whether
# key clauses appear in the summary, plus an LLM-judge)
def summary_metric(example, prediction, trace=None):
    has_key_terms = all(
        term.lower() in prediction.summary.lower() for term in example.required_key_terms
    )
    reasonable_length = 1 <= prediction.summary.count(".") <= 6
    return has_key_terms and reasonable_length

# 5) Prepare train and validation sets (human-labeled, real/anonymized examples)
train_examples = [...]   # a list of dspy.Example, e.g. 80 examples
dev_examples = [...]     # e.g. 30 examples
test_examples = [...]    # 30 examples the optimizer has never seen

# 6) Compile the program with an optimizer
from dspy.teleprompt import MIPROv2

optimizer = MIPROv2(metric=summary_metric, auto="medium")
compiled_summarizer = optimizer.compile(
    summarizer,
    trainset=train_examples,
    valset=dev_examples,
)

# 7) Independent validation on the test set
success_rate = sum(
    summary_metric(example, compiled_summarizer(contract_text=example.contract_text))
    for example in test_examples
) / len(test_examples)

print(f"Test set success rate: {success_rate:.2%}")

# 8) Save and version the compiled program
compiled_summarizer.save("contract_summarizer_v1.json")
```

Notice what's missing from this flow: at no point did we write "here's the exact prompt text." We defined the task through the signature, module, and metric; the `compile` step produced/selected the instruction text and the few-shot examples on our behalf.

## Pitfalls: what to watch out for

**Overfitting to the eval set.** The optimizer selects the variant that scores highest on the dev set. If the dev set is small or lacks diversity, the optimizer can find odd patterns that "work on the dev set but don't generalize" (for example, if adding a particular word to every answer happens to boost the score due to a coincidental correlation in the dev set). Ways to mitigate this: a sufficiently large and diverse dev set, independent validation on a separate test set, and, where possible, cross-validation (checking consistency across k different dev set splits).

**The cost of optimization runs.** Especially comprehensive optimizers like `MIPROv2` try dozens to hundreds of instruction/example combinations by making that many LLM calls. With large models (especially expensive APIs), this can add up to meaningful cost in a single optimization run. Practical recommendation: use a cheaper/faster model during the optimization phase and validate the resulting program later on the target model, or start with the optimizer's "auto" mode set to a lower cost/scope tier (light/medium/heavy).

**Reproducibility.** LLM calls are inherently somewhat stochastic (even with temperature set to zero, there can be small variability on the provider's side). The same optimizer run on the same data may not produce a bit-for-bit identical result every time. To manage this: (a) fix the random seed, (b) save the compiled program to disk the moment you find it, and never "recompile" in production — always use the saved, fixed program, and (c) record which optimizer version, which dataset, and which model version it was compiled with (think of it like a "model card").

**Metric hacking.** Automatic optimization maximizes the metric you defined to the letter — not your intent. If your metric is simplistic, like "the word X should appear in the answer," the optimizer can "learn" to jam that word into every response regardless of context. It's essential to design the metric as close as possible to the actual business goal, and to periodically sample optimized outputs and review them by eye.

**"Black box" optimized prompts.** The final instruction text found by the optimizer sometimes doesn't give a satisfying answer to the human question of "why was this wording chosen." This can be a problem in enterprise settings that require auditability and explainability (especially in regulated sectors like finance, healthcare, or the public sector). Always put the compiled prompt through human review before taking it to production — "whatever the optimizer said, that's what it is" is not the right approach.

**Data leakage.** Make sure your train and test sets are genuinely disjoint; having the same customer record appear (in different forms) in both the training set and the test set can produce an artificially inflated success score.

## Can you use meta-prompting without DSPy

Yes — DSPy isn't the only path. As a lighter-weight approach, periodically showing a prompt you already have to a "critic" LLM (asking "what are this prompt's weaknesses, on which edge cases does it fail, how could it be improved") and getting improvement suggestions, then trying those by hand and measuring them on your eval set, is also a form of applying meta-prompting — just at a lower level of automation. You can think of this as "an intermediate step toward DSPy": first set up your eval set and your metric, try meta-prompting in a semi-manual way, and as the process matures and volume grows, move to a framework like DSPy. Automation itself isn't the goal — the goal is predictable, measurable prompt quality.

## A closing observation

In my trainings I often say this: prompt engineering was the "manual craft" phase of the early years of LLMs, and it's likely a transitional stage rather than a permanent one. Just as software engineering moved from assembly to higher-level languages, and from manual memory management to garbage collection, we're living through a similar transition from writing prompts by hand toward a model of "declaring behavior + automatic optimization." DSPy is the most concrete, most mature tool we have for this transition today. But to see the benefit of this transition, you first need to build a solid evaluation culture — for a team without metrics, without an eval set, DSPy is just a layer that adds complexity. Measure first, then automate.
