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.
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:
- Fix the bug
- Also add an eval case for it
- 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
| Dimension | Traditional | EDD |
|---|---|---|
| Tuning prompts | Vibes — “I think this is better” | Run eval, scores decide |
| Swapping models | Look at marketing benchmarks | Run your eval |
| Adding RAG / reranker | Based on intuition | Before/after comparison |
| Bug fixing | Fix once, done | Fix + add eval case |
| PR review | Look at code | Look 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.
Recommended tools
| Tool | Purpose |
|---|---|
| promptfoo | YAML-configured eval + CI integration |
| Langfuse Datasets | Online trace + eval in one |
| OpenAI Evals | Open-source framework, flexible |
| Inspect AI (UK AISI) | Serious safety eval |
| RAGAS / TruLens | RAG-specific eval |
| Plain pytest | Simplest 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”.
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.”
Recommended further reading
- HelloAI L4-08 LLM Evaluation
- HelloAI L7-09 LLMOps Panorama
- HelloAI L4-17 Adaptive RAG
- Hamel Husain on Evals (industry classic)
- Langfuse Datasets docs
🚧 3 Common Pitfalls
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.