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.
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
- Position bias — prefers whichever answer is shown first. One team saw the judge pick "shown first" on 9 of 50 close pairs: an 18% bias rate. Fix: run both orderings, keep the verdict only when they agree.
- Verbosity bias — a crisp correct reply scores 3.6/5; a padded version of the same facts scores 4.3/5. If your rubric doesn't say "don't reward length," you're optimizing toward waffle.
- Self-preference — favors its own family's style. If judge and system are the same model, distrust "the new one is better" until you validate.
- Leniency / compression — on a 1–10 scale everything lands 6–8. The best argument for low-cardinality outputs.
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.
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%})")
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.
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.
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
- Pointwise scores v1 and v2 both 4.0/5 ("no improvement"); pairwise prefers v2 on 70%. Most likely:
- Swapping answer order flips the winner on 20% of pairs. The reading and fix:
- 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?
- You prefer binary "grounded: yes/no" over a 1–10 faithfulness scale mainly because:
- A new model ships for the system you grade; your validated judge now disagrees with humans more. First action: