Search Tech Journey

Find topics, journeys and posts

6-month learning plan122 / 130
back to blog
llmadvanced 55m read

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.

LLMsM14 · LLMs & Applications· Session 122 of 130 90 min

🎯 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.

You will be able to
  • 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

Evaluating a chef vs evaluating a calculator
🌍 Real world

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.

💻 Code world

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

Every LLM eval decision picks a point in a 3-D cube
  • 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

  1. 2002
    BLEU · IBM
    N-gram overlap metric for machine translation. First widely-used reference-based metric — still cited, mostly wrong for modern LLMs.
  2. 2004
    ROUGE
    Recall-oriented n-gram metric for summarisation. Same era, same limits.
  3. 2019
    BERTScore
    First embedding-based similarity metric. Correlates with humans better than BLEU — but still needs a reference answer.
  4. 2023
    LLM-as-Judge (Zheng et al.)
    MT-Bench + Chatbot Arena show GPT-4 as judge matches human agreement rate. Unblocked reference-free evaluation.
  5. 2023
    RAGAS
    Purpose-built metrics for RAG: context precision/recall, faithfulness, answer relevance. Uses an LLM under the hood.
  6. 2024
    LMSYS Arena · production
    Real 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

Programmatic asserts (per commit, seconds)
Output is valid JSON. Response length under X. No PII leaked. No banned words. These run on every PR.
unit
Golden set (per commit or nightly, minutes)
50–500 hand-graded examples with expected outputs or rubric scores. Run through LLM-as-Judge. Alert on >3pp regression.
regression
Real-user sample (nightly, tens of minutes)
Sample 100–500 recent prod queries, re-run through pipeline, judge against rubric. Catches drift the golden set misses.
drift
Human audit (weekly, hours)
PM + domain expert review 50 sampled outputs by hand. Look for failure modes the automated eval misses.
ground truth
Online metrics (continuous)
Thumbs-up rate, retention, refund rate, tickets. The only metrics that pay rent.
product

The four RAGAS metrics — what each catches

Context Precision

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
Context Recall

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
Faithfulness

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 Relevance

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

1
Freeze the golden set

100 (question, expected-answer, expected-source-doc) triples. Version-controlled, hand-reviewed once a quarter.

2
Run new build

Point the pipeline at all 100 questions. Capture answers + retrieved contexts + latency.

3
Score via judge

Send each row to gpt-4o with rubric. Get faithfulness, relevance, precision, recall.

4
Compare vs baseline

Diff current run vs last green run. Fail CI if any metric drops >3 percentage points.

5
Human spot-check on failures

For every regressed row, PM eyeballs the answer. Confirms the judge is right, not overreacting.


Common misconception
✗ What most people think

"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."

✓ What is actually true

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.

Why the myth is so sticky

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.

Prove it to yourself

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.
From first principles
Start with the question

Why is perplexity a poor evaluation metric for a chat or task model, even though it is exactly the training objective?

  1. 1
    Perplexity 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
  2. 2
    For 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
  3. 3
    So 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
  4. 4
    Worse, 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
  5. 5
    And 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

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.

Mental modelThe eval set is the product spec

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.
🔔 Fires when you see

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.

The tradeoff

How do you evaluate a generative feature before shipping — human review, LLM-as-judge, or automated deterministic checks?

Human evaluation
+ you gain the only method that captures qualities nobody has managed to specify — tone appropriateness, subtle unhelpfulness, cultural missteps; and it is the calibration source everything else is validated against
− you pay slow and expensive enough that it cannot run per-commit, so it never becomes a gate; suffers its own reliability problems (annotator drift, fatigue, ambiguous guidelines) that are usually unmeasured
pick when periodic calibration of your automated metrics, and final review before a high-stakes launch — not as a continuous signal
LLM-as-judge
+ you gain runs in minutes on thousands of examples, so it can gate every change; handles open-ended output where no reference exists; and the rubric makes your quality definition explicit and reviewable
− you pay costs money per evaluation run; carries systematic biases that must be measured and corrected; and it is a moving target — the judge model itself gets updated, which silently shifts your scores
pick when open-ended generation you must evaluate continuously, once you have validated agreement against human labels on a calibration set
Deterministic automated checks
+ you gain free, instant, perfectly reproducible, and a hard gate you can put in CI; zero ambiguity about whether it passed
− you pay only applies to properties you can express as code — schema validity, compilation, citation presence, latency, refusal detection; says nothing about whether the answer is good
pick when always, for every property that admits a programmatic check — this should be the first layer, not the last resort
What a senior engineer actually does

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

GOLDEN list · lines 12–37
Hand-labelled ground truth. In production this lives in a CSV or DB; 50 rows minimum on day one, growing to a few hundred.
data
Verdict schema · lines 40–44
Pydantic model forces the judge to return exactly these fields. If the LLM returns malformed JSON, Pydantic throws — you catch bugs at parse time, not in dashboards.
schema
JUDGE_SYSTEM · lines 46–52
The rubric. Two rules matter most: 'do NOT use outside knowledge' (stops the judge filling in facts) and 'length is not quality' (fights verbosity bias).
rubric
temperature=0 · line 65
Non-negotiable for judges. Even at temperature=0.2, the same input can flip verdicts across runs and your CI becomes flaky.
determinism
response_format=json_object · line 66
Guarantees valid JSON. Without it you spend hours writing regex parsers for malformed judge output.
reliability
calibrate() · lines 76–86
Agreement with human labels. If this drops below 85%, your metric dashboard is lying to you.
trust
position_bias_test() · lines 89–96
For pairwise judges: run both orders, check for asymmetry. This is the single biggest silent bug in comparison evals.
bias
Try itFeel the difference between a calibrated and an uncalibrated judge

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.

💡 Hint · Then swap gpt-4o-mini for gpt-4o and re-run. Note the agreement percentage change and the reasoning quality change.

(d) Production reality · 15 min

War story Microsoft · OneNote Copilot· 2024millions of notes summarised daily
🔥 What broke

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.

🧯 The fix
Three changes in one sprint: (1) upgrade judge to 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.
🎓 Lesson to steal
The judge must be at least as strong as the model it grades — often stronger. And every rubric needs an anti-verbosity clause with concrete examples, not a vague "be concise" instruction.
War story Anthropic · Claude releasesevery model release
🔥 What broke
Early Claude eval pipeline compared two model outputs side-by-side and asked GPT-4 to pick the better one. Whichever answer was shown first won ~62% of the time — regardless of quality.
🧯 The fix

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.

🎓 Lesson to steal
Position bias in pairwise LLM judging is universal, large (~10pp), and easy to fix. Always double-run with swapped order. If you're not, your leaderboard is noise.
War story Chatbot Arena · LMSYS· 20241M+ human pairwise votes
🔥 What broke
Several open-source models started ranking suspiciously high in Arena. Investigation found teams were fine-tuning on Arena-like prompts — the eval set had leaked into training data, invisibly, via public scraped datasets.
🧯 The fix
LMSYS now rotates a private holdout every 30 days, publishes de-contamination scripts, and cross-checks Arena ranking against MT-Bench and MMLU-Pro. Suspicious jumps trigger a manual audit before the model gets a public row.
🎓 Lesson to steal
Every popular benchmark is training data within 6 months of publication. Keep a private, rotating eval set that never touches the internet.

Common failure modes (add these callouts to your team wiki)

Where this shows up in the rest of the plan

LLM Evaluation is the safety net under every LLM session
S117 · RAG I
The retrieval you evaluate with context precision + recall.
S119 · Prompt Engineering
Every prompt change gets scored on your golden set before it ships.
S123 · Fine-Tuning
You can't decide 'did fine-tuning help?' without a pre/post eval diff.
S124 · LLM Serving
Online metrics (P95 latency, cost/query) live next to quality metrics in the same dashboard.
S130 · Design an AI Chat Product
The capstone system-design session — eval loop is one of the six pillars.
S142 · Observability for ML
Prod-eval drift = the online half of this pyramid.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

  1. What is an LLM golden set, and how big is a good day-one one?
  2. What is the single most dangerous bias in LLM-as-Judge, and how do you defeat it?
  3. 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.