A version-controlled judge harness that turns "the model feels worse this week" into a red CI check with a diff you can point at.
An eval platform is a compiler for model quality: it takes a frozen set of inputs (the golden set), runs your system against them, scores each output with deterministic checks plus LLM judges, and emits a single verdict — pass, regress, or improve — that a human or a CI gate can act on. The hard parts are not the judges; they are the boring distributed-systems parts wrapped around judges: making runs reproducible, caching judge calls so a 5,000-case suite costs cents instead of dollars, detecting a real regression versus judge noise, and isolating twelve teams on one shared budget. The mental model: treat every eval run as an experiment keyed by (dataset_version, system_version, judge_version), store the per-case results immutably, and compare versions as deltas — never absolute scores in a vacuum. If you can't answer "what changed and by how much, beyond noise" in one query, you have a dashboard, not an eval platform.
The words first.
Step by step.
(input, output, judge_version).Remember this: an eval platform measures deltas between versions, and the judge is an instrument you must calibrate before you trust its readings.
The single highest-leverage artifact is the golden set, and it is the one thing you should never outsource or auto-generate carelessly. Per the Confident AI playbook and Comet's LLM-as-judge guide, the working recipe is 30–50 expert-annotated examples per judge dimension, segmented by use case, user segment, and failure mode, with a target of under 20% annotator disagreement on clear cases. That last number is your sanity floor: if humans can't agree on the easy cases, no judge will, and the dimension is underspecified.
Treat golden sets as versioned data, not config. Each case is {id, input, reference_output?, metadata, labels, dimension} and lives in a content-addressed store so a run can pin dataset_version = sha256(sorted cases). This is non-negotiable for reproducibility: a regression alarm is meaningless if you can't prove the dataset didn't move underneath you. Golden sets also rot — a policy document from 2021 contradicts its 2026 version, and a case whose "correct" answer changed becomes a silent false-positive regression. Build a review cadence and track per-case last-validated dates.
A subtle trap: golden sets must be balanced toward failure modes you've actually seen, not toward easy happy-path cases. A suite that's 95% trivial queries will report 96% pass and tell you nothing the day a real bug ships. Pull hard cases from production traces (online → offline pipeline) so the static set tracks the live distribution.
There are three judge architectures and you'll use all three for different jobs:
The discipline most candidates skip: a judge is a model that can be wrong, so you measure its error. Build a calibration set where you have human labels, run the judge, and compute agreement. The rubric design rules that move agreement: 3–5 dimensions per judge call maximum (more and quality collapses), few-shot examples with scores (consistency jumps), separate specialized judges for distinct concerns (a hallucination judge and a tone judge should be different prompts), and start binary pass/fail before numeric scales — a judge that can't reliably split pass from fail has no business emitting a 7.3/10.
Symbols in plain words: agreement = fraction of calibration cases where the judge's verdict matches the human label. noise band = how much two runs of the same system can differ purely from judge randomness. We use these to decide if a score drop is real or just the judge flickering.
Concrete example. Calibration set = 100 human-labeled cases. The judge matches the human on 82 of them → agreement = 0.82. Now run the unchanged system through the judge twice on the 500-case golden set. Run A passes 451/500 = 0.902. Run B passes 446/500 = 0.892. The runs differ by 0.010 with nothing changed — that's judge noise. Repeat a few times and you find run-to-run pass-rate swings sit within about ±0.012. So your noise band ≈ 1.2 percentage points.
Decision rule that falls out: a new version that scores 0.902 → 0.898 (drop of 0.4 pts) is inside the noise — do not page anyone. A drop of 0.902 → 0.871 (3.1 pts) is ~2.5x the noise band — that's a real regression, fail the gate. What this did to the data: it converted a raw score delta into a significance decision, so the gate fires on signal, not jitter.
The honest caveat: agreement of 0.82 means the judge disagrees with humans on ~1 in 5 cases. For a deploy gate that's often fine because you care about aggregate movement across hundreds of cases, where per-case noise averages out — but it is not fine for auto-rejecting an individual user's output in production. Match the instrument's precision to the decision's blast radius.
Order matters for both cost and correctness. For each case:
This is the 5-step regression workflow distilled: prepare datasets → instrument the app to log traces → deterministic checks → LLM-judge for semantic quality → compare versions via experiment tracking.
The judge calls dominate cost, and they are highly cacheable because in CI you re-run the same golden set against systems that change one prompt at a time. Cache key = hash(judge_model, judge_version, judge_prompt_version, input, candidate_output). If the system output for a case is byte-identical to last run (common — most cases are unaffected by a one-line prompt tweak), the judge verdict is reused for free. In a typical PR that changes 8% of outputs, you pay for ~8% of judge calls. Two layers, mirroring semantic-cache practice: an exact K-V layer for identical (input, output) and optionally a semantic layer (cosine ≥ ~0.95) for near-identical outputs where the verdict is safe to reuse. Be conservative with the semantic layer on a judge — a 0.95-similar output can flip a borderline pass/fail, so high-precision threshold and only on non-gating dimensions.
Offline = the gate. Fixed golden set, before merge/deploy, blocks bad versions. Online = production monitoring: sample 10–20% of live traces for detailed LLM-judge scoring (logging cheap metrics — tokens, cost, latency — for 100%), route flagged responses to a manual-review queue, and feed the gnarliest ones back into the offline golden set. The two form a loop; online finds the failure modes offline didn't anticipate.
For multi-team (the IC6 core): one eval service, many tenants. Each team owns its golden sets, rubrics, and gate thresholds (namespaced, isolated — team A's judge-version bump must not move team B's baseline). The platform owns the shared runner, the judge-call cache, quota enforcement, and cost attribution. Per-team token budgets with rate limits at multiple levels (per-team, per-judge-model, system-wide) stop one team's 5,000-case suite from starving the rest. Cost is attributed per team/feature/model on every trace so the bill is legible and chargeable back.
A real, runnable offline eval runner with deterministic-checks-first ordering, judge caching, and a noise-band gate. This is the shape of the CI step.
import hashlib, json, sqlite3, statistics, re
from dataclasses import dataclass, asdict
# ---- content-addressed caches & result store (sqlite stands in for Redis/Postgres) ----
db = sqlite3.connect("eval.db")
db.execute("CREATE TABLE IF NOT EXISTS judge_cache(k TEXT PRIMARY KEY, verdict INT, reason TEXT)")
db.execute("CREATE TABLE IF NOT EXISTS runs(exp TEXT, case_id TEXT, dim TEXT, score INT)")
@dataclass(frozen=True)
class Case:
id: str; input: str; dimension: str
must_contain: tuple = () # deterministic check
must_be_json: bool = False
def dataset_version(cases):
blob = json.dumps([asdict(c) for c in cases], sort_keys=True, default=list)
return hashlib.sha256(blob.encode()).hexdigest()[:12]
# ---- LAYER 1: deterministic checks (free, run first, short-circuit) ----
def deterministic(case: Case, output: str):
if case.must_be_json:
try: json.loads(output)
except Exception: return ("fail", "invalid_json")
for token in case.must_contain:
if token.lower() not in output.lower(): return ("fail", f"missing:{token}")
if re.search(r"\b\d{3}-\d{2}-\d{4}\b", output): return ("fail", "pii_ssn")
return ("pass", "deterministic_ok") # not a final pass — just survived
# ---- LAYER 2: LLM judge (cached, only for survivors) ----
JUDGE_VERSION = "hallucination-judge-v3"
def judge(case: Case, output: str, call_model) -> tuple[int, str, bool]:
key = hashlib.sha256(f"{JUDGE_VERSION}|{case.input}|{output}".encode()).hexdigest()
hit = db.execute("SELECT verdict, reason FROM judge_cache WHERE k=?", (key,)).fetchone()
if hit: return hit[0], hit[1], True # cache hit -> zero cost
# binary pass/fail, single dimension, few-shot rubric lives in call_model's prompt
score, reason = call_model(case.dimension, case.input, output)
db.execute("INSERT OR REPLACE INTO judge_cache VALUES(?,?,?)", (key, score, reason))
return score, reason, False
def run_eval(cases, system, call_judge, exp_id):
cache_hits = total_judged = 0
for c in cases:
out = system(c.input)
verdict, reason = deterministic(c, out)
if verdict == "fail":
score = 0
else:
score, reason, hit = judge(c, out, call_judge)
total_judged += 1; cache_hits += int(hit)
db.execute("INSERT INTO runs VALUES(?,?,?,?)", (exp_id, c.id, c.dimension, score))
db.commit()
hit_rate = cache_hits / max(total_judged, 1)
return hit_rate
def pass_rate(exp_id, dim):
rows = db.execute("SELECT score FROM runs WHERE exp=? AND dim=?", (exp_id, dim)).fetchall()
return statistics.mean(s for (s,) in rows) if rows else 0.0
# ---- the CI gate: compare to baseline against a measured noise band ----
def gate(new_exp, base_exp, dim, noise_band=0.012):
new, base = pass_rate(new_exp, dim), pass_rate(base_exp, dim)
delta = new - base
if delta < -2 * noise_band: # >2x noise = real regression
return "FAIL", f"{dim}: {base:.3f} -> {new:.3f} (Δ{delta:+.3f}, regression)"
if delta > 2 * noise_band:
return "IMPROVE", f"{dim}: {base:.3f} -> {new:.3f} (Δ{delta:+.3f})"
return "PASS", f"{dim}: Δ{delta:+.3f} within noise"What's load-bearing: (1) dataset_version pins the ruler so a regression can't be a phantom of a moved dataset; (2) deterministic checks run before and short-circuit the judge — broken JSON never costs a token; (3) the judge cache key includes JUDGE_VERSION, so bumping the judge correctly invalidates everything (a judge change is a measurement change and must re-score); (4) the gate compares to a baseline against an empirically measured noise_band, not a hardcoded "must be >0.9". Swap the sqlite tables for Redis (judge cache) + Postgres (immutable run store) and call_judge for a real Claude/GPT call with a few-shot binary rubric, and this is production-shaped.
| Decision | Cheap / fast option | Expensive / accurate option | What changes at scale |
|---|---|---|---|
| Judge architecture | Pointwise binary, small judge | G-Eval CoT, large judge, pairwise | Pairwise's quadratic blowup makes it offline-only; pointwise is the only thing that survives per-trace online |
| Judge model | Haiku-class judge (~12x cheaper than Sonnet-class) | Frontier judge for fuzzy dims | At 5k cases × 12 teams × N PRs/day, judge model choice is the budget |
| Caching | Exact K-V on (input,output,judge_ver) |
+ semantic layer (cos ≥ 0.95) | Cache hit rate 85–95% in CI (most outputs unchanged per PR) is the difference between cents and dollars per run |
| Gate strictness | Aggregate pass-rate + noise band | Per-segment gates + per-case veto | Per-segment gates catch regressions hidden by averages; cost is more flaky-flag triage |
| Online sampling | 10–20% traces judged, 100% basic metrics | 100% judged | Judging 100% of production at scale costs as much as serving; sample and route flags to review |
| Freshness of golden set | Quarterly manual review | Continuous online→offline mining | Stale sets silently rot into false regressions; mining keeps them on-distribution |
Cost math you should say out loud. A 5,000-case suite, judge at ~$0.40/M tokens (current frontier-level pricing), ~1.5k tokens/judge-call ≈ 7.5M tokens ≈ $3 per cold full run. With an 88% cache hit rate on a typical PR, the marginal run is ~$0.36. Across 12 teams running CI dozens of times a day, the cache is the only thing standing between you and a four-figure daily judge bill — and remember the agentic-era trap: cheap per-token prices hide expensive per-task costs when a judge fans out across thousands of cases.
Latency. Offline runs are throughput-bound, not latency-bound — batch the judge calls, use continuous batching on a self-hosted judge if you own one (3–10x throughput), and the gate finishes in the time it takes to judge the ~8% of changed outputs. Online scoring is async and off the request path entirely; never put an LLM judge in the user's critical path.
Failure modes. (1) Goodhart — teams optimize the judge, not the product; the score climbs while users complain. Rotate held-out cases and keep human review in the loop. (2) Judge drift — the judge provider silently updates the model and your baseline shifts under you; pin judge model versions and re-run calibration on every judge bump. (3) Golden-set rot — covered above. (4) Averaged-away regressions — a 3-point drop on your highest-value segment hides inside a flat aggregate; gate per-segment. (5) Self-preference bias — a judge from the same family as the system under test tends to favor it; use a different judge family for bake-offs.
dataset_version from the golden-set hash and system_version from the commit → runs the system over all cases → deterministic checks short-circuit obvious fails for free → survivors hit the judge, keyed by (judge_version, input, output), so unchanged outputs (the ~90% a one-line change didn't touch) are cache hits at zero cost → aggregate per dimension/segment → compare to the baseline experiment, fail if any segment drops beyond 2x the noise band. Flake sources: judge nondeterminism (mitigate with temperature 0 and the noise-band gate), a provider-side judge update invalidating the baseline (pin versions), and golden-set rot producing phantom regressions (cache key won't save you — that's a data problem).judge_version. Bump the judge prompt and you silently mix two instruments' readings in one baseline.Flashcard. Eval platform = experiments keyed by
(dataset_version, system_version, judge_version)→ deterministic checks short-circuit before a calibrated, cached judge → gate on the delta vs baseline beyond a measured noise band, per segment. The judge is an instrument; the golden set is the product.
Next: /evals for the eval-design depth behind these platform mechanics, then /system-design/design-a-rag-chatbot to see where online-mined failure cases come from.