ai_ml / evaluation / 02_building_eval_sets.md

Building eval sets

6 interview angles 5 min read source

Building eval sets

The unglamorous work that determines whether everything else is meaningful. A hundred good examples beats any framework.

Where cases come from

Source Quality Note
Real user queries from logs best reflects the actual distribution
Support escalations and complaints best these are your known failures
Generated from your documents by an LLM decent must be human-checked
Hand-written by domain experts good expensive, covers edge cases
Public benchmarks poor for your task contaminated, generic

Start with production logs. Sample real queries, stratified across intents and difficulty, not just the head. Synthetic-only eval sets test the system against questions you imagined rather than questions people ask, and they systematically miss the messy ones.

What a case looks like

class EvalCase(BaseModel):
    id: str
    query: str
    context: dict = {}                 # user state, conversation history
    expected: str | None = None        # for deterministic checks
    expected_chunks: list[str] = []    # for retrieval recall
    must_contain: list[str] = []       # required facts
    must_not_contain: list[str] = []   # forbidden content
    should_refuse: bool = False
    rubric: str | None = None          # for a judge
    tags: list[str] = []               # intent, difficulty, source

Tags matter more than they look: they let you report per-segment rather than one aggregate, which is how you notice the system got worse for one intent while the average improved.

Coverage that catches real failures

Deliberately include:

  • The head — the common cases, weighted as they occur.
  • The tail — rare but important intents.
  • Ambiguous queries where the right behaviour is to ask a clarifying question.
  • Unanswerable queries — the corpus genuinely doesn’t contain the answer. Tests whether the system says so or confabulates. This category is the one most often missing, and it’s where trust is lost.
  • Adversarial — injection attempts, jailbreaks, off-topic requests.
  • Multi-turn — follow-ups that only make sense in context (“what about the second one?”).
  • Long inputs at realistic lengths, not toy ones.

Sizing

Size Good for
20-30 smoke test in CI on every commit
100-200 the working set — statistically meaningful, affordable to run
500+ release gates, model migrations

100-200 is the sweet spot. Enough that a few-point change isn’t noise, small enough to run in minutes for a few dollars.

Run the smoke set on every change and the full set before merging or upgrading a model.

Deterministic checks first

Before reaching for a judge, extract everything checkable:

def deterministic_checks(case: EvalCase, output: str) -> dict[str, bool]:
    return {
        "parses": try_parse_json(output) is not None,
        "schema_valid": validate_schema(output),
        "contains_required": all(s in output for s in case.must_contain),
        "avoids_forbidden": not any(s in output for s in case.must_not_contain),
        "has_citation": bool(CITATION_RE.search(output)),
        "refused": is_refusal(output) == case.should_refuse,
        "within_length": len(output) < MAX_LEN,
    }

These are free, fast, deterministic and catch a large share of real regressions. A judge is for what’s left.

Statistical honesty

from scipy.stats import beta

def score_ci(passes: int, n: int, conf: float = 0.95) -> tuple[float, float]:
    """Wilson-style interval via the beta distribution. Report this, not a bare rate."""
    lo = beta.ppf((1 - conf) / 2, passes, n - passes + 1) if passes else 0.0
    hi = beta.ppf(1 - (1 - conf) / 2, passes + 1, n - passes) if passes < n else 1.0
    return lo, hi

On 100 cases, an 85% pass rate has a confidence interval of roughly ±7 points. An 85% → 88% “improvement” is noise. Reporting a bare percentage without an interval is how teams ship changes that did nothing.

If you need to detect small differences, either grow the set or use paired comparison — run both variants on the same cases and count wins, losses and ties, which has far more statistical power than comparing two independent rates.

Versioning

The eval set lives in the repo, in version control, alongside the code.

  • Cases are append-mostly. Deleting a case because it started failing is how a suite stops catching regressions.
  • Record the model version with every result. “Score dropped” is meaningless without knowing what changed.
  • Store outputs, not just scores. When something regresses you want to read what it actually produced, and re-scoring old outputs under a new rubric is often useful.

The feedback loop

production failure -> triage -> add as eval case -> fix -> case passes
                                       |
                                 stays in the suite forever

This is the mechanism that makes a system improve. A complaint that doesn’t become a test case will recur.

Weekly triage of production failures into the eval set costs an hour and compounds. It’s a process answer rather than a technical one, and it lands well in interviews for exactly that reason.

Interview angle

  • “How do you build an eval set for an LLM feature?” — sample real production queries stratified by intent and difficulty, add known failures from escalations, and deliberately include unanswerable and adversarial cases. 100-200 cases is the practical working size.
  • “How many examples do you need?” — enough that your effect size exceeds the confidence interval. At 100 cases the interval is around ±7 points, so small differences need paired comparison or a bigger set.
  • “What do you check without a judge?” — parseability, schema validity, required and forbidden strings, citation presence, refusal behaviour, length. Deterministic, free, and they catch most real regressions.
  • “Your eval score went from 85% to 88%. Ship it?” — not on that alone. On 100 cases that’s within noise. Run a paired comparison on the same cases, or expand the set.
  • “What’s the most commonly missing category in eval sets?” — unanswerable queries. Without them you never measure whether the system admits ignorance or confabulates, which is where user trust actually breaks.
  • “How does an eval set stay relevant?” — triage production failures into it regularly. Cases are append-mostly; removing a failing case to make the suite green defeats the purpose.