Loading...

Implementing the LLM judge lifecycle from the Netflix paper

Netflix recently released a research paper on The Lifecycle of LLM-as-a-Judge for Large-Scale Recommendation Explanations. It treats an LLM judge like a model in production, with a benchmark, tuning loop, deployment gate, and drift monitoring. Let's see how to implement this practically, since the paper doesn't include an implementation codebase.

Using a second model to score the first is standard practice, and the architecture is well documented. Building one is less well documented. This walks through all four phases with working code you can run without an API key, including the places where the wording of a rubric, or the order of two fields in a JSON schema, quietly decides the verdict.

The problem

A model generates text for users, but nobody can manually review every response. So, you use a second model to evaluate the first against predefined criteria, a pattern commonly known as LLM-as-a-Judge.

All those criteria live in one place, a rubric. It tells the judge what passes, what fails, where the line is, and shows an example of each. It's just a plain Markdown file and that's the only thing the judge gets. No reference answer to compare against.

That leaves one problem. Nothing checks the judge.

What the paper proposes

Netflix published an answer, The Lifecycle of LLM-as-a-Judge. Treat the judge as a model in production, not a prompt you write once. That gives four phases.

PhaseWhat happens
BirthPeople take some real model outputs, like the one-line explanations shown next to a recommended title, and label each one pass or fail for every criterion. They also add a short note explaining why. These graded examples become the benchmark. They are basically the answer key used to measure how well the judge is doing later.
TrainingYou reword the rubric until the judge agrees with the human labels. A separate model does the rewriting, not the judge itself. Nothing gets retrained here, only the text changes.
DeploymentBefore an explanation is shown to anyone, the judge scores it. If it fails, the writing model tries again, and it is told which criterion it missed and why. If it still cannot pass, nothing is shown.
MonitoringEvery week, people and the judge score a new set of outputs. People may disagree with each other, so that gives us a baseline. If the judge disagrees too much with people, we consider it drift. Then we tune the rubric.

The paper explains the design, but there is no code with it.

This post fills that gap. The full build is on GitHub, LLM Judge Lifecycle, all four phases, running without an API key. Every number below is reproducible from it.

What the judge is grading

Think of a streaming app. Next to each recommended film there is a short line saying why it was picked for you. A model writes that line, and that line is what the judge grades.

For example, because you watched Zodiac, try Prisoners, another slow-burn crime mystery.

Four criteria decide whether that sentence can be shown.

The first three need judgement. The fourth is just counting words. Keep that in mind, it matters for the whole post.

What this post covers

We'll go step by step. First a cheap judge, worth having before you pay for one. Then the four phases in order, building the benchmark, tuning the rubric, putting the judge in front of the writer, and watching for drift. Two more things, cost and small samples, apply at every phase, so they come last.

Something to know before you start. The mistakes in this build do not crash. They produce numbers that look like results, and the tests stay green. If it crashed you would fix it. Because it does not, the wrong number ships.

Step 0. Write the free judge first

Code, src/rules.py. Run it with python run.py eval --criterion concise.

Before you build any of the four phases, ask a cheaper question. Does this criterion need a model at all?

The rubric is a markdown file, so the model reads it as prose. Add an inline tag to some of the bullets and a short script can read the same file.

The script is free and exact, but it only matches text. It can't handle paraphrasing or anything needing real understanding. That's where a model helps. Until the model judge beats simple string matching, there's no reason to pay for the cost and latency.

For one criterion, the rules scored 0.167 while the model scored 0.667. For another, 0.500 vs 1.000. That gap is what justifies paying for the model. It is measured, not assumed.

The rule engine is deterministic, so your tests stop depending on a model being available, and anyone can run the pipeline before paying for a key.

Phase 1. Build the benchmark

Code, src/benchmark.py. Run it with python run.py benchmark.

The benchmark is the answer key. People read a sample of real outputs and mark each one pass or fail on every criterion, with a short note on why. Ours is about eighty examples across the four criteria.

Split it into train, validation and test up front. You tune on train, check on validation, and only look at test at the end. Without that split you are tuning against the same examples you report scores on.

Decide the split by hashing the example id, not by position in the file. Ours started out position-based, and appending twelve new examples moved three of them across the boundary, so old scores stopped being comparable to new ones.

Phase 2. Tune the rubric

Code, src/prompts.py, src/rart.py. Run it with python run.py tune --criterion concise.

With the benchmark in place, phase two tunes the rubric until the judge matches those labels.

Small wording changes can flip the judge's answer, so keep at least one rule you can check yourself. Ours is the 12-40 word limit. Count the words and you know whether the judge is right.

In the first run the judge got 12 of 20 wrong, and all 12 were inside the limit. The reasons it wrote showed us why.

Here is one of those reasons, in the judge's own words. It counted 29 words against a limit of 40, and said the explanation passes. Then it wrote "however, following the exact rubric instructions", and returned FAIL.

So the judge knew the answer and still marked it wrong. Two things caused that, and fixing only one of them was not enough.

Rewording the prompt took 12 wrong verdicts to 5. Swapping the two fields took it to zero.

Why this is easy to miss

The number you watch when tuning is specificity. Of all the outputs people marked bad, how many did the judge also mark bad?

Now imagine a lazy judge that fails everything it sees. People marked 10 of the 20 outputs bad. The lazy judge fails all 20, so it caught all 10. Specificity is 1.000, a perfect score from a judge that did no work at all.

That is the trap. Failing too much moves the number up, not down.

So how do you catch it? On grounded, specific and safe you cannot, because checking those means re-reading every output yourself. The word count is different. 29 is under 40, so a FAIL there is provably wrong. That is what gave the bias away. Take that one rule out and every number would have looked fine.

Then check the tuning was worth it

Code, src/metrics.py. Run it with python run.py ablation --criterion concise.

That means scoring the tuned rubric against the untuned one.

In our run the tuned rubric won by 0.67. It won on rubrics that were byte-identical.

The score adds three parts, and one checks whether the judge agreed with the person for the same reason, not just the same verdict. That part only runs on the tuned side, so the untuned side scored zero on something nobody had measured on it.

Identical inputs have to produce identical scores. If they do not, the difference is in how you measured, not in what you changed.

Phase 3. Put the judge in front of the writer

Code, src/serving.py. Run it with python run.py serve.

Phases one and two give you a judge you trust. The last two make it run on its own.

Deployment puts the judge in front of the writing model. A rejected explanation goes back for another attempt with the reason attached, and if it still cannot pass, nothing is shown. That is why a gate makes sense here. A missing explanation costs a little attention. A wrong one reaches a paying customer and cannot be taken back.

Phase 4. Watch for drift

Code, src/monitoring.py. Run it with python run.py monitor.

Monitoring catches all of it going stale. People disagree with each other, so a fixed threshold is a number someone made up. The bar floats with the people instead, and the judge is out of line only when it sits below where they already are.

That gap is drift, and phase four is the only part that reports it. Teams usually skip it, so a judge can be wrong for months while the dashboard stays green.

Two things that cut across every phase

These two are not steps. They apply to every phase above, and both are easy to get wrong in the direction that flatters you.

What it costs

Code, src/providers/gemini.py, src/runtime.py. Every command prints its own token and cost tally.

You pay per token. A thinking model thinks privately before it answers, and you pay for those tokens too. The API reports them in a different field from the answer, so if you add up only the answer, your cost report comes out too small.

How much too small? On one prompt our writing model spent 1,746 tokens thinking, then wrote a 40 word sentence. That is 44 tokens of thought per token you can see. Most providers let you set a thinking budget, a cap on that private thinking. We set ours to zero and got a sentence just as good for none of it.

Check this on your own stack before sizing a retry budget. Here the judge was 87% of spend, because it runs once per criterion per attempt while the writing model runs once.

Reading small numbers

Code, src/metrics.py. Run it with python run.py eval --criterion concise --split test.

The whole benchmark is about eighty labelled examples across four criteria, and once you hold some back for testing, each criterion is scored on five to seven.

That is tiny. Get three out of three right and the report says 1.000. That looks solved, but it is three examples.

A confidence interval tells you the room for error. Score five out of six and the true number sits between 0.44 and 0.97, nearly the whole range. So the 0.83 you printed does not tell you much.

So print the interval next to every number. It stops a small sample being read as a finished result.

Use the Wilson interval, not the textbook normal approximation. The textbook one says 1.00 to 1.00 at eight out of eight, claiming certainty from eight examples.

What to take from this

Three numbers from this build looked like results and were not. None of them raised an error.

The full build is on GitHub, LLM Judge Lifecycle, with two domains and a test pinning each problem above. It runs offline. The architecture comes from the Netflix paper, The Lifecycle of LLM-as-a-Judge for Large-Scale Recommendation Explanations.