Your suite from Module 9 leans on llm-rubric assertions you have never validated. This module builds a judge properly: binary rubrics, the exact prompt pattern, calibration against 20 human labels with agreement and Cohen's kappa, the three biases that make naive judges lie, and a full faithfulness judge in runnable Python.
Module 6 gave the one-paragraph warning: an uncalibrated LLM judge is vibes with a spreadsheet. Module 9 then had you write llm-rubric assertions anyway, because you cannot iterate a prompt without scalable grading. This module closes the loop — you will build a judge as a piece of engineering with its own spec, its own tests, and its own known failure modes. The rule that governs everything here: a judge is a model you ship, so it gets an eval too. Its eval is called calibration, and its golden set is human labels.
The economics are blunt. A human reviewer grades maybe 60 outputs an hour and drifts after two hundred. A judge model grades hundreds per minute at fractions of a cent each. Once your suite passes ~50 rubric-graded cases, or you want to grade a sample of production traffic continuously, human-only grading is dead — the eval simply stops being run, which is worse than being wrong.
But two boundaries stay fixed. Judges do not replace error analysis — reading traces and building the failure taxonomy (Module 6) is still human work; the judge only scales the application of criteria the humans discovered. And judges do not own high-stakes verdicts — a ship/no-ship call on a safety-critical slice gets human review of the actual failures, with the judge serving as a pre-filter that decides what humans look at first.
The judge scales your judgment; it must never replace the step where you form that judgment. If nobody on the team has read fifty real outputs this month, the judge is automating a standard no one currently holds.
The single highest-leverage decision in judge design is the answer format. Teams instinctively ask for a 1–10 quality score. Resist, for three reasons that compound:
The method is decomposition: take the fuzzy quality you care about and split it into independent yes/no checks. "Is this a good RAG answer?" decomposes into: (a) every factual claim is supported by the retrieved context, (b) the question is actually answered, (c) no fabricated citations, (d) says "I don't know" when the context lacks the answer. Each becomes its own judge call — or its own criterion within one structured call, as in the worked example below. One criterion per verdict also kills the halo effect, where a fluent answer drags every sub-score upward.
If asked "how would you grade summaries with an LLM judge?", the strong answer starts with decomposition into binary criteria and calibration against human labels — not with a prompt that says "rate this summary 1–10".
Every reliable judge prompt has the same skeleton, in this order:
What you leave out matters as much: no examples of "good style", no "be fair", no invitation to weigh factors holistically. A judge prompt is a spec, not a vibe.
The scenario: your team ships a RAG assistant over internal policy docs (Module 5 covered the retrieval side). The failure mode that matters most is unfaithfulness — answers that sound right but are not supported by the retrieved context. Here is a complete, runnable judge. It uses Claude Haiku 4.5 as the judge model — the cheapest current Anthropic tier, which is the right default for judging: you will run thousands of these calls, and calibration (not model size) is what makes a judge trustworthy.
# judge.py — RAG faithfulness judge (pip install anthropic)
import json
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
JUDGE_MODEL = "claude-haiku-4-5" # cheap tier; calibration makes it honest
VERDICT_SCHEMA = {
"type": "object",
"properties": {
"unsupported_claims": {
"type": "array", "items": {"type": "string"},
"description": "Each claim in the answer NOT entailed by the context",
},
"reasoning": {"type": "string"},
"faithful": {"type": "boolean"},
},
"required": ["unsupported_claims", "reasoning", "faithful"],
"additionalProperties": False,
}
JUDGE_PROMPT = """You are a strict evaluator checking whether an answer is \
FAITHFUL to a set of retrieved context passages. You do not grade style, \
completeness, or helpfulness — only faithfulness.
Definition: a claim is SUPPORTED only if a specific sentence in the context \
entails it. General knowledge, plausible inference, and "probably true" do \
NOT count as support. An answer is faithful only if every factual claim it \
makes is supported. Refusing or saying the context doesn't contain the \
answer is faithful by definition.
<question>
{question}
</question>
<context>
{context}
</context>
<answer>
{answer}
</answer>
List every unsupported claim (quote the answer's words), then give your \
reasoning, then the verdict."""
def judge_faithfulness(question: str, context: str, answer: str) -> dict:
response = client.messages.create(
model=JUDGE_MODEL,
max_tokens=1024,
output_config={"format": {"type": "json_schema", "schema": VERDICT_SCHEMA}},
messages=[{
"role": "user",
"content": JUDGE_PROMPT.format(
question=question, context=context, answer=answer
),
}],
)
text = next(b.text for b in response.content if b.type == "text")
return json.loads(text)
if __name__ == "__main__":
verdict = judge_faithfulness(
question="How long do refunds take?",
context="Refunds are processed within 5-7 business days of approval.",
answer="Refunds usually take 24 hours once approved.",
)
print(json.dumps(verdict, indent=2)) # expect faithful: false
Design choices worth naming: the criterion is singular (faithfulness only — completeness would be a second judge), "supported" is defined, the evidence is delimited in tags so ticket-style injection inside the answer cannot rewrite the judge's instructions, the schema forces unsupported claims before the verdict (evidence-first), and the structured-output constraint means parsing never breaks on prose. That smoke test at the bottom is your first sanity check — a judge that passes "24 hours vs 5–7 business days" as faithful is broken before calibration even starts.
Now the step most teams skip, which is the step that makes everything above trustworthy. Label 20 examples by hand, then measure whether the judge agrees with you.
Sourcing the 20: pull real question/context/answer triples from your system — not synthetic ones — and deliberately include roughly half you believe are unfaithful, because a judge tested only on good outputs proves nothing. Label them yourself against the same written definition the judge uses; where you hesitate, sharpen the definition first (your hesitation is a rubric bug). Store them as labeled.jsonl, one JSON object per line: question, context, answer, human_faithful.
# calibrate.py — agreement + Cohen's kappa for the judge
import json
from judge import judge_faithfulness
def cohens_kappa(human: list[bool], judge: list[bool]) -> float:
n = len(human)
p_observed = sum(h == j for h, j in zip(human, judge)) / n
h_yes, j_yes = sum(human) / n, sum(judge) / n
p_chance = h_yes * j_yes + (1 - h_yes) * (1 - j_yes)
return 1.0 if p_chance == 1 else (p_observed - p_chance) / (1 - p_chance)
human, judge, disagreements = [], [], []
for line in open("labeled.jsonl"):
ex = json.loads(line)
verdict = judge_faithfulness(ex["question"], ex["context"], ex["answer"])
human.append(ex["human_faithful"])
judge.append(verdict["faithful"])
if human[-1] != judge[-1]:
disagreements.append((ex, verdict))
agreement = sum(h == j for h, j in zip(human, judge)) / len(human)
print(f"Agreement: {agreement:.0%} Cohen's kappa: {cohens_kappa(human, judge):.2f}")
for ex, verdict in disagreements:
print(f"\nDISAGREE on: {ex['answer'][:80]}...\n judge said: {verdict['reasoning'][:150]}")
Agreement % is the intuitive number: on what fraction of the 20 did judge and human give the same verdict? Aim for 90%+ before trusting the judge, per the bar Module 6 set.
Cohen's kappa, in plain words: agreement % flatters you when the labels are lopsided. If 90% of your examples are faithful, a broken judge that answers "faithful" every single time scores 90% agreement while measuring nothing. Kappa asks: how much better than that dumb-luck baseline did the judge do? It rescales agreement so that 0 means "no better than guessing the majority label" and 1 means perfect. Read it roughly as: below 0.4, the judge is unusable; 0.4–0.6, weak — tighten the rubric; 0.6–0.8, solid working agreement; above 0.8, strong. A judge can have 90% agreement and kappa 0.3 at the same time — that combination means your label set is lopsided and the judge is coasting on it, and kappa is the number that tells you.
The disagreement printout is not decoration — it is the actual work product. Each disagreement is either a judge failure (tighten the prompt) or a human labeling error (fix the label) or an ambiguous rubric (sharpen the definition). Resolve all of them, re-run, and re-check. Then keep spot-checking a sample of judge verdicts forever, and re-calibrate whenever you change the judge prompt, the judge model, or the task distribution — agreement measured once is a snapshot, not a property.
Never tune the judge prompt against the same 20 labels you report kappa on — that is overfitting the referee. Hold out a slice of labels you only touch when declaring the judge calibrated, exactly as Module 6's holdout rule demands for the system under test.
Judges fail in known, mechanical ways. Memorize these three; interviews ask for them by name, and production suites break on them silently.
A fourth failure mode from Module 6 is worth restating in judge terms: criteria drift — a vague rubric invites the judge to substitute its own notion of "good." The mitigation is everything in the rubric-design section above: one criterion, defined terms, evidence-first output.
The practical combination: pointwise binary criteria as your permanent suite; a pairwise tournament when two candidate prompts both pass the suite and you need a tiebreak.
A trustworthy judge is: one binary criterion, defined terms, evidence before verdict, structured output, calibrated against held-out human labels to kappa ≥ 0.6, spot-checked forever, and never from the same model family as the contestants it ranks. Every word of that sentence is a checklist item.
A teammate proposes this pairwise judge prompt for choosing between two support-reply prompts (Module 9's task). It contains three planted defects from this module. Find them before opening the answer.
You are an expert evaluator. Below are two candidate replies to the same
customer ticket. Reply A was produced by our new prompt; Reply B by the old
one. We hope the new prompt is better.
A good reply is thorough and detailed — it should fully explain policies
so the customer never needs to write back.
Ticket: {{ticket}}
Reply A (new prompt): {{reply_a}}
Reply B (old prompt): {{reply_b}}
Grade each reply 1-10 and declare the winner. Respond with JSON:
{"score_a": n, "score_b": n, "winner": "A" or "B"}
Also worth asking: the team plans to run this judge on Claude Sonnet — the same model that generates Reply A. What fourth problem does that add, and what is the fix?
calibrate.py, and report agreement, kappa, and your resolution of every disagreement. If kappa lands below 0.6, revise the judge prompt once and re-measure on a fresh holdout.llm-rubric assertion with a javascript assertion that calls your calibrated judge (or re-express the calibrated criterion as the rubric text verbatim). Document which judge version and calibration run the suite now depends on.