Trusting your judges¶
An LLM judge is a model grading a model, so its scores are worth exactly as much as its agreement with a human. This page covers how AI Eval measures that agreement, the trust states it derives from the measurement, how to export the result for CI, and the configuration that decides which model judges and how it decodes. It is for whoever owns the eval numbers other people rely on.
Only LLM judges get a trust state and only they appear under Manage > Judges. Deterministic graders are left out on purpose: their score is a fixed function of their code, so there is no human agreement to measure and no model drift to track. See graders for which is which.
Why a judge needs validating¶
A judge produces a number for every response, and a number looks like a measurement whether or not it is one. Validation is what turns it into one. You label a set of real responses yourself, the judge re-scores the same responses, and the two verdict sets are compared cell by cell.
Raw agreement hides the failure mode that matters. A judge that passes everything agrees with you on every response you passed, which reads as high accuracy while telling you nothing. So AI Eval reports two rates instead of one.
| Rate | Question it answers | What a low value means |
|---|---|---|
| TPR, true positive rate | Of the responses you passed, how many did the judge pass? | The judge fails good work. |
| TNR, true negative rate | Of the responses you failed, how many did the judge fail? | The judge waves bad work through. |
A judge is trusted when both rates reach 0.90, following Hamel Husain's eval guidance and Shreya Shankar's work on judge validation. The threshold is a constant in the report, not a per-site setting.
When your gold set contains only one class (all passes or all fails), the rate
for the missing class is undefined. AI Eval stores it as unknown, never as 0.0,
and every surface renders it as n/a. An undefined rate can never masquerade as
a measured zero.
Editing the prompt¶
The template must contain four %s placeholders (dimension, question,
expectations, answer). Write %% for a percent sign. A template that does not
fit is refused on save. A template saved earlier that no longer fits makes
every answer it scores an error, with the reason on the result.
Answers the judge gave no score, because of an error or because it did not apply, are shown as Not scored on the report, history and Judges page. They are left out of the matrix, rates and repeat diagnostics. Any such answer blocks trust; check the judge and validate again. The judge comparison leaves out and counts answers a judge gave no score on any try.
Building a gold set¶
The gold-label and blind re-label screens need validate ai eval judges, which
also permits saving trace labels for judge validation. Promoting a trace into a
dataset additionally needs annotate ai eval results or administer ai eval.
The promotion control is hidden when you lack that permission. Importing traces
needs operate ai eval or administer ai eval.
Each LLM judge that scores with its own prompt has a gold labeling screen at
/admin/config/ai/ai-eval/judges/{grader_id}/gold, opened from its Label
traces tab. The decision rubric judge and content review judges have neither
the tab nor the page. The screen serves one trace at a time in a stable order. Mark it pass,
fail, or defer. Saving advances to the next unlabeled trace, wrapping to the first one
when needed. Deferred traces keep their place and are skipped during this
advance. Next unlabeled returns
to the first trace you have not labeled in this pass. The last trace stays
on screen and shows Pass complete when the pass is done. Defer means "come back to
this", so it does not count toward coverage.
Only final answers enter the gold pool: the normalized reply must have non-empty text and no response tool calls. A provider's finish reason does not decide this. The screen filters before filling its 200-answer window, so tool steps do not crowd answers out. Coverage and agreement use the same rule over the full domain. A domain containing only tool steps or empty replies explains that it has no final answers yet.
Labels already stored on non-final spans are kept, but are left out of validation. The judge page reports how many such labels were not used.
Two scoping rules decide which traces you see and what your labels are worth:
- Domain tags. A judge carries a list of domain tags, set on its Edit prompt tab. The tags define the judge's trace pool, so an accuracy judge for support answers is validated on support traces rather than on whatever happens to be imported. Saving tags never bumps the prompt version, so tagging a judge cannot invalidate a trusted validation of its current prompt.
- Dimension. Every label written from the gold screen is stamped with the
judge's dimension (
accuracy,relevance, and so on). Validation reads only labels in the judge's own dimension, so an accuracy gold set never leaks into a tone judge's numbers. Labels written from the trace review queue carry no dimension: they are holistic labels, and an LLM judge does not read them. - Results page reviews. The pass or fail people give an answer on a results page counts only for a judge without a dimension (the decision rubric judge, a content review judge), and only when that judge scored that answer in that run: it gave a score, zero included, without an error. A review of an answer from a test the judge was not on, or that the judge failed to score or did not apply to, says nothing about it and is left out. A review that counts is still a verdict on the whole answer while the judge checks only its own rules, so the validation report and the calibration page say so: an answer failed for a wrong fact counts against a judge that checks only style.
- Reviews from before the upgrade. Results stored before AI Eval recorded which judges scored each answer cannot say which judge a review is about, so their reviews no longer count for any judge's validation. They are kept, not deleted, and nothing guesses their judges from today's tests. The judge's validation page says how many were left out; run the test again and review the new results to validate on them. A judge with domain tags never reads results-page reviews (they carry no tags), so its page does not count them.
A coverage counter tracks pass and fail labels across the judge's whole
tag-scoped domain against the gold_target_n setting, 30 by default. The counter
is computed over the full domain rather than the visible window, so it cannot
overstate progress.
The blind re-label pass keeps the order of your first-pass label ids. Saving a verdict advances one position without revealing your earlier verdicts.
The Deferred: N count tracks deferred answers in the review window. Once every answer there is labeled or deferred, the page says All answers are labeled or deferred. N deferred. Use Review deferred answers to return to the first deferred answer. Defer never counts toward gold coverage.
Continuing an unfinished validation¶
Return to the judge page to continue its newest unfinished validation. The page shows how many answers have been scored and resumes polling. It uses the labels saved when the run started, even if you have since changed them. The launch form stays hidden while that run is open. Use Cancel to stop it and restore the launch form. Leaving the page pauses progress until you return; scoring still happens through the page's protected POST requests.
How reliable is the gold set itself¶
TPR and TNR compare the judge against a human, so neither can mean more than that human's labels do. If a labeler agrees with themselves 80% of the time on a dimension, a judge reported above that ceiling is measuring gold-set noise.
AI Eval measures the ceiling instead of assuming it. Span labels are stored per labeler and per round, so:
- With two reviewers labeling the same traces, both verdicts persist and the screen reports Cohen's kappa alongside raw percent agreement and the overlap count.
- With one reviewer, a blind re-label mode serves back traces you already labeled with your previous verdict hidden, stores the second pass as round 2, and reports your self-agreement the same way. Round 2 never feeds validation.
Undefined agreement (no overlap, or an overlap where only one class was used) is reported as undefined. Manage > Judges shows the same reliability figure next to each trust state, so a TPR/TNR verdict is always read against the quality of the set behind it.
Running a validation¶
A judge this site cannot run, such as the decision rubric judge when the AI module has no Decision operation, reads Not available on Manage > Judges. Its page says why and offers no validation, and the test forms show it disabled with the same reason. A test saved earlier with that judge keeps it.
Open the judge from Manage > Judges and use the launch form. It has four controls:
| Control | Values | Effect |
|---|---|---|
| Gold labels | All labels, Trace reviews only | Which pool of human labels to draw from. |
| Hold-out split | full, 70/30 |
full scores every available label. 70/30 scores a random 30% for a quicker run. |
| Threshold | 0 to 5 | The judge score at or above which a response counts as a pass, before it is compared to your label. Starts at the latest validation's threshold; with none, the field is empty and required. |
| Split seed | optional integer | Only used with 70/30. |
The threshold is on the 0 to 5 scale every judge is compared on, whatever the judge's own scale: validation normalizes each score first. The form says what that means for the judge: on a judge that scores 0 to 100, 4.5 is 90, and on a judge that scores 0 to 1, 2.5 is 0.5. The form also lists the tests that use the judge and that you can see, with their question pass threshold, and marks one that differs from the latest validation. A test's question threshold applies to the average of all its graders, not to one judge, so it is context, not a value to copy. The validation history and the run comparison show the threshold each validation ran at.
The run steps one example per poll and finishes with a confusion matrix, a disagreement inspector listing the false positives and false negatives, and a verbosity check reporting whether the judge favors long answers where your labels do not. The matrix reads "Human: pass / fail" against "Judge: pass / fail" whatever the judge's dimension, and its verdict uses the same words as the trust badges: Trusted or Failed validation. For each disagreement the inspector shows the judged response, your verdict and note, and the judge's verdict and its own reason; long text folds into an expandable block.
Seeds make a hold-out draw reproducible. Leave the field blank and a fresh seed
is generated and recorded; enter a seed from an earlier run to reproduce that
exact draw over the same gold-label pool. Adding or relabeling gold examples
changes the pool, and with it the draw. A full run never draws, so it records
no seed and any typed seed is ignored. Every validation is stored as a new
attempt, so distinct seeds are visible evidence of distinct draws rather than a
claim about them.
Sample size¶
The n behind a verdict is TP + FP + FN + TN. Answers the judge gave no score
are counted as not scored, apart from skipped labels, so they never inflate
n. Any not-scored answer blocks trust: check the judge and validate again. A verdict
resting on 12 labels and one resting on 300 are both reported with their n, and
Manage > Judges also shows how many validation attempts have accumulated, so a
judge validated repeatedly until it cleared the bar is distinguishable from one
validated once.
A judge is trusted only after at least 10 distinct pass labels and 10 distinct
fail labels were scored, with both rates at least 0.90. Repeated scores of one
answer count once. Below that floor, Not enough labels is a warning to add
labels and validate again. The gold_target_n setting is a labeling target,
not this minimum. Full uses every label; a 70/30 split of a small set rarely
reaches the minimum.
Comparing two validations¶
The judge's Compare runs tab sets two of its validations side by side: TPR and TNR, the length check, the smallest gap the judge can tell apart, the threshold, the verdict, the config version and the seed, with the change from the older to the newer. It starts on the newest two; pick any two of the judge's last 50 validations to compare those instead. The comparison has its own address, so you can bookmark or share it. When the two found exactly the same figures, which is common for repeats of one run, the page says so above the table.
Trust states¶
stateDiagram-v2
[*] --> not_validated
not_validated --> not_enough_labels: fewer than 10 pass or fail labels scored
not_enough_labels --> trusted: re-validated on enough labels
not_validated --> trusted: TPR and TNR both >= 0.90
not_validated --> untrusted: either rate below 0.90
trusted --> stale: report older than judge_trust_stale_days
trusted --> stale: judge now uses a different model
trusted --> stale: prompt edited after the report
stale --> trusted: re-validated above the bar
stale --> untrusted: re-validated below the bar
untrusted --> trusted: re-validated above the bar
Pages and the CLI name the states the same way: Trusted, Not validated
(not_validated), Not enough labels (not_enough_labels), Failed validation (untrusted) and Validation out of
date (stale). A test with no LLM judge, only graders such as Format, reads
No judge: there is nothing to validate. The dashboard's Judge not
trusted count covers every test whose worst judge is in one of the four
states that are not trusted, never-validated judges included, and leaves out
tests with no judge.
Trust decays with time. A trusted report older than judge_trust_stale_days (30
by default, and settable in configuration) reads as stale.
The state is computed when it is read, so changing the window re-evaluates every
stored report with no data migration. An untrusted verdict never decays into
stale: it stays untrusted regardless of age.
A trusted report also reads as stale when the judge now uses a different provider or model than the one recorded on the report, because the verdict measured a judge that no longer runs. Stale, not untrusted: nothing showed the new model to be wrong, it has simply not been measured.
A trusted report also reads as stale when you edit the judge's template or
guidance after the report. The report names the version it measured; the Judges
page shows it as v1 (now v2). A report on the default prompt reads as stale
once any override is saved. Saving only the model or domain tags does not
change the prompt version; a changed model still makes it stale by the model
rule. Fixture validations use the scores in the file and measure no prompt,
so they read as stale once the judge has an override. Validate in the browser
to measure the override. Trust is read when a page opens, so a result stored
earlier also loses its Trusted mark and corrected rate after a prompt edit.
Note
Trust is advisory by default. A judge that is not trusted still scores,
and every verdict it contributed to is marked provisional with the reason.
With refuse_unvalidated_judges on (Do not count a pass that rests on a
judge that is not trusted in Settings), such a gate pass is recorded as a
fail. A hard gate then makes Drush exit 1; a soft gate does not. The
setting applies to runs that finish after the change; earlier results keep
their verdict. See
running evaluations.
One thing validation cannot fix: a judge from the same model family as the model
it grades is not an independent measurement. Models over-score their own family's
output, so part of every score is the judge grading its own reflection, and the
TNR your gold set measured no longer transfers cleanly. AI Eval compares the two
families on every run and warns when they collide. Vendor-namespaced aggregator
model ids such as anthropic/claude-sonnet-4 resolve to the underlying family;
identities the heuristic cannot resolve are shown as unknown rather than as a
verified different family. The warning blocks nothing, since a same-family
judge may be the only judge you have, but read the scores as family-inflated
until a different family's judge is available.
The trust corpus, headless¶
The same corpus rendered under Manage > Judges is readable from the CLI, one row per LLM
judge. A judge that has never been validated is listed as not_validated rather
than omitted, so counting your validated judges is a subtraction rather than a
guess.
# Table: judge, trust, n, skipped, not scored, TPR, TNR, threshold, split,
# config version, seed, validated date, attempts
drush ai-eval:judges
# The same corpus as JSON, for CI and release checks
drush ai-eval:judges --json
ai-eval:judges (alias aej) is a reader and always exits 0.
The JSON is an array with one object per LLM judge and stable keys:
| Key | Type | Notes |
|---|---|---|
grader_id |
string | The grader plugin id. |
trust |
string | trusted, untrusted, not_enough_labels, stale, or not_validated. |
n |
integer | TP + FP + FN + TN behind the latest verdict. |
skipped |
integer | Examples excluded from the matrix. |
not_scored |
integer | Labeled answers the judge gave no score; null before this was recorded. |
tpr, tnr |
float | null when the rate is undefined. |
threshold |
float | The pass threshold that validation used. |
hold_out_split |
string | full or 70/30. Legacy null rows normalize to full. |
config_version |
integer | null when the shipped template was scored. |
seed |
integer | null when no hold-out draw was recorded. |
validated_at |
integer | Unix timestamp. |
attempts |
integer | Accumulated validation runs. 0 when never validated. |
Every key except grader_id, trust, and attempts is null for a judge that
has never been validated. A null is never collapsed to 0.
Validating from a fixture in CI¶
The UI path reads gold labels out of the database. The CLI path reads them from a YAML fixture, which is what a pipeline can commit and diff.
Both paths add a row to the judge's validation history, and its Labels column
says which pool a row used: this site for a validation run in the browser,
file: followed by what the file says about its labels for one given to drush,
and not recorded for rows from before the column existed. For
ai-eval:validate-judge that is the fixture's meta.labels, else
meta.label_source, else the file's name. For the content review module's
ai-eval-cr:import-run it is meta.label_source, else meta.labels, else a
note that the fixture does not say where its labels came from. Run validation in the browser never reads a file, so a judge
validated only from files has no labels for it: the page then says so and shows
the drush command for that judge.
# Export a sample of stored results into a labeling fixture
drush ai-eval:sample-traces --grader=accuracy_grader --n=20 \
--stratified=true --window-days=30 \
--output=data/judge_validation/accuracy-labels.yaml
# After filling in human_label by hand, validate the judge against it
drush ai-eval:validate-judge --grader=accuracy_grader \
--fixture=data/judge_validation/accuracy-labels.yaml
ai-eval:sample-traces (alias aest) takes --grader and --output (both
required), --n (default 20), --stratified (default true, balances pass and
fail rows where possible), and --window-days (default 30). It writes a meta
block plus one row per example, with human_label left null for you to fill in.
ai-eval:validate-judge (alias aevj) takes:
| Option | Default | What it does |
|---|---|---|
--grader |
required | The grader plugin id to validate. |
--fixture |
required | Read human labels from this YAML fixture path. |
--threshold |
3.0 |
The binary pass boundary, on the shared 0 to 5 scale. |
--json |
off | Also print the report as JSON. |
--force |
off | Proceed when the fixture's meta.grader does not match --grader. |
--resolution-confidence |
0.9 |
Share of a gap's ranked pairs the judge must get right for that gap to count as resolved. Only used when the fixture declares a pairs block. |
Exit codes make it a gate:
| Code | Meaning |
|---|---|
0 |
The report is trusted. |
3 |
Validation ran, and the judge is not trusted. |
1 |
Bad input: missing options, unreadable or unparseable fixture, an invalid row, a malformed pairs block, or no labeled rows. |
A fixture needs at least 10 distinct scored pass and 10 distinct scored fail items; otherwise the command exits 3.
CLI runs are persisted like UI runs, so a fixture validation shows up in the judge's history. They score the full label set with no hold-out draw, so the row records no seed and no config version. It measures no prompt, so it reads as out of date once the judge has a prompt override; validate in the browser to measure that override.
--threshold is expressed on the shared 0 to 5 scale and every grader score is
converted to that scale before comparison, so one threshold means the same thing
for every judge. A yes/no judge lands on exactly 0 or 5, so any threshold above 0
and up to 5 gives it the same verdict. If you hand-write a fixture whose
grader_score values are in some other unit, declare the scale under meta:
meta:
grader: groundedness_grader
min_score: 0
max_score: 1
Without it, the scale comes from the grader's own declaration, and failing that
the scores are read as 0 to 5, which is what fixtures written before per-grader
scales existed meant. Fixtures produced by ai-eval:sample-traces already carry
the right meta.
Discrimination resolution¶
TPR and TNR say whether a judge is right about one answer. They do not say how far apart two answers must be before it can tell them apart, which is the question that matters when something claims a small improvement.
A fixture can answer it by declaring ranked pairs alongside its rows. Each pair names a better row, a worse row, and the true quality gap between them in the fixture's own score units:
pairs:
- better: 12
worse: 13
gap: 1.0
The judge is credited with a pair only when it scores the better side strictly
higher: a tie is not a ranking. Pairs are grouped by their declared gap, each gap
is judged on its own pairs, and a gap counts as resolved when the one-sided 95%
lower bound on its ranking rate clears --resolution-confidence. The reported
resolution is the finest gap that holds up.
The bound is the gate, not the observed rate, so five correct rankings out of five establish nothing: the report publishes what the claim costs instead, which at the default confidence is 25 consecutive correct rankings at one gap. A judge that establishes nothing reports no figure rather than a zero, and pairs the judge scored on only one side are counted as unusable rather than dropped.
Resolution is measured on the fixture path only. The in-app validation screen works from your gold labels, which carry verdicts rather than ranked pairs, so it reports agreement rates and no resolution figure. The optimizer reads recorded resolutions to decide whether a candidate's gain is something any judge in the run can actually see.
The judge configuration surface¶
Decoding is pinned¶
The judge is the measurement instrument, so it should vary as little as the
provider allows. Before every scoring call AI Eval pins the judge provider to
temperature 0 and top_p 1.
That narrows judge variance. It does not remove it: no seed is pinned, and a hosted model can still return a different answer to an identical prompt. This is why the optimizer repeats every measurement rather than trusting a single run, and why the optimizer page describes judges as noisy. Read the pin as reducing avoidable variance, not as reproducibility.
Only knobs the model actually advertises are set, negotiated through the AI
module's provider configuration. A provider that exposes neither knob gets no
pin, and the step is a no-op rather than an error. Whether the pin applied is
recorded in the run identity snapshot and exported with the envelope
(judge_decoding_pinned), so "the judge was pinned" is a recorded fact about the
run rather than an assumption about the code.
Some models refuse the knobs their provider advertises. Claude Haiku 5.5, for
example, answers a request with temperature set by an error. When the error
names a pinned knob and says it is not accepted, the judge asks again without
the pin, and stays unpinned for the rest of the run, so the model costs one
extra call per run rather than one per question. The run identity then records
the judge as not pinned. Any other error still fails the judge as before.
To use the provider's own decoding instead, turn off Pin judges to
temperature 0 on the AI Eval settings form (pin_judge_decoding in
ai_eval.settings). It is on by default.
This is separate from the target's own sampling temperature. The target is the system under test and should normally run at its production temperature; see targets.
Which model judges¶
Every LLM judge uses the site judge unless it sets its own model. The site judge
is one provider and model pair, judge_provider and judge_model in
ai_eval.settings, chosen on the settings form. A judge's own model is set on
its Edit prompt tab, in the Judge model section, from the same provider
and model list. Leave it on Use the site judge model to follow the site
judge. The override is stored with the judge's other per-judge settings in
ai_eval.judge_config.{grader_id} as judge_provider and judge_model, and it
applies only when both are set.
Changing a judge's model is not a prompt edit, so it never creates a new prompt version. It does change what the judge measures, so it is tracked the other way round: every validation records the provider and model that judged it, and a trusted verdict earned on a different model than the one the judge uses now reads as stale until you validate again. Manage > Judges shows each judge's current model (marked "own model" or "site judge") and, when they differ, the model its latest validation used. Validations recorded before per-judge models existed carry no model, show "not recorded", and keep their verdict.
Resolution order for one scoring call:
- The provider and model come from the judge's own override when both are
set, otherwise from
judge_providerandjudge_model. - The prompt template comes from the judge's stored override when one exists, otherwise from the template the plugin ships. Overrides are versioned, and the version scored is recorded on the validation report.
- The response handed to the judge is capped at
judge_response_char_limit(4000 characters by default). This is a different cap fromresponse_char_limit, which bounds the stored copy of a response. - Decoding is pinned as described above.
The judge's reason is kept up to 1000 characters, and the disagreement inspector shows it next to the human reviewer's own note and the judged response.
Because both the site judge and the per-judge models are mutable, a run records
the judge identity in force at the time. The run snapshot carries judge_models,
one entry per LLM grader with the provider, model, and whether it came from the
judge's own setting or the site judge. judge_provider and judge_model keep
naming the run's judge: the shared pair when all of the run's judges use one
model, and mixed when they differ. Result pages and exported envelopes read
that snapshot rather than live settings, so changing a judge's model tomorrow
does not silently re-attribute yesterday's results. See
sharing results.
The same-family warning on the dashboard compares each LLM grader's own model with the target's model, so a judge that has its own model is checked on that model rather than on the site judge.
References¶
- Hamel Husain, Evals FAQ
- Shreya Shankar et al., Who Validates the Validators?