Frontier Engineering
← Evaluation & Feedback Loops

Golden Set Regression Gate

Also known as: eval suite in CI, regression testing for prompts, quality gate

Block any prompt, model, or pipeline change that regresses a curated set of known-correct cases.

9 min read

The problem

Prompts are edited constantly and their effects are non-local: a clarifying sentence added for one edge case silently breaks three behaviours nobody thought to check. Without an automated gate the feedback loop is a user complaint days later, and by then several changes have shipped and nobody can say which one caused it. Manual spot-checking does not scale and does not catch what it does not think to look at.

The pattern

Maintain a curated set of inputs with known-good outputs or checkable assertions, and run it on every change to a prompt, model, or pipeline component. Score with a mix of deterministic checks and judge models, compare against the current baseline, and fail the build on regression. Every production bug found becomes a new case in the set.

Use when

  • More than one person edits prompts, or prompts change more than occasionally
  • You are changing models and need to know what the swap actually costs
  • Regressions have reached production and been discovered by users
  • You want to refactor a prompt and need a safety net to do it confidently

Avoid when

  • Pre-product-market-fit prototyping where the target behaviour changes daily
  • Purely open-ended creative output with no checkable notion of correct
  • The suite would be so small it gives false confidence — under about thirty cases

Tradeoffs

  • GainRegressions are caught before merge instead of by users days later

    CostThe set must be built and then maintained as intended behaviour legitimately changes

  • GainPrompt refactoring becomes safe, so prompts stop calcifying out of fear

    CostEvery CI run costs model calls; a large suite is a real recurring bill

  • GainModel migrations become a measurement rather than a leap of faith

    CostJudge-scored cases are noisy; thresholds must tolerate variance without hiding regressions


This is unit testing for non-deterministic systems. The mechanics differ — assertions are fuzzier and scores are distributions — but the purpose is identical: change code without silently breaking behaviour.

Cases carry their own assertions

Exact-match expectations are too brittle for generated text. Attach assertions instead, and prefer deterministic ones — they are free, fast, and never flaky. Reserve judge-based scoring for the genuinely subjective qualities that nothing else can check.

class Case(BaseModel):
    id: str
    input: str
    context: dict = {}
    tags: list[str] = []          # "refund", "multi-hop", "adversarial", "regression"
    assertions: list[Assertion]


CASES = [
    Case(
        id="refund-outside-window",
        input="I want to return something I bought 45 days ago.",
        tags=["refund", "policy", "regression"],
        assertions=[
            Contains(any_of=["30 day", "30-day"]),      # cites the actual policy
            NotContains(["I can process that refund"]),  # must not promise it
            CitesSource("returns-policy"),
            JudgeRubric("Declines clearly but offers the exchange alternative.", min_score=4),
        ],
    ),
]

Gate on the delta, not the absolute

An absolute pass-rate threshold is the wrong gate: it blocks work when the suite gets harder and permits slow decay when it does not. Compare against the committed baseline instead, allow a small tolerance for judge noise, and always block on a case that flips from pass to fail — a specific regression matters even when the aggregate looks fine.

TOLERANCE = 0.02   # absorbs judge variance, not real regressions


def gate(current: RunResult, baseline: RunResult) -> GateResult:
    newly_failing = baseline.passing_ids - current.passing_ids
    if newly_failing:
        return GateResult(False, f"regressed: {sorted(newly_failing)}")

    if current.score < baseline.score - TOLERANCE:
        return GateResult(False, f"score {current.score:.3f} < baseline {baseline.score:.3f}")

    for tag in TRACKED_TAGS:                      # aggregate can hide a broken subgroup
        if current.score_for(tag) < baseline.score_for(tag) - TOLERANCE:
            return GateResult(False, f"tag '{tag}' regressed")

    return GateResult(True, f"{current.score:.3f} vs baseline {baseline.score:.3f}")
warning

Check per-tag scores, not just the total. A change that improves twenty easy cases while breaking three adversarial ones shows up as a net gain and ships a security regression.

Grow the set from production

The best cases are the ones that already broke. Make adding a case part of fixing every production bug — the same discipline as writing a regression test — and the suite becomes an accumulating record of everything your system has learned not to do.

def case_from_incident(trace_id: str, expected: str, tags: list[str]) -> Case:
    trace = traces.load(trace_id)
    return Case(
        id=f"regression-{trace_id[:8]}",
        input=trace.user_input,
        context=trace.retrieval_context,
        tags=[*tags, "regression", "from-production"],
        assertions=derive_assertions(expected),
    )
  • Prefer deterministic assertions; use judges only for genuinely subjective qualities
  • Gate on the delta against a committed baseline, never on an absolute threshold
  • Block on any pass-to-fail flip, even when the aggregate improves
  • Track per-tag scores so a broken subgroup cannot hide inside the average
  • Add a case for every production bug, as a matter of routine
tip

Run judge-scored cases three times and take the median. Single-run judge scores are noisy enough to produce flaky gates, and a flaky gate is one people learn to override.