S122 · LLM Evaluation — LLM-as-Judge, RAGAS, Golden Sets
How you measure a non-deterministic, unbounded-output system without lying to yourself. Golden sets, judge models, RAGAS, and the biases that ruin every naive eval.
🎯 Ship an offline + online eval loop for an LLM system that a stakeholder actually trusts.
Why this session exists
Every LLM team goes through the same three-stage grief cycle. Stage one: "vibes." A PM types five prompts, likes the answers, ships. Stage two: users complain, and nobody can prove whether last week's prompt tweak made things better or worse. Stage three: someone builds an eval harness — usually two months too late, after a public regression. This session skips you straight to stage three. LLM eval is not a chore; it is the product loop.
- Explain the difference between reference-based, reference-free, and task-specific metrics without notes.
- Design a 50-example golden set for a new LLM product in under an hour.
- Write an LLM-as-Judge that resists position bias, verbosity bias, and self-preference bias.
- Compute the four RAGAS metrics (context precision/recall, faithfulness, answer relevance) on a real RAG pipeline.
- Know when to stop trusting your judge and re-calibrate against human labels.
Prerequisites
- S117 · RAG I — Chunking Strategies & Indexing (you need a system to evaluate)
- S119 · Prompt Engineering (the thing you'll iterate on)
- S092 · Evaluation Metrics (classical precision/recall/F1)
(a) Intuition · 5 min
You can grade a calculator with a spreadsheet: 2 + 2 must equal 4. Grade a hundred inputs, count matches, done. That's classical ML eval — labels, accuracy, F1.
You cannot grade a chef the same way. There are five equally good ways to cook a steak, the "right answer" depends on who's eating it, and the person grading is themselves a chef with tastes. That's LLM eval. You need rubrics, tasters, and calibrated judges — not a diff.
An LLM output has three properties a classifier doesn't: it's an unbounded string (infinite valid answers), it's non-deterministic (same input → different output), and its quality is subjective (faithfulness, tone, helpfulness).
So we invent three tools: golden sets (a small, hand-graded regression suite), LLM-as-Judge (a bigger LLM rating outputs against a rubric), and online metrics (thumbs-up rate, refund rate, retention). Together they form a testing pyramid — offline is fast and cheap, online is slow and truthful.
The three orthogonal axes
- What you measure — reference-based (BLEU, exact match), reference-free (toxicity, coherence), or task-specific (faithfulness, tool-use success).
- Who measures — programmatic (regex/schema), LLM-as-Judge (fast, biased), or human (slow, expensive, ground truth).
- When it runs — CI on every commit (fast, deterministic subset), nightly on prod sample (broader), weekly manual audit (deepest, catches drift).
The tools appear in a rough historical order
- 2002BLEU · IBMN-gram overlap metric for machine translation. First widely-used reference-based metric — still cited, mostly wrong for modern LLMs.
- 2004ROUGERecall-oriented n-gram metric for summarisation. Same era, same limits.
- 2019BERTScoreFirst embedding-based similarity metric. Correlates with humans better than BLEU — but still needs a reference answer.
- 2023LLM-as-Judge (Zheng et al.)MT-Bench + Chatbot Arena show GPT-4 as judge matches human agreement rate. Unblocked reference-free evaluation.
- 2023RAGASPurpose-built metrics for RAG: context precision/recall, faithfulness, answer relevance. Uses an LLM under the hood.
- 2024LMSYS Arena · productionReal users blind-pair-vote on LLM outputs. The Elo ranking is now the industry benchmark.
(b) Visual walkthrough · 15 min
The full eval pipeline
The testing pyramid for LLM systems
Fast + cheap at the bottom, slow + truthful at the top
The four RAGAS metrics — what each catches
Retriever pulled junk?
- Of the top-k chunks, how many are relevant?
- LLM-as-Judge on (query, chunk) pairs
- Drops when embedding model or top-k changes
- High = retriever is precise; low = user pays for irrelevant tokens
Retriever missed gold?
- Of the facts needed to answer, how many are in the retrieved chunks?
- Requires ground-truth answer to compare
- Drops when new content is added but not indexed
- Ceiling on final answer quality
Answer grounded?
- Of the claims in the answer, how many are supported by the context?
- Judge decomposes answer into claims, checks each vs context
- Drops when model hallucinates
- The single most important RAG metric
Answer on-topic?
- Does the answer actually address the question?
- Judge reverses: generate questions from answer, embed-compare to original
- Drops when model rambles or dodges
- Complements faithfulness (faithful but off-topic is still bad)
One concrete release loop
100 (question, expected-answer, expected-source-doc) triples. Version-controlled, hand-reviewed once a quarter.
Point the pipeline at all 100 questions. Capture answers + retrieved contexts + latency.
Send each row to gpt-4o with rubric. Get faithfulness, relevance, precision, recall.
Diff current run vs last green run. Fail CI if any metric drops >3 percentage points.
For every regressed row, PM eyeballs the answer. Confirms the judge is right, not overreacting.
"LLM-as-judge is subjective. To evaluate properly I need human labels — the judge is just a cheap approximation I use because I can't afford annotators."
A well-constructed judge with a specific rubric and reference output can reach inter-rater agreement with humans comparable to the agreement between two humans — which is itself often far from perfect. The real problems with judges are not subjectivity but measurable, correctable biases: position bias in pairwise comparisons, length bias, self-preference for outputs from the same model family, and sensitivity to formatting.
Because "human evaluation is ground truth" is the accepted default, and it is rarely audited. Ask two annotators to rate the same 100 summaries on a 1–5 scale and the agreement is often mediocre — humans disagree about what "helpful" means too. The judge feels less trustworthy because its errors are systematic and therefore visible, while human error is noisy and averages out of view.
Measure your judge's biases before trusting a single number it produces:
# Position bias: run every pairwise comparison BOTH ways.
# score(A,B) and score(B,A). Disagreement rate = position bias.
# Fix: always average both orders, or randomise and report the rate.
#
# Length bias: correlate judge score with response length across a
# fixed-quality set. A strong positive correlation means you are
# partly measuring verbosity.
#
# Calibration: hand-label 100 items yourself. Compute Cohen's kappa
# between judge and you, AND between you and a second human.
# The second number is the ceiling. Judge >= human-human agreement
# means the judge is as good as an annotator.Why is perplexity a poor evaluation metric for a chat or task model, even though it is exactly the training objective?
- 1Perplexity measures the model's probability assigned to a specific reference continuation, token by token.forced by · it is the exponentiated average negative log-likelihood of that exact sequence
- 2For generation tasks, many different outputs are equally correct. "Paris" and "The capital is Paris" and "It's Paris." are all right, but only one is the reference.forced by · natural language has enormous surface variation over a single semantic content
- 3So a model that produces a perfect answer with different wording is penalised exactly as hard as one producing a wrong answer of the same length.forced by · likelihood is computed against token identity, and it has no notion of meaning
- 4Worse, perplexity is not comparable across models with different tokenizers, since the per-token normalisation depends on how many tokens the text was split into.forced by · a model with a larger vocabulary gets fewer, harder predictions; the units differ
- 5And it is dominated by the easy majority of tokens — function words, punctuation, formatting — which the model predicts near-perfectly and which carry almost none of the task-relevant information.forced by · an average over tokens weights every token equally, and most tokens are trivial
Therefore perplexity measures distributional fit to a reference corpus, which is a good training signal and a bad task metric. It is useful for comparing checkpoints of the same model on the same tokenizer, and for almost nothing else.
And note what this predicts: the right metric must be defined on the output's semantics and must admit multiple correct answers — which forces you either to an exact-match task with a verifiable answer (code that compiles and passes tests, arithmetic, structured extraction), or to a model-based judge. There is no third option, and that dichotomy is why the field is where it is.
Whatever your eval measures is what your system will be optimised toward — by you, by every prompt tweak, by every model upgrade decision. So the eval set is not a testing artefact; it is an executable statement of what you believe correct behaviour is.
Which means the failures you have not put in the eval set will never be fixed, and the qualities you cannot measure will silently degrade with every change you ship.
- Build the eval set from real production failures, not from imagined cases. Every incident should end with a new eval item — that is the ratchet.
- Prefer verifiable tasks wherever possible: does the code run, does the JSON validate, does the number match. Deterministic checks beat any judge.
- Judges need a rubric, a reference where one exists, and a fixed output format. "Rate 1–10" without criteria measures nothing reproducible.
- Always report per-slice results. An aggregate score hides the fact that you improved the common case and broke the case that generates complaints.
Fire this model the moment you see: a model upgrade proposed with no regression eval · a single aggregate quality number · a judge prompt with no rubric · an incident closed without adding a test case · anyone comparing two systems on a benchmark neither was designed for.
How do you evaluate a generative feature before shipping — human review, LLM-as-judge, or automated deterministic checks?
Layer all three by cost and frequency: deterministic checks on every commit, a judge on every meaningful change, human review weekly and before launch — with the human labels used specifically to validate that the judge still agrees with you.
The mistake that costs the most is treating evaluation as a phase before shipping rather than as production infrastructure. The eval set should grow from production failures continuously, and pin the judge model version the way you pin any other dependency — an unpinned judge means your quality metric changes underneath you and you will attribute the shift to your own system.
(c) Hands-on · 25 min
Build a mini LLM-as-Judge that scores answer faithfulness against a source document, then measures its agreement with human labels — the calibration step almost everyone skips.
#!/usr/bin/env python3
# eval_judge.py — a calibrated LLM-as-Judge for answer faithfulness.
# pip install openai pydantic
import json
import os
import random
import statistics
from typing import Literal
from openai import OpenAI
from pydantic import BaseModel, Field
client = OpenAI()
# ---------- 1. golden set with human labels ----------
GOLDEN = [
{
"context": "The Eiffel Tower was built in 1889 for the World's Fair. It is 330 meters tall.",
"question": "When was the Eiffel Tower built and how tall is it?",
"answer": "It was built in 1889 and is 330 meters tall.",
"human_faithful": True,
},
{
"context": "The Eiffel Tower was built in 1889 for the World's Fair. It is 330 meters tall.",
"question": "When was the Eiffel Tower built and how tall is it?",
"answer": "It was built in 1887 by Napoleon and is 400 meters tall.",
"human_faithful": False,
},
{
"context": "Python 3.12 was released in October 2023 and introduced PEP 695 type parameter syntax.",
"question": "What did Python 3.12 introduce?",
"answer": "PEP 695 type parameter syntax, released October 2023.",
"human_faithful": True,
},
{
"context": "Python 3.12 was released in October 2023 and introduced PEP 695 type parameter syntax.",
"question": "What did Python 3.12 introduce?",
"answer": "Match statements and the walrus operator.",
"human_faithful": False,
},
# ...in a real system, 50-500 rows.
]
# ---------- 2. judge schema ----------
class Verdict(BaseModel):
faithful: bool
unsupported_claims: list[str] = Field(default_factory=list)
score: Literal[1, 2, 3, 4, 5]
reasoning: str
JUDGE_SYSTEM = """You are a strict fact-checker. You are shown a CONTEXT and an ANSWER.
Decide whether EVERY factual claim in the ANSWER is directly supported by the CONTEXT.
Rules:
- Do NOT use outside knowledge. If a claim is true in the real world but not in the CONTEXT, it is UNSUPPORTED.
- Length is not quality. A short answer can be fully faithful; a long answer can be full of hallucinations.
- Return JSON matching the schema exactly."""
JUDGE_USER = """CONTEXT:
{context}
ANSWER:
{answer}
Return JSON:
{{"faithful": bool, "unsupported_claims": [str], "score": 1-5, "reasoning": str}}
Where score=5 means fully supported, score=1 means mostly fabricated."""
def judge(context: str, answer: str, model: str = "gpt-4o-mini") -> Verdict:
resp = client.chat.completions.create(
model=model,
temperature=0, # critical — otherwise judge scores drift across runs
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": JUDGE_SYSTEM},
{"role": "user", "content": JUDGE_USER.format(context=context, answer=answer)},
],
)
return Verdict(**json.loads(resp.choices[0].message.content))
# ---------- 3. calibration: judge-vs-human agreement ----------
def calibrate(rows, model="gpt-4o-mini"):
hits = 0
for row in rows:
v = judge(row["context"], row["answer"], model=model)
agree = v.faithful == row["human_faithful"]
hits += agree
marker = "✅" if agree else "❌"
print(f"{marker} judge={v.faithful} human={row['human_faithful']} "
f"score={v.score} reason={v.reasoning[:80]}")
agreement = hits / len(rows)
print(f"\nJudge-human agreement: {agreement:.0%} on {len(rows)} rows")
return agreement
# ---------- 4. bias check: does order matter? ----------
def position_bias_test(row_a, row_b, n=10, model="gpt-4o-mini"):
"""Ask the judge to pick the better of two answers, N times, both orders."""
wins_a = 0
for _ in range(n):
order = random.choice(["ab", "ba"])
first, second = (row_a, row_b) if order == "ab" else (row_b, row_a)
# ...judge call with a rubric that picks a winner
# (elided for brevity — pattern is the same as judge() above)
# If wins_a >> n/2 regardless of who's actually better → position bias.
# ---------- 5. run ----------
if __name__ == "__main__":
print("=== Judge calibration ===")
agreement = calibrate(GOLDEN)
if agreement < 0.85:
print("⚠️ Agreement <85% — do NOT trust this judge yet.")
print(" Options: upgrade to gpt-4o, tighten the rubric, or add few-shot examples.")
else:
print("✅ Judge is calibrated. Safe to use in CI.")What each block is doing
Anatomy of the eval harness
Add 6 more borderline rows to GOLDEN — answers that are half-supported ("Eiffel Tower was built in 1889 by Gustave Eiffel for the Paris World's Fair" — the year and fair are in context, the name isn't). Run calibrate() twice: once with gpt-4o-mini, once with gpt-4o. Compare agreement.
(d) Production reality · 15 min
A team used gpt-3.5-turbo as judge to score gpt-4 summaries. Judge average: 4.3/5. Engineers celebrated, shipped a prompt tweak that improved judge score to 4.5.
Two weeks later, user thumbs-down rate doubled. Post-mortem: the weaker judge couldn't catch subtle factual errors and systematically preferred longer summaries — the same summaries humans found bloated.
gpt-4o, (2) add explicit "prefer concise, factually grounded answers" to the rubric with two few-shot examples, (3) require the judge to quote the exact source span for each accepted claim. Thumbs-down rate returned to baseline in 10 days.Every pairwise call is now run twice with the order swapped, and a "win" requires both orders to agree. If the two orders disagree, the pair is marked "tie" and never counted as a preference.
This roughly halves throughput but eliminates position bias entirely. The RAGAS library adopted the same pattern in v0.1.
Common failure modes (add these callouts to your team wiki)
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
- What is an LLM golden set, and how big is a good day-one one?
- What is the single most dangerous bias in LLM-as-Judge, and how do you defeat it?
- When you say "we improved by 3 percentage points", what should the next person ask you?
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.