AI Engineer Dojo Contents
Chapter 5

LLM-as-Judge

When there's no answer key and "good" is a matter of judgment, you make a capable model do the grading. It's the single most-used technique in the modern eval engineer's day — and the one most likely to quietly lie to you if you don't pin it down.

A support bot replies: "I've refunded your order — 3–5 business days." Good answer? No reference to diff against; "good" depends on accuracy, brand, completeness, safety. You have ten thousand a day. So you hand the answer plus a rubric to a strong model and ask it to grade. The rest of this chapter makes that judgment trustworthy — an unvalidated judge is just an opinion wearing a number.

Pointwise vs. pairwise

Pointwise: one answer + rubric → a score or label. Good for tracking a metric over time. Pairwise: two answers → which is better. The workhorse for "did my change help?", because judges (like people) compare far more reliably than they score absolutely. Worked: if real quality rose a hair between two prompts, a pointwise 1–5 judge scores both "4" (you conclude "no change"); head-to-head it picks the new one 7 times out of 10. Same judge, same answers — the comparative framing recovered a signal the absolute scale flattened to zero.

Name to know: G-Eval

Asked in an interview how you build a judge, G-Eval is the technique to name: turn the rubric into explicit evaluation steps, then have the model score through that form rather than emit an unexplained verdict. The original method used chain-of-thought; in production, ask for a short, auditable rationale or structured criteria result instead of depending on a model's hidden reasoning. It is the standard reference for rubric-driven LLM-as-judge, and frameworks like DeepEval ship it out of the box.

The biases, in real numbers

Who grades the judge?

Validate it against humans on the decision-relevant slice. A judge that agrees 95% on easy cases but 55% on the hard ambiguous ones — where decisions actually get made — reads a reassuring "87%" blended, and is useless. Always report agreement on the hard slice, not the average.

Try it · ~20 minFree · mock judge

Build a pairwise judge and measure its position bias

Write a pairwise judge, then measure its own position bias by swapping order. Runs free on a mock; flip USE_REAL_API = True (with a key) and the identical code grades with a real model. Goal: a number — "flipped on N pairs" — and a read on whether you'd trust raw verdicts.

Lab code — runs free (mock judge by default)

USE_REAL_API = False        # free mock by default; True needs a key

if USE_REAL_API:
    import anthropic
    from pydantic import BaseModel
    client = anthropic.Anthropic()
    class Verdict(BaseModel):
        reason: str        # reasoning BEFORE the label
        winner: str        # "A" or "B"
    def compare(question, a, b):
        r = client.messages.parse(
            model="claude-opus-4-8", max_tokens=400,
            messages=[{"role": "user", "content":
                f"Pick the more accurate, NOT-padded reply. Reason, then winner."
                f"\n\nQ:{question}\n\nA:\n{a}\n\nB:\n{b}"}],
            output_format=Verdict)
        return r.parsed_output.winner
else:
    # Mock with a built-in tilt: it leans toward slot A on close calls
    def compare(question, a, b):
        return "A" if len(a) >= len(b) else "B"   # length/position-ish bias

def robust_compare(question, x, y):
    """Run both orderings; trust only when they agree."""
    x_as_A = (compare(question, x, y) == "A")   # x in slot A
    x_as_B = (compare(question, y, x) == "B")   # x in slot B
    if x_as_A == x_as_B:
        return "x" if x_as_A else "y"
    return "biased"                             # flipped with position

PAIRS = [   # (question, reply_x, reply_y) — close in quality
    ("refund?", "Refunded; 3-5 days.", "Refunded today, arrives in 3-5 days."),
    ("hours?",  "9 to 5, Mon-Fri.",    "We're open 9-5 Monday through Friday."),
    ("reset?",  "Use 'Forgot password'.", "Click the 'Forgot password' link."),
]
flips = sum(robust_compare(q, x, y) == "biased" for q, x, y in PAIRS)
print(f"position-biased on {flips}/{len(PAIRS)} pairs ({flips/len(PAIRS):.0%})")
Worked solution

The mock decides by length, so a longer reply wins regardless of slot — its verdict tracks the answer, not the position, and robust_compare returns a clean winner. Now make the mock tilt by position instead:

    def compare(question, a, b):
        return "A"        # always pick the first slot — pure position bias

Re-run: every close pair now comes back "biased" (3/3, 100%), because the winner flips the instant you swap order. That's the whole lesson — one judge call gives a verdict; two calls give a verdict and a confidence signal, and the swap-and-agree wrapper converts silent position errors into an honest "biased" you can route to a human. On a real model the rate is usually 15–20% on genuinely close pairs, not 100% — but you only know that because you measured it.

Case study

PatchProof: grading the grader on the diffs that matter

A dev-tools startup ships PatchProof, which reads a pull request and posts a verdict — merge or needs work. There's no answer key for "is this review good," so they grade PatchProof with an LLM judge that scores each review 1–5. The ship signal looks strong: a 4.1 average, up after a model upgrade. Here's the validation that stopped a bad launch.

Ground truth first. Take 100 real PRs; two staff engineers independently label each merge / needs-work and adjudicate disagreements. That's the only thing the judge is measured against.

Grade the grader on the slice that matters. Blended agreement is a comfortable 91% — but on the 24 security-relevant diffs (auth changes, input validation) it's 58%. The judge is most confident exactly where a miss ships a vulnerability.

Position & verbosity bias. The "old vs. new model" comparison always showed one diff first; swap the order and 8 of 40 verdicts flip (20%). And the new model wrote longer rationales — same correctness, more words — which nudged the judge 3.5 → 4.2. A chunk of the "win" was layout and wordiness, not quality.

The fix, and the call. Switch to a binary merge-safe: yes/no rubric that says "don't reward length," force reasoning before the verdict, and run every comparison in both orderings — trusting only agreement. Re-validated on the security slice, agreement rises to 84%. The real story wasn't "4.1 and climbing"; it was a wordier model hiding a weak spot on security reviews — now visible, and gated before release.

Running case · Meridian × Remi

This chapter: Remi's helpfulness dashboard reads 4.2 (up from 3.8 after a prompt tweak). Validated against 120 human-labeled replies it's 88% overall but only 56% on the hard refund slice, and the gain is mostly verbosity — padded replies score 3.7→4.4 on identical facts. Pairwise with order-swap shows the change is flat, so Meridian holds Remi's billing launch. (Ch 8: they evaluate Remi again once it starts taking refund actions.)

Quiz · Chapter 5

  1. Pointwise scores v1 and v2 both 4.0/5 ("no improvement"); pairwise prefers v2 on 70%. Most likely:
  2. Swapping answer order flips the winner on 20% of pairs. The reading and fix:
  3. A judge agrees 95% on easy cases, 55% on the hard cases where decisions are made; the set is 80% easy, so it reads 87%. Why is "87%" misleading?
  4. You prefer binary "grounded: yes/no" over a 1–10 faithfulness scale mainly because:
  5. A new model ships for the system you grade; your validated judge now disagrees with humans more. First action:
← Back Continue →

AI Evaluation Engineer — New-Grad Edition · AI Engineer Dojo · aiengineerdojo.com