All articles

Your LLM Judge Is Untested Code

Your agent has tests. The thing grading them doesn't. Five rules for a judge you can trust.

Petar IvanovPetar Ivanov
β€’β€’7 min read
On this page

Every agent eval suite has one component nobody tests. You wrote careful scenarios, mocked the tools, and ran each one five times. Then you handed the final verdict to another LLM call and believed it.

That call is the judge. A wrong judge doesn't fail your suite. It passes replies that should have failed, and nobody hunts for bugs in a green build.

Treat the judge like any other component. Give it a narrow contract and tests that pass before its grades count. Here are five rules, with examples in Python. The same shapes work in a TypeScript suite.

1. Write criteria a human could check in five seconds

"The response is helpful and professional" is a vibe, and a judge will wave almost anything through it. "The response says the card will be blocked" is a fact you can check in five seconds. If a criterion takes longer, split it.

Keep three or four criteria per step, and ask about one criterion per judge call. When one prompt holds four criteria, the verdict on one sways the verdict on the next (Stureborg et al. 2024 measured this anchoring effect).

Store criteria as named rubrics in YAML, so every scenario reuses the same "card-blocking reply". Load them through a Pydantic model with extra="forbid". A typo in a field name then fails on load instead of quietly checking nothing.


2. Never ask the judge what code already knows

A judge is the slowest, most expensive, and least reliable check you have. Plain code answers which tools ran, what arguments the API got, and whether a refund total adds up. It's exact and free, and your mocked tools already log their calls.

Save the judge for meaning. "Your card ending in 4821 is now blocked" and "Done, I've blocked it" are both correct, and no regex covers that space. A criterion the judge keeps getting wrong usually belongs in code.


3. Count the threshold, never ask for a score

Sooner or later someone wants a number. Don't get it from the judge. In the same Stureborg study, models asked for a 1-100 rating put most replies between 70 and 100, bunched on round numbers, and shifted when the prompt was reworded.

Count binary verdicts instead:

  • A step: 3 of 4 criteria held β†’ 75%.
  • A scenario: 4 of 5 runs passed β†’ 80%.
  • The suite: 46 of 50 scenarios green β†’ 92%, the number you gate a release on.

Count the percentage from binary verdicts instead of asking the judge for a 1-100 score

A drop in any of those traces back to the criterion that flipped.

Two caveats. Mark the must-hold criteria, because on a plain average, an agent that never blocks the card still scores 75%. One failed must-hold fails the step.

For something that isn't binary, like tone, use three labels (fails, partial, holds) rather than a hundred. A reviewer can tell "partial" from "holds". Nobody can defend 78 over 82.


4. Calibrate the judge, then gate on it

This rule makes the rest trustworthy. A calibration set is a handful of hand-written replies, some clearly good and some clearly bad, each labeled with the verdict every criterion should produce. Run the judge over them with no agent and no mocks, because the judge is what's under test:

Python
# evals/judge.py
from typing import Literal

from pydantic import BaseModel


class Verdict(BaseModel):
    reason: str                     # what you read when a check fails
    label: Literal["pass", "fail"]  # the only two answers the judge can give


def judge(reply: str, criterion: str) -> Verdict:
    """One model call, one criterion, output parsed straight into a Verdict."""
    ...


# tests/evals/test_calibration.py
import pytest

from evals.calibration import load_calibration_cases
from evals.judge import judge


@pytest.mark.parametrize("case", load_calibration_cases(), ids=lambda c: c.name)
def test_judge_matches_hand_labels(case):
    for criterion, expected in case.expected_labels.items():
        verdict = judge(case.reply, criterion)
        assert verdict.label == expected, f"{criterion}: {verdict.reason}"

With that Literal as the output schema, the judge can't answer "mostly passes" or "8/10". Keep reason to one sentence. The structured-outputs docs warn that a field asking for step-by-step reasoning can trigger a refusal on current Claude models.

This test catches calibration drift. Someone rewords a criterion to fix one case, and a known-bad reply starts passing. Without the test, nothing tells you.

Small changes move judges a lot. Swapping only the order of two answers made GPT-4 flip its verdict on 46.3% of Vicuna-13B versus ChatGPT pairs, two closely matched models (Wang et al. 2023).

In a study of 13 judge models, most leaned toward "correct" when unsure (Judging the Judges). Lenient is the direction that turns a suite green. Those were older models, and your calibration set tells you how yours behaves.

Calibration runs as CI job 1; any mismatch with the hand labels skips the scenario job

Run calibration as its own CI job, before the scenarios. If the judge fails, CI skips the scenario job, and you don't burn minutes of agent runs on a judge you know is broken.


5. Grade with a different model, and let calibration size it

LLM judges recognize and favor their own writing. Panickssery et al. found GPT-4 and Llama 2 tell their own text from other models' text better than chance, and the bias grew when fine-tuning improved that recognition.

Let the agent's model grade its own replies, and that bias leans toward passing. Use a different model, ideally from another family, because judges also favor related models (Preference Leakage).

A bigger judge isn't always a better one. A narrow yes/no check is easier than writing the reply, though in Judging the Judges only the largest judges came close to human agreement.

Judge calls also add up. Four criteria, five runs, and 50 scenarios make 1,000 calls for one graded step. Your calibration set settles the size question. If a cheaper judge passes it, it's good enough.

One more known bias. MT-Bench found judges swayed by the order they read two replies in, and two of its three judges fell for padded answers. Grading one reply at a time removes the order problem, and binary criteria make length matter less.


A green suite should mean the agent passed

The judge decides what "working" means. That makes it the last thing to trust on faith. Give it a narrow job, make it count instead of guess, and test it before it grades anything.

Do that, and a green suite means the agent passed. Skip it, and a green suite means the judge said so.


πŸ“Œ TL;DR

  • Criteria a human can check in five seconds. Three or four per step, one per judge call.
  • Code checks what code knows. Tool calls, fields, and totals get exact checks. The judge grades meaning.
  • Count verdicts instead of asking for a score. Tally pass/fail across criteria, runs, and scenarios, and mark the must-holds.
  • Calibrate and gate. Hand-labeled replies run first in CI, and a failing judge skips the scenarios.
  • Grade with a different model, ideally from another family, sized by calibration.

Split out of The 3 Layers of Testing AI Agents, which covers the full three-layer setup these judges plug into.

Related articles

Whenever you’re ready, here’s how I can help you:

  1. 1.

    The Conscious React: React architecture, design & clean code β€” 100+ production tips across 6 chapters, updated for React 19, plus 4 companion repos you can clone and run.

  2. 2.

    The Conscious Node: Node.js architecture, design & clean code β€” 157 production tips across 10 chapters, from module boundaries to the transactional outbox and zero-downtime deploys.

  3. 3.

    The JavaScript Architect Bundle: Both Complete Packages: the two books, all 7 React and Node companion repos, and the AI rules files.

  4. 4.

    Free Resources: Architecture playbooks, cheat-sheets, and the JavaScript Architect Roadmap β€” practical guides for leveling up to senior.

The T-Shaped Dev

Get both architecture playbooks free

30K+ engineers read The T-Shaped Dev. Sign up here and you get the React and Node.js Architecture Playbooks as PDFs, then new deep dives when they're published. Unsubscribe in one click.

Petar Ivanov

Written by

Petar Ivanov

Software engineer, author, and speaker. I help JavaScript developers grow from Mid β†’ Senior β†’ Architect β€” production-grade React, Node.js, and AI systems.