Skip to content

    Forward Deployed Engineer

    Model-Graded Evaluation: Judge the Judge First

    Use a model to grade free-text answers only after it matches human labels: write a fixed-label rubric, measure agreement, know the biases, set a gate.

    By Published 8 min read

    • fde
    • evaluation
    • llm-as-judge
    • rubric
    • calibration
    Share

    This lesson is part of Forward Deployed Engineer: Evaluate and Capstone. Enroll for free, then sign in, to read it in full.

    What you'll learn: when a model can grade free-text answers, how to write a fixed-label rubric, and why you check the judge against human labels first. No real model was run: the page states no real-model numbers.

    At a glance

    Figure 1. Where the judge sits in the evaluation loop

    The Eval service sends each answer from the Orchestrator and a reference to the Model gateway, which calls the judge model; the Eval service compares the label with a human labelOrchestrator to Eval service (answers); Eval service to Model gateway (answer + reference); Model gateway to LLM provider (judge call); Human labels to Eval service (ground truth)Orchestrator: answers under testOrchestratoranswers undertestEval service: compares labelsEval servicecompares labelsModel gateway: rubric promptModel gatewayrubric promptLLM provider: the judgeLLM providerthe judgeHuman labels: 20 answersHuman labels20 answersanswersanswer + referencejudge callground truth

    How to read this: the judge is one more model call, and only the human labels tell you whether its labels mean anything.

    • Use a model judge only where exact match fails, with two labels.
    • A judge is a model under test: check it against human labels.
    • Agreement needs a baseline: an always-"correct" judge can score high.
    • You set the threshold and sample size; the gate refuses a judge below them.

    Scenario

    Composite scenario, not a real company. Lumen Industrial Supply's copilot answers in free text, so exact match fails: "Warranty is 24 months" and "The warranty is two years" are the same answer. The team wants a model to grade answers after every change. The FDE's condition: the judge first grades 20 answers people already graded.

    What you will be able to do

    • Choose code, human or model grading.
    • Write a fixed-label rubric and a judge that returns a label or invalid.
    • Compute agreement and baseline, and block a weak judge.

    Prerequisites

    Core ideas

    Pick the cheapest grader that works

    As of 2026-11-03, Anthropic's evaluation guide ranks code grading fastest and most reliable, human grading slow and expensive, model grading fast and flexible: "test to ensure reliability first, then scale". Check it first.

    Answer to grade (Lumen) Grader Why
    "Which port does the console use?" Code: string match on 8443 Exact and free
    "How long is the warranty?" "24 months" or "two years" Model judge, after calibration Wording varies
    A price or safety condition the customer acts on Judge, plus a person reads a sample A wrong grade costs more

    A rubric with fixed labels

    The guide advises clear rubrics and a fixed output such as 'correct' or 'incorrect'. Any other reply becomes invalid: a disagreement, never a default.

    Verified: capstone/fde-product-discovery-copilot/lessons/M6-L5/m6_l5_judge.py

    RUBRIC = (
        "TASK: judge_answer\n"
        "You grade one ANSWER against one REFERENCE for a product question.\n"
        "Reply with JSON: {\"label\": \"correct\" or \"incorrect\", \"reason\": one sentence}.\n"
        "correct: every fact in the REFERENCE is stated, in any wording, and no fact is added that the REFERENCE lacks.\n"
        "incorrect: a REFERENCE fact is missing or changed, an unsupported fact is added, or the answer declines to answer.\n"
        "Use only the REFERENCE. Do not reward length or confidence."
    )
    
    
    class Verdict(BaseModel):
        label: str
        reason: str = ""
    
    
    def build_messages(question: str, reference: str, answer: str) -> list[Message]:
        body = json.dumps({"question": question, "reference": reference, "answer": answer})
        return [Message("system", RUBRIC), Message("user", body)]
    
    
    def judge(provider: LLMProvider, question: str, reference: str, answer: str) -> str:
        """One label from LABELS, or INVALID. A bad reply is a visible disagreement, never a silent default."""
        reply = provider.complete(build_messages(question, reference, answer), Verdict.model_json_schema())
        try:
            label = parse_json_model(Verdict, reply.text).label
        except ValueError:  # json.JSONDecodeError and pydantic ValidationError are both ValueErrors
            return INVALID
        return label if label in LABELS else INVALID

    Judges have known biases

    Bias What Zheng et al. report (§3.3) Check for Lumen
    Position A tendency to favour certain positions; a mitigation is to ask twice with the order swapped and accept a win only if it holds both times (§3.4). Not applicable: one answer at a time
    Verbosity Favouring longer answers, even when less clear or accurate Pad answers with a no-fact sentence; do labels change?
    Self-enhancement A judge may favour its own answers Grade with a different model than the writer
    Limited reasoning Limited maths and reasoning ability Check sums with code

    Anthropic's guide agrees with the self-enhancement row: use a different model from the writer. The paper tested 2023 models; measure yours.

    Verified: capstone/fde-product-discovery-copilot/lessons/M6-L5/m6_l5_judge.py

    PADDING = " This is a standard item from the Lumen range and is widely used on industrial sites."
    
    
    def padding_flips(judge_label: Callable[[str], str], answers: Sequence[str]) -> list[int]:
        """Positions whose label changes when a sentence with no new facts is appended: a verbosity probe."""
        return [i for i, a in enumerate(answers) if judge_label(a) != judge_label(a + PADDING)]

    Agreement is the test, and it needs a baseline

    Figure 2. Calibrating a judge before use

    flowchart TD accTitle: Calibrating a model judge before use accDescr: Humans label a sample first, the judge labels the same answers, agreement is computed, and the judge is used only if agreement reaches the threshold the reader set; otherwise the rubric is fixed and the judge runs again. A["Pick a sample of answers"] --> B["Humans label them first"] B --> C["Judge labels the same answers"] C --> D["Agreement, baseline, confusion count"] D --> E{"At or above your threshold?"} E -->|yes| F["Use the judge. Recalibrate when rubric, model or answer type changes"] E -->|no| G["Read the disagreements and fix the rubric"] G --> C

    How to read this: the loop runs on human labels fixed in advance; only a pass lets the judge grade anything you report.

    Predict first. A judge always answers "correct". Humans labelled 14 of 20 answers correct. What agreement does it get?

    Answer

    14 of 20: 70 percent. When one label dominates, high agreement proves little.

    • Agreement: the share of answers where judge and human labels match.
    • On MT-bench first-turn questions, without ties, Zheng et al. report 85 percent between GPT-4 and humans and 81 percent between humans (§4.2). That task compares two chat answers: do not copy 85 as your threshold.

    Verified: capstone/fde-product-discovery-copilot/lessons/M6-L5/m6_l5_judge.py

    def agreement(judge_labels: Sequence[str], human_labels: Sequence[str]) -> Agreement:
        if len(judge_labels) != len(human_labels):
            raise ValueError(f"{len(judge_labels)} judge labels but {len(human_labels)} human labels")
        if not human_labels:
            raise ValueError("no labels to compare")
        pairs = list(zip(human_labels, judge_labels))
        return Agreement(
            total=len(pairs),
            agreed=sum(h == j for h, j in pairs),
            confusion=dict(Counter(pairs)),
            disagreements=tuple(i for i, (h, j) in enumerate(pairs) if h != j),
            human_counts=dict(Counter(human_labels)),
        )
    
    
    class JudgeNotCalibrated(Exception):
        pass
    
    
    def require_calibration(result: Agreement, threshold: float, min_items: int) -> None:
        """The gate. The reader sets both numbers; there is no default on purpose."""
        if not 0 < threshold <= 1:
            raise ValueError("threshold must be above 0 and at most 1")
        if result.total < min_items:
            raise JudgeNotCalibrated(f"only {result.total} labelled answers, need at least {min_items}")
        if result.rate < threshold:
            raise JudgeNotCalibrated(f"agreement {result.rate:.0%} is below the threshold {threshold:.0%}")

    Worked example

    Five invented questions, four answers each. The label columns are scripted by hand, not from any real model. C is correct, I is incorrect.

    Questions and references: A, protocols of gateway G-1? "Modbus TCP and OPC UA." B, ingress rating of sensor S-1? "IP67." C, warranty on controller C-1? "Two years." D, port of the G-1 web console? "Port 8443." E, is S-1 certified for outdoor use? "Yes, when mounted with the supplied gasket." Answers 1 to 4 are for A, 5 to 8 for B, and so on.

    # Answer Human Judge v1 Judge v2
    1 G-1 supports Modbus TCP and OPC UA. C C C
    2 It speaks OPC UA and Modbus TCP, so both common plant protocols are covered. C C C
    3 G-1 supports Modbus TCP, OPC UA and MQTT, which covers most plant networks and cloud brokers. I C I
    4 G-1 supports OPC UA and Modbus TCP. C C C
    5 S-1 is rated IP67. C C C
    6 S-1: IP 67. C I C
    7 S-1 is IP67 rated. C C C
    8 S-1 is rated IP68 and is fully waterproof. I C I
    9 The warranty is two years. C C C
    10 Warranty is 24 months. C C I
    11 The warranty runs for two years from purchase. C C C
    12 Two years, extendable to five with a service contract. I C C
    13 The web console is on port 8443. C C C
    14 The console listens on 8443. C C C
    15 The web console uses port 8080. I I I
    16 I don't have the port number for that gateway. I I I
    17 Yes, it is certified for outdoor use when mounted with the supplied gasket. C C C
    18 Yes, S-1 can be used outdoors. I I I
    19 Yes, with the supplied gasket fitted. C C C
    20 Yes, when it is mounted with the supplied gasket. C C C

    The real output of python lessons/M6-L5/m6_l5_example.py, with a threshold of 0.85 and a minimum of 20 answers (both chosen for this example):

    rubric v1
    agreement 16/20 = 80%; always giving the commonest human label scores 70%
    human correct   judge correct   13
    human correct   judge incorrect 1
    human incorrect judge correct   3
    human incorrect judge incorrect 3
    disagreements at positions: 3, 6, 8, 12
    FAIL: agreement 80% is below the threshold 85%
    rubric v2
    agreement 18/20 = 90%; always giving the commonest human label scores 70%
    human correct   judge correct   13
    human correct   judge incorrect 1
    human incorrect judge correct   1
    human incorrect judge incorrect 5
    disagreements at positions: 10, 12
    PASS: the judge may be used for this rubric

    Version 1 was fooled by confident extra claims (3, 8, 12) and a format difference (6). It caught 3 of the 6 incorrect answers.

    • Version 1 is the rubric without its added-fact clause; version 2 is as shown.
    • Version 2 fixed 3, 6 and 8, still misses 12, and broke 10 ("24 months" equals "two years"). It catches 5 of 6.
    • All invented and illustrative. A stand-in judge that calls any answer of 8 or more words correct has 12 of 20 labels flip under the padding probe.

    At the customer: "Can we use AI to grade the answers?" Yes where code cannot grade, once their own people have graded 20 answers. Report agreement with the baseline, show the disagreements, fix the threshold first.

    Build it

    Code: capstone/fde-product-discovery-copilot/lessons/M6-L5/. The tests pin every number above.

    1. Label answers 1 to 10 with the Human column covered; save a JSON list.
    2. Save version 1's labels for the same ten answers as a second list:
    ["correct","correct","correct","correct","correct","incorrect","correct","correct","correct","correct"]
    1. Run the check from the capstone folder:
    PYTHONPATH=. ./.venv/Scripts/python lessons/M6-L5/m6_l5_judge.py --human my_labels.json --judge judge_labels.json --threshold 0.8 --min-items 10

    If your ten labels equal the page's Human column, the real output is:

    agreement 7/10 = 70%; always giving the commonest human label scores 80%
    human correct   judge correct   7
    human correct   judge incorrect 1
    human incorrect judge correct   2
    disagreements at positions: 3, 6, 8
    FAIL: agreement 70% is below the threshold 80%

    Eight of ten human labels are correct, so always answering "correct" scores 80 percent, and version 1 is below that. The command exits with code 1 on FAIL, for CI. Differences may mean an ambiguous rubric.

    To measure a real judge, pass AnthropicProvider (app/llm/anthropic_provider.py) to judge, with the key and model id in environment variables. Fill in this table with your own results.

    Judge model Rubric version Answers labelled Agreement Always-majority baseline Incorrect answers caught Padding flips Gate passed

    Decide first.

    Can this judge grade? (interactive when JavaScript is on)

    Rubric v2 agrees with humans on 18 of 20 answers (threshold 85 percent). The team wants a different judge model and to keep the pass. What do you say?

    • Fine: any judge model will pass
    • Recalibrate: the pass belongs to this rubric and judge model
    • Lower the threshold
    Answer

    Recalibrate: the pass belongs to this rubric and judge model. Recalibrate when the rubric, judge model or answer type changes.

    Model X writes the copilot's answers. A teammate wants X as the judge because it is set up. What do you do?

    • Use X: it knows the product
    • Use X with a lower threshold
    • Use a different model and calibrate it first
    Answer

    Use a different model and calibrate it first. A judge may favour its own answers; a lower threshold does not remove that.

    After a prompt change, answers get longer and the judge's pass rate rises. What do you check first?

    • Run the padding probe and compare a sample with human labels
    • Report the new pass rate
    • Raise the threshold
    Answer

    Run the padding probe and compare a sample with human labels. Longer answers lifting the pass rate is what verbosity bias looks like.

    Exercise

    Write 10 answers to two Lumen-style questions. You and a colleague label them separately, then compare with agreement.

    Acceptance criteria:

    • two label files made independently;
    • agreement, baseline and disagreeing answers;
    • one rubric sentence rewritten.

    Done when you can say whether your human-to-human agreement is high enough to hold a judge to.

    Hint 1: where to start

    Mix reworded right answers, extra claims and one decline.

    Hint 2: using the code

    Pass one person's labels as judge_labels, the other's as human_labels.

    Common mistakes

    • Reporting a judge's pass rate without human labels.
    • Quoting agreement without the baseline.
    • Labelling after seeing the judge's labels.
    • Letting the writing model judge its own answers.

    Knowledge check

    1. Why does the gate take a threshold and a minimum sample size from you instead of using defaults?

    The right threshold depends on what a wrong grade costs. With 20 answers, one answer moves the rate 5 points.

    2. A judge agrees with humans on 80 percent of answers, and 80 percent of the human labels are "correct". Is the judge useful?

    Not shown to be: always answering "correct" scores 80 percent too. Check how many incorrect answers it catches.

    3. Why does an unparseable judge reply become `invalid` and not "incorrect" or "correct"?

    A silent default would hide a broken judge. invalid never equals a human label, so it shows as a disagreement.

    4. Version 2's rubric fixed three errors and introduced one. What do you do?

    Compare the disagreements, not the totals. Then edit the rubric and re-run.

    5. The paper reports 85 percent agreement between GPT-4 and humans. Can you adopt 85 percent as your threshold?

    No. The paper compared two chat answers with 2023 models. Set your threshold from your own cost of a wrong grade.

    You can now

    • Pick code, human or model grading.
    • Write a fixed-label rubric and a judge.
    • Compute agreement, run a padding probe and block an uncalibrated judge.

    Further reading

    Last verified: 2026-11-03

    Next

    Back to the core path: AI evaluation report template: method, results, limits

    Enroll to keep reading

    Lesson 8 of 8 · Forward Deployed Engineer: Evaluate and Capstone

    Use a model to grade free-text answers only after it matches human labels: write a fixed-label rubric, measure agreement, know the biases, set a gate.

    In the rest of this lesson

    • Worked example
    • Build it
    • Exercise
    • Common mistakes
    • Knowledge check
    • You can now

    Enrolling is free. It unlocks every lesson in Forward Deployed Engineer: Evaluate and Capstone, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments