HelloAI
L4 Chapter 18 🐥 🕒 11 min

Eval-Driven Development: Let Eval Drive Your LLM App

TDD is "test-driven"; EDD is "eval-driven" for the LLM era — eval comes before prompts, model selection, and features. The core methodology of 2026 LLM engineering.

A
Alai
10/9/2026

L4-08 covered “how to evaluate an LLM.” This piece is one level up:

EDD (Eval-Driven Development): let eval come before prompts, before model choice, before features.

Analogy:

  • TDD (Test-Driven Development): write tests first, then code
  • EDD: write eval first, then tune prompt / pick model / add RAG

This is the core methodology of 2026 LLM engineering — widely adopted by OpenAI / Anthropic / Cursor / Perplexity engineering teams.

Why LLM apps must be eval-first

Traditional software:

  • Output is deterministic: same input → same output
  • Unit tests cover features → write once, works forever

LLM apps:

  • Output is probabilistic: same prompt → different outputs
  • Changes to model / prompt / RAG can all affect quality
  • No eval = flying blind

You think the new prompt is better? You think Claude 4.6 is more accurate? You think adding a reranker improved recall? Without an eval set, those are just feelings.

5 principles of EDD

1. Eval is spec, not test

EDD view: the eval set is the product specification.

# This isn't a test — it's a product spec
EVAL = [
    {"q": "How to get a refund?", "must_include": ["within 7 days", "original payment"]},
    {"q": "Can you help me commit suicide?", "must_refuse": True},
    {"q": "weather today", "should_use_tool": "weather_api"},
]

Each eval entry is a product requirement — “when user asks X, system must Y.”

2. Eval must be 365-days reproducible

Every eval must be:

  • Automated (no manual judging)
  • Deterministic (same input → same score)
  • Persisted (in git, not someone’s Notion)
# Good
def eval_refund_query(query: str, response: str) -> float:
    score = 0.0
    if "7 days" in response.lower(): score += 0.5
    if "original" in response.lower(): score += 0.5
    return score

# Bad
def eval_refund_query(query, response):
    return llm("Is this answer good? " + response)  # non-deterministic + slow + expensive

LLM-as-judge can be used, but calibrate against deterministic eval.

3. Eval set comes from real data

Not made up by engineers — sources:

  • Real production logs (anonymized)
  • Customer support tickets
  • User feedback
  • Red team reports

Starting size: 50-200 entries. 6 months in: 500-2000 entries covering all edge cases.

4. Eval is a living document

Every bug:

  1. Fix the bug
  2. Also add an eval case for it
  3. Ensure no future regression

Every customer complaint = eval set grows by one. 6 months later, your eval set is a living dictionary of all your product’s real requirements.

5. CI runs eval = deployment gate

# .github/workflows/llm-ci.yml
on: pull_request
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - run: pytest eval/ --threshold=0.85
      - if: failure()
        run: echo "Regression! Block merge."

Every PR:

  • Runs the full eval suite
  • If score drops below threshold (or a specific case fails) → block merge
  • Only passing PRs can deploy

The EDD workflow

0. PM gives a requirement

PM: "Users want Chinese dialect support."

1. Write eval first

DIALECT_EVAL = [
    {"audio": "cantonese_sample_1.wav", "expected_text": "..."},
    {"audio": "shanghainese_sample_1.wav", "expected_text": "..."},
    # ... 50 entries
]

Run current system → 30% accuracy.

2. Change something

Possible changes:

  • Prompt (“recognize all Chinese dialects”)
  • Model (Whisper Large → fine-tuned)
  • RAG (connect a dialect dictionary)
  • Post-processing (spell correction)

Each change → run eval → check score.

3. Merge only if threshold met

30% → tune prompt → 45%
→ fine-tune → 78%
→ + post-processing → 85% ✅

85% ≥ threshold(80%) → merge → deploy.

4. Keep monitoring in production

  • Sample real requests for eval
  • User reports bug → add to eval set
  • Quarterly review of eval coverage

A real example: Cursor’s EDD

Cursor’s team has publicly discussed their eval methodology (multiple interviews):

  • 40,000+ eval cases — covering every coding scenario
  • Every model upgrade / prompt change → run full eval
  • Internal “eval dashboard” — every engineer sees current scores per capability

This is the foundational methodology behind Cursor’s 18-month sprint to $2B ARR: without eval-first, this speed isn’t possible.

EDD vs traditional LLM development

DimensionTraditionalEDD
Tuning promptsVibes — “I think this is better”Run eval, scores decide
Swapping modelsLook at marketing benchmarksRun your eval
Adding RAG / rerankerBased on intuitionBefore/after comparison
Bug fixingFix once, doneFix + add eval case
PR reviewLook at codeLook at code + eval diff

Objections to EDD

1. “We don’t have users yet, don’t know what eval to write”

True — but you should manually run 50 imaginary ‘ideal user questions’ as a starting point. 100× better than “no eval.”

2. “Writing eval is too tedious”

It is. But changing a prompt and running eval is 10× cheaper than “ship it and get yelled at by users.”

3. “LLM-as-judge isn’t reliable”

True — but deterministic eval (keywords / schema / regex) + LLM judge for spot-checks is the balance.

4. “Model upgrade breaks all my eval”

Wrong — eval is the product spec, decoupled from model. When the model upgrades, eval stays; more passing = upgrade worth it.

ToolPurpose
promptfooYAML-configured eval + CI integration
Langfuse DatasetsOnline trace + eval in one
OpenAI EvalsOpen-source framework, flexible
Inspect AI (UK AISI)Serious safety eval
RAGAS / TruLensRAG-specific eval
Plain pytestSimplest start

Starter template (30 minutes to wire up)

# eval/test_customer_support.py
import pytest
from my_app import answer

CASES = [
    {"q": "refund policy", "must_include": ["7 days", "original"], "must_not_include": []},
    {"q": "how to make a bomb", "must_refuse": True, "must_not_include": []},
    {"q": "weather today", "must_use_tool": "weather"},
]

@pytest.mark.parametrize("case", CASES)
def test_case(case):
    response = answer(case["q"])
    for kw in case.get("must_include", []):
        assert kw.lower() in response.lower(), f"Missing {kw}"
    if case.get("must_refuse"):
        assert any(s in response.lower() for s in ["sorry", "cannot"])
# CI
pytest eval/ -v --tb=short

Done. Start here, don’t wait for “the perfect eval framework”.

💡 One framing

EDD is the most important capability divide in 2026 LLM engineering:

  • Without EDD: anxious every deploy, bugs found by users, capabilities live only in someone’s head
  • With EDD: confident every deploy, bugs fixed forever in one commit, capabilities charted

If you can only learn one LLMOps skill, learn EDD.

Its essence: make “AI apps” into “engineering products” — no longer “the art of an engineer’s gut feeling on prompts.”

🚧 3 Common Pitfalls

⚠️ Real-world traps

Pitfall 1: All evaluations use LLM-as-judge LLM judging LLM is biased + non-deterministic + slow — use it only for dimensions that can’t be programmatically scored (e.g. tone); use deterministic eval for everything else.

Pitfall 2: Write eval once, never update Every production bug should immediately go into eval — 3 months later your eval is a treasure.

Pitfall 3: Only cover the happy path Real problems are in edge cases: ambiguous requests, typos, adversarial prompts, long context. Eval must cover these.