AI Evaluation Framework
Measure before you ship — LLM-as-a-judge, its biases, and catching regressions with numbers
Learning Objectives
By the end of this project, you will be able to:
- Explain why "it seemed to work" is not an evaluation, and why text outputs with no single right answer can't be checked with
assert ==. - Implement LLM-as-a-judge: build a prompt that scores an answer on a scale, and justify why a strong judge is usable (~80% human agreement).
- Name and reproduce the biases of an LLM judge — position, verbosity, self-enhancement, weak math grading — and say which one caused a bad score.
- Apply the standard mitigations: reasoning-before-score (chain-of-thought), reference-guided judging, and position-swapping.
- Parse a judge's free-text reply robustly into a numeric score (JSON, "Score: 4", "4/5"), clamping out-of-range values.
- Aggregate per-case scores into a decision: a mean score and a pass-rate against a threshold.
- Build a regression test: freeze a dataset, score a baseline, re-score after a change, and flag the cases that got worse.
- Distinguish reference-free metrics (faithfulness/relevance) from reference-based and judge-based scoring.
1. Motivation
Why This Exists
You changed your system prompt. Is the assistant better or worse? With code you'd run the tests — but there is no assert summary == expected, because a good summary, a good answer, a good explanation has no single correct string. "Most AI developers ship based on vibes and demo impressions. Production failures happen because no one built evals." The breakthrough is LLM-as-a-judge: a strong model can grade open-ended answers and "match both controlled and crowdsourced human preferences well, achieving over 80% agreement — the same level of agreement between humans." That makes quality measurable, and measurable means improvable.
Two problems. First, scoring: turn "is this answer good?" into a number, at scale, without a human for every example. Second, regression: once you can score, freeze a dataset and detect when a change makes things worse — the silent failure that ships when no one was measuring. The catch: the judge is not a neutral oracle — it has biases (position, verbosity, self-enhancement) that quietly corrupt your numbers if you don't design around them.
What Breaks Without It
Without an eval harness, every change is a gamble — you ship and hope, and regressions slip through because no one was watching a number. And if you build a judge but ignore its biases, your metric lies to you: a prompt change that only makes answers longer looks like an improvement (verbosity bias).
Real-World Stakes
Every serious AI product has an eval harness behind it; the ones that don't ship regressions they can't see. "Evaluation infrastructure is the invisible moat… Braintrust, LangSmith, and others are built on selling evaluation infrastructure." The judgment that separates a pro from an amateur: do you trust a single demo, or do you have a dataset, a judge you've validated, and a pass-rate you watch on every change?
Eval infrastructure is a standalone product category precisely because it lets a team ship fast without breaking quality. Internally it's the moat; externally it's a business. Either way the unit of value is: a number you trust, attached to a change, before it reaches users.
2. Mental Model
Explain Like I'm 12
Imagine grading 500 essays. There's no answer key — a good essay can be written a thousand ways — so you hire a really sharp teaching assistant to read each one and give it a 1–5. That TA (the judge) is fast and mostly agrees with you. But the TA has quirks: they give higher marks to longer essays, and to whichever essay they read first. So you make two rules: "write down your reasons before you give the number," and "if you're comparing two, read them in both orders." Now their grades are trustworthy enough that when you change how you teach, you can re-grade the same 500 essays and see whether scores went up or down. That up-or-down check is a regression test; the TA-with-rules is LLM-as-a-judge.
Explain Like I'm a Software Engineer
- A judge is an LLM call whose output is a score, not an answer.
build_judge_prompt(question, answer, reference?)→litellm.completion→ free text →parse_judge_score→ an int in[1, scale]. - The judge prompt encodes the mitigations: a fixed scale (1–5), reasoning before the score (chain-of-thought — improves agreement and is auditable), and the reference when you have one (reference-guided).
- The model returns prose, so you must parse robustly: JSON
{"score": 4}, "Score: 4", or "I'd give this 4/5." Extract the integer, clamp to range, fail loud only when truly unparseable. - A run produces
list[JudgeResult].summarize→(mean, pass_rate@threshold).compare_runs(baseline, candidate)→ whichcase_ids dropped beyond a tolerance = regressions.
Real-World Analogy
A judge is a restaurant health inspector with a known soft spot. They're trained, consistent, and mostly right — you can run a whole city's restaurants past them. But suppose they unconsciously score bigger kitchens higher. If you don't know that, you'll conclude "big kitchens are cleaner" when you've just measured the inspector's bias. The fix isn't to fire the inspector (they're 80% reliable) — it's to standardize the rubric, make them write findings before the grade, and audit for the soft spot. Same with an LLM judge.
How It Works (Diagram)
ONE CASE:
question: "Summarize the refund policy."
answer: "<the system-under-test's output>"
reference?: "<gold summary, if you have one>"
│
▼ build_judge_prompt (scale 1–5, "reason FIRST, then score", include reference)
judge LLM ──► "The answer covers X and Y but misses the 30-day window. Score: 3"
│
▼ parse_judge_score → 3 (robust to JSON / '3/5' / 'Score: 3'; clamp to [1,5])
JudgeResult(case_id, score=3, reasoning="...misses the 30-day window")
MANY CASES:
summarize([...]) → mean=3.8, pass_rate@4 = 0.62
compare_runs(baseline, candidate, tolerance=0) → regressions=[case_7, case_12]
3. Technical Explanation
Formal Definition
An LLM judge is a function (question, answer[, reference]) → score ∈ {1..N} realized by an LLM call. Single-answer grading scores one answer; pairwise compares two. Agreement is how often the judge matches a human; GPT-4 reaches ">80% agreement, the same level of agreement between humans." Pass-rate = |{c : score(c) ≥ t}| / |C|; a regression (tolerance τ) is candidate.score < baseline.score − τ.
How It Works Step by Step
- Design the dataset. A case is
(id, question[, reference]). Good cases are representative, include edge inputs, and are frozen — the dataset must not change between baseline and candidate. - Build the judge prompt. A system message with the role and scale; a user message with the question, the answer, and the reference if present. Bake in the mitigations: reasoning, then the score, in a parseable format.
- Call the judge. One
litellm.completionattemperature=0for repeatability. The judge can (and often should) differ from the model under test — avoid self-enhancement bias. - Parse the score. The reply is prose. Try JSON; else regex "score: N" or "N/scale"; else first in-range integer. Clamp to
[1, scale]. Raise only when there is no number at all — silent0s poison your mean. - Aggregate.
summarize→ mean + pass-rate. One number you trust beats 200 you scroll past. - Regression-test. Score the baseline once and store it; after a change, score the candidate on the same dataset and
compare_runspercase_id.
Key Concepts
| Concept | Definition | Why It Matters |
|---|---|---|
| LLM-as-a-judge | Using a strong LLM to score/compare answers in place of a human or fixed metric | The only scalable way to grade open-ended text; ~80% human agreement |
| Position bias | The judge favors an answer by its order, not its quality | A named failure; cancel via position-swap |
| Verbosity bias | The judge prefers longer answers regardless of correctness | A wordier prompt can fake an "improvement"; watch length |
| Self-enhancement bias | The judge favors its own model family / style | Don't judge a model with itself by default |
| Reasoning-before-score | Make the judge explain, then score (chain-of-thought) | Cheapest, highest-leverage mitigation; also auditable |
| Pass-rate | Fraction of cases scoring ≥ a threshold | Turns a list of scores into a ship/no-ship decision |
| Regression test | Re-score a frozen dataset after a change; flag drops | "Did my prompt change help or hurt?" answered with numbers |
| Reference-free metric | Score without a gold answer (faithfulness, relevance) | Grade when no human reference exists |
An LLM judge is not ground truth. ~80% agreement means about 1 in 5 verdicts may differ from a human — a strong signal, not an oracle. And a higher mean does not automatically mean "better": verbosity bias and a hidden per-case regression both break that inference. Treat scores as evidence, watch length, and compare per case.
4. Guided Examples
The lab stack: litellm (the judge call via config.py), pure-Python parsing/aggregation, and a small frozen dataset. Examples mirror the guiding tests.
Example 1: Simplest Case — parse a judge's reply into a score
from llm_judge import parse_judge_score
parse_judge_score('{"reasoning": "covers it well", "score": 4}', scale=5) # (4, "covers it well")
parse_judge_score("Reasoning: solid but terse.\nScore: 5", scale=5) # (5, "Reasoning: solid but terse.")
parse_judge_score("I'd rate this 3/5.", scale=5) # (3, "I'd rate this 3/5.")
parse_judge_score("Score: 9", scale=5) # (5, ...) ← clamped to scale
The judge replies in prose, and the format varies. parse_judge_score extracts the integer (JSON or text), clamps it into [1, scale], and keeps the reasoning. The harness is only as reliable as this step — a model that says "9/5" must not become a 9 in your mean.
Example 2: Real-World Case — aggregate scores into a decision
from llm_judge import JudgeResult
from metrics import summarize
results = [
JudgeResult("c1", 5, "..."), JudgeResult("c2", 4, "..."),
JudgeResult("c3", 3, "..."), JudgeResult("c4", 2, "..."),
]
s = summarize(results, pass_threshold=4)
print(s.n, s.mean_score, s.pass_rate) # 4 3.5 0.5 (two of four scored >= 4)
Four scores become a mean (3.5) and a pass-rate (50% at threshold 4). The pass-rate is the ship/no-ship number — "half my answers are good enough" is a decision; a list of four integers is not.
Example 3: When It Fails — a regression hides inside a flat average
from llm_judge import JudgeResult
from regression_runner import compare_runs
baseline = [JudgeResult("a", 4, ""), JudgeResult("b", 5, ""), JudgeResult("c", 3, "")]
candidate = [JudgeResult("a", 4, ""), JudgeResult("b", 2, ""), JudgeResult("c", 5, "")]
report = compare_runs(baseline, candidate, tolerance=0)
print(report.regressions) # ['b'] — dropped 5 → 2
print(report.improvements) # ['c'] — rose 3 → 5
print(round(report.baseline_mean, 2), round(report.candidate_mean, 2)) # 4.0 3.67
The mean barely moved (4.0 → 3.67), but case b regressed hard (5 → 2) while c improved — they nearly cancel in the average. A flat mean can hide a real regression; compare_runs surfaces it per case. This is why a regression test is not just "did the average drop?"
5. Reflection Before Building
Before writing any code, fill in UNDERSTANDING.md in your own words. No copy-pasting from the lesson. No AI assistance for writing — only for checking. If you can't explain it simply, go back and re-read.
Knowledge Check — Answer These in UNDERSTANDING.md
- In your own words, why can't you evaluate an open-ended answer with
assert output == expected? What does LLM-as-a-judge do instead? - The judge agrees with humans ~80% of the time. Why is that "good enough to use" but "not safe to fully trust"? What do you do about the other 20%?
- Pick two judge biases (position, verbosity, self-enhancement). For each, describe a concrete scenario where it gives the wrong score, and the mitigation.
- Why ask the judge for reasoning before the score? Why
temperature=0? - Predict what breaks if
parse_judge_scoresilently returns0on an unparseable reply instead of raising. What happens to your mean and your pass-rate? - Why must the eval dataset be frozen between baseline and candidate? What does a regression test measure if the cases change?
- Why can a regression be invisible in the mean but obvious in
compare_runs? (Use the Example 3 numbers.) - The one thing you still don't fully understand about evaluating AI systems.
After filling in UNDERSTANDING.md, use the AI mentor pattern in docs/meta/learning-flow.md to get feedback. Record the feedback in UNDERSTANDING_FEEDBACK.md.
6. Project Assignment
See PROJECT.md and source/project.md for the full specification including file structure and extended requirements.
Core Requirement
Build a reusable evaluation harness in code/:
llm_judge.py—build_judge_prompt(bias-mitigated prompt) andparse_judge_score(robust extraction) are the learner core;judgeandJudgeResultare provided.metrics.py—summarize(mean + pass-rate) is the learner core;exact_match/containsare provided.regression_runner.py—compare_runs(baseline vs candidate → regressions) is the learner core; the run-a-dataset harness is provided.dataset.py/report.py— frozen test cases + formatting. (Provided.)
Extended: pairwise judging with position-swap; reference-guided agreement lift; a faithfulness metric (reuse Project 04); cost/latency tracking; a verbosity audit.
The judge prompt asks for reasoning before the score, uses a fixed scale, and is reference-guided when a reference exists; parse_judge_score is robust, clamps range, and fails loud on no-score; reporting includes a pass-rate and a per-case regression comparison; the guiding tests pass; UNDERSTANDING.md done before any code; FAILURE_ANALYSIS.md reproduces a bias and a hidden regression; EVALUATION.md is quantitative; STARCALLOS_REFLECTION.md names ≥1 concrete pattern.
Open code/README.md for setup, the milestone build order, and the file roles (which files are provided vs. learner-owned). Run python -m pytest to see the failing guiding tests, then implement the learner-owned functions in milestone order until they pass.
7. Project Milestones
Work through these in order. Each milestone should produce runnable code before moving on.
M1 — Judge prompt
llm_judge.build_judge_prompt — scale, reasoning-before-score, optional reference. Validation: prompt contains the answer, the scale, asks for reasoning then score; includes the reference when given (offline test).
M2 — Parse score
llm_judge.parse_judge_score — JSON / "Score: N" / "N/scale", clamp to range. Validation: parses each format; "9" with scale 5 → 5; garbage → ValueError (offline test).
M3 — Aggregate
metrics.summarize — mean score + pass-rate at a threshold. Validation: [5,4,3,2]@4 → mean 3.5, pass_rate 0.5 (offline test).
M4 — Regression
regression_runner.compare_runs — per-case drops beyond tolerance. Validation: baseline vs candidate flags the regressed and improved cases; means computed (offline test).
M5 — End-to-end eval
Wire dataset → system-under-test → judge → summarize → report. Validation: run the harness on a prior project's outputs; get a mean + pass-rate report.
M6 — Break / Evaluate
Reproduce a judge bias (verbosity/position) and a hidden regression. Validation: a wordier-but-not-better answer scores higher; a regression hidden in a flat mean; recorded in FAILURE_ANALYSIS.md.
8. Self-Evaluation
After building, honestly evaluate your implementation against these criteria. Record your answers in EVALUATION.md.
| Criterion | Does your implementation... | Pass? |
|---|---|---|
| Judge prompt | define a fixed scale and ask for reasoning before the score? | ☐ |
| Reference-guided | include the reference in the prompt when one is provided? | ☐ |
| Robust parse | handle JSON and plain-text replies, and clamp out-of-range scores? | ☐ |
| Fail loud | raise (not silently 0) when the reply has no score? | ☐ |
| Aggregate | report a mean and a pass-rate against a threshold? | ☐ |
| Regression | flag per-case drops, not just a change in the average? | ☐ |
| Frozen dataset | compare baseline and candidate over the same cases? | ☐ |
| Judge ≠ subject | use a judge model that isn't the model under test (or note the bias)? | ☐ |
Your implementation may have problems if:
- Your judge emits a score with no reasoning (or scores before reasoning).
parse_judge_scorereturns0on an unparseable reply — your mean is now a lie.- You report only a mean and miss a per-case regression (Example 3).
- You judge a model with itself and call the inflated score a win (self-enhancement).
- A prompt change "improved" scores but only made answers longer (verbosity bias).
- Your baseline and candidate were scored on different cases.
9. Common Mistakes
| Mistake | Why It Happens | Consequence | Fix |
|---|---|---|---|
| Trusting the judge as ground truth | "GPT-4 agrees 80% of the time" | The other ~20% silently corrupts decisions | Treat scores as a strong signal; spot-check |
| Score before reasoning | Asking for a number first | Worse, less consistent scores | Reasoning first, then score (CoT) |
| Silent parse fallback to 0 | "Handle the error quietly" | A few 0s tank the mean; false regression | Clamp valid scores; raise on no-score |
| Mean-only reporting | A single number feels clean | A real regression hides in a flat average | Add per-case compare_runs |
| Judging a model with itself | Convenient, one model | Self-enhancement bias inflates the score | Use a different/stronger judge |
| Ignoring verbosity | Longer looks more thorough | A wordier prompt fakes an "improvement" | Log length next to score; audit it |
| Changing the dataset between runs | Adding cases as you go | Baseline vs candidate no longer comparable | Freeze and version the dataset |
temperature > 0 on the judge | Default sampling | Same answer gets different scores; flaky regression | Judge at temperature=0 |
10. Connections
Builds On
The judge is Project 01's litellm.completion — same call, but its job is to output a score. The faithfulness metric you build as an extension is Project 04's RAGAS work generalized (F = |V|/|S|), and reference-guided judging is the same instinct as grounding an answer in retrieved context (Projects 03/04). Any prior project is a system under test you can now measure instead of eyeball.
Enables
Project 08 (agent) needs evaluation badly — agents fail in long multi-step trajectories, and "did the agent succeed?" is itself a judge call over a transcript. Project 09 (personal learning OS) uses evaluation to know whether the system is actually helping. The LLM-rated importance from Project 05 was a special case of LLM-as-a-judge; this project is the general tool.
Production Patterns
Real eval stacks (Braintrust, LangSmith, OpenAI Evals) are this plus scale: versioned datasets, LLM-as-judge with validated prompts, pairwise + position-swap, reference-guided scoring, per-case dashboards, regression gates in CI, and cost/latency tracking per run. The frontier adds human-in-the-loop spot-checks to calibrate the judge and catch the ~20% it gets wrong.
StarcallOS Relevance
StarcallOS will change prompts, swap models, and add features constantly. Without this harness, every change is a gamble — you ship and hope. With it, every change is gated by a number: a frozen eval dataset of real StarcallOS tasks, an LLM judge you've validated and de-biased, a pass-rate you watch, and a regression report that blocks a merge when quality drops. The disciplines here are what make StarcallOS improvable instead of merely changeable.
Sources
See source/resources.md for the complete annotated source list.
Tier 1 — Official Documentation
sources/official-docs/litellm-completion.md— the judge is onelitellm.completioncall;temperature=0for repeatability (carried from Project 01)
Tier 2 — Foundational Papers
sources/papers/mt-bench.md— Zheng et al. 2023: LLM-as-a-judge, ~80% human agreement, the biases (position/verbosity/self-enhancement) and mitigations (reasoning-before-score, reference-guided, position-swap)sources/papers/ragas.md— Es et al. 2023: reference-free metrics — faithfulness (F = |V|/|S|), answer/context relevance (carried from Project 04)sources/papers/sentence-bert.md— embeddings behind answer-relevance similarity (carried from Projects 02/04)
Tier 3 — Engineering Guides
- None specific to this lesson — MT-Bench is itself the engineering reference; regression testing is the engineering application of the harness.
Tier 4 — Educational Sources
- None specific to this lesson — production eval platforms (Braintrust, LangSmith, OpenAI Evals) are the real-world instantiations worth browsing once the core is built.
Optional — Going Deeper
Read these after the harness works. Two directions beyond a single judge score: make the eval observable (a regression becomes a traced event, not a print line), and make the generator self-correct using the same critique discipline. Neither is required to finish the project.
sources/official-docs/opentelemetry-genai-semconv.md(optional — depth) — a standard vocabulary (spans, attributes) for model calls: the production form of your eval traces. Turns ad-hoc judge logs into structured, queryable telemetry you can alert and regress on.sources/papers/self-refine.md(optional — depth) — Madaan et al. 2023: iterative generate → self-feedback → revise. Your judge scores; Self-Refine feeds the critique back to improve the next draft — the evaluator-optimizer loop your judge could drive (carries into Project 08).