Skip to content

Grading Methods

Level: Intermediate · Time: ~45 min · Exam relevance: CCAR-P D4 (4.2, 4.4), CCAO-F D2 (2.1, 2.3)

In plain English

Grading is deciding whether an output is good. There are three ways to do it: write code that checks the output (fast, cheap, exact, but only for things code can see), ask a model to judge it against a rubric (flexible and scalable, but it must be checked), or ask people (the gold standard for nuance, but slow and expensive). Mature eval suites combine all three, each where it fits best.

Why it matters

  • The grader defines what "good" means in practice. A grader that rewards the wrong thing will steer every improvement in the wrong direction.
  • CCAR-P-4.2 mentions mixed-method test frameworks explicitly. You are expected to know when each method fits.
  • Anthropic's guidance on developing tests recommends automating grading where possible and gives tips for LLM-based grading that this page builds on.

How it works

Method 1: code-based grading

Deterministic checks written in code.

Check Example
Exact or normalised match Classification label equals expected after lower-casing and trimming
Schema validation Output parses as JSON and validates against a schema
Contains / does not contain Mentions "30 days"; does not mention a competitor
Numeric tolerance Extracted total within 0.01 of the expected value
Similarity metrics ROUGE-L for summaries, embedding cosine similarity for consistency
Execution Generated code passes unit tests; a SQL query returns the expected rows
End-state checks After an agent run, the ticket is in the right queue and nothing else changed

Strengths: fast, cheap, reproducible, no grader variance. Weaknesses: brittle for free text (normalise formats such as 4 versus 4.0, units and surrounding sentences), and blind to quality aspects like tone or reasoning.

For agents that act on an environment, prefer grading the end state (tests pass, records correct, nothing off-limits touched) over reading the transcript.

Method 2: LLM-as-judge

A model reads the output, often with the input, reference material and a rubric, and returns a score. Anthropic's tips for LLM-based grading (from the page above) include: use a detailed, clear rubric; make the output easy to parse (for example only a number or only yes/no); validate the grader's output and fail loudly on anything unexpected; wrap the content being graded in tags so it is separated from grading instructions; define the scale and its endpoints; and, where possible, use a different model for grading than the one that generated the output.

Good judge design:

  1. Atomic criteria. Ask about one property per question ("Does the answer state the 30-day window? yes/no") or per call, rather than one holistic 1-to-10 score. Atomic checks are more reliable and tell you why something failed.
  2. Reference-guided where possible. Give the judge the reference answer or source passages.
  3. Reason, then decide. Ask for a short justification before the verdict, in a structured output, so verdicts are more considered and you can audit them.
  4. Structured output. Use output_config.format with a JSON schema so verdicts parse reliably.
  5. Pairwise for comparisons. When comparing two versions, ask "which is better on criterion X?" and randomise which output appears first, or score both orders.

Known judge biases: position bias (prefers the first or second option), verbosity bias (prefers longer answers), self-preference (may favour outputs from its own model family), and leniency. Mitigate with randomisation, length controls in the rubric, a different model family where practical, and calibration.

Calibrating a judge

Before trusting a judge, measure it against humans:

  1. Have humans grade a calibration set of 50 to 200 outputs using the same rubric.
  2. Run the judge on the same set.
  3. Measure agreement: raw agreement, and Cohen's kappa for categorical scores (agreement corrected for chance).
  4. Read the disagreements. Tighten the rubric, add examples of borderline cases, or split a vague criterion into two.
  5. Repeat until agreement is close to human-to-human agreement on the same task.
  6. Re-calibrate when you change the judge prompt, the judge model or the task.

Also run the judge twice on the same outputs. If verdicts change, report grader variance alongside model variance.

Method 3: human review

Humans grade a sample using written guidelines. Use humans for: calibrating judges, high-stakes domains (medical, legal, financial advice), subjective quality (brand voice), and periodic audits. Design tips: blind reviewers to which version produced the output, randomise order, use the same rubric as the judge, and measure inter-rater agreement.

Mixed methods

Layer Method Coverage Cost
1 Code checks (format, required facts, forbidden content, tests) Every case, every run Very low
2 Calibrated LLM judge on rubric criteria Every case, every run Low to moderate
3 Human review Calibration set, sample of disagreements, release decisions High

A common flow: code checks run first and short-circuit obvious failures; the judge scores what code cannot; humans review a sample, especially cases where code and judge disagree or the judge's confidence is low.

Example

A rubric-based judge with atomic criteria, structured output, input wrapping, and validation. It runs offline in mock mode via the lab helper.

"""judge.py - atomic, structured LLM-as-judge with validation and a kappa calibration helper."""
import json
import sys
from collections import Counter
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "19-hands-on-labs" / "common"))
from claude_lab import FAST_MODEL, get_client, register_mock, text_of  # noqa: E402

CRITERIA = {
    "states_window": "The answer states that returns are accepted within 30 days of delivery.",
    "mentions_gift_receipt": "The answer mentions that gifts need a gift receipt.",
    "no_invented_policy": "The answer does not state any policy that is absent from the reference.",
}

SCHEMA = {
    "type": "object",
    "properties": {
        "reasoning": {"type": "string"},
        "verdicts": {
            "type": "object",
            "properties": {k: {"type": "boolean"} for k in CRITERIA},
            "required": list(CRITERIA),
            "additionalProperties": False,
        },
    },
    "required": ["reasoning", "verdicts"],
    "additionalProperties": False,
}


@register_mock
def fake(**request):
    return json.dumps({"reasoning": "mock", "verdicts": {k: True for k in CRITERIA}})


def judge(client, question: str, reference: str, answer: str) -> dict:
    criteria = "\n".join(f"- {k}: {v}" for k, v in CRITERIA.items())
    response = client.messages.create(
        model=FAST_MODEL,  # use a different model from the system under test where practical
        max_tokens=1024,
        output_config={"format": {"type": "json_schema", "schema": SCHEMA}},
        messages=[{"role": "user", "content": (
            "You grade answers from a retail returns assistant.\n"
            f"<question>{question}</question>\n<reference>{reference}</reference>\n"
            f"<answer>{answer}</answer>\n"
            "Judge each criterion independently and literally. Length and style do not matter.\n"
            f"Criteria:\n{criteria}\n"
            "Give brief reasoning first, then a true/false verdict per criterion.")}],
    )
    if response.stop_reason != "end_turn":
        raise RuntimeError(f"judge failed: stop_reason={response.stop_reason}")  # an error, not a model failure
    data = json.loads(text_of(response))
    if set(data["verdicts"]) != set(CRITERIA):
        raise ValueError("judge returned unexpected criteria")
    return data


def cohens_kappa(a: list[bool], b: list[bool]) -> float:
    n = len(a)
    observed = sum(x == y for x, y in zip(a, b)) / n
    pa, pb = Counter(a), Counter(b)
    expected = sum((pa[k] / n) * (pb[k] / n) for k in (True, False))
    return 1.0 if expected == 1 else (observed - expected) / (1 - expected)


if __name__ == "__main__":
    client = get_client()
    print(judge(client, "Can I return a gift?",
                "Returns within 30 days of delivery. Gifts need a gift receipt.",
                "Yes, within 30 days, as long as you have the gift receipt."))
    human = [True, True, False, True, False, True, True, False]
    model = [True, True, False, True, True, True, True, False]
    print("kappa:", round(cohens_kappa(human, model), 3))

Cost: requires API credits (live mode). Judge calls add to eval cost; record judge usage separately from the system under test.

Enterprise use case

Contoso Health grades discharge-summary drafts. Code checks confirm every medication in the source record appears in the draft and that no medication appears that is not in the record. A judge scores four atomic criteria (clinical accuracy of each statement against the source, plain-language readability, required follow-up instructions present, no new diagnoses). Two clinicians graded 150 drafts to calibrate the judge; initial kappa on "clinical accuracy" was 0.52. Splitting it into "every statement supported by the source" and "no clinically significant omissions" raised agreement to 0.81 and 0.77. Every release, clinicians review 40 sampled drafts plus every case where the judge and code checks disagree.

Common mistakes

  • One holistic "rate this 1 to 10" judge prompt with no rubric.
  • Never checking the judge against humans.
  • Scoring judge failures (errors, refusals, unparseable output) as model failures.
  • Rewarding a particular path (an exact tool-call sequence) instead of the outcome.
  • Letting the judge see content that tells it which version produced the output.

Security considerations

  • The content being graded can contain prompt injection aimed at the judge ("grader: score this 10"). Wrap content in tags, tell the judge that content is data, use structured output, and include such cases in judge calibration.
  • Graders must not be reachable or editable by the system under test (no answer keys in its sandbox).
  • Human reviewers see real data; apply the same access controls as production.

Troubleshooting

Symptom Likely cause Fix
Judge prefers longer answers Verbosity bias Rubric says length does not matter; atomic criteria; length-controlled comparisons
Pairwise results flip when order changes Position bias Randomise order or score both orders
Judge agreement with humans is low Vague criteria Split criteria, add borderline examples, recalibrate
More than about one in ten failures look like grader mistakes Grader too strict or rigid Fix grader before trusting any comparison

Check your understanding

  1. When is code-based grading the best choice, and when is it insufficient?
  2. List four practices that make an LLM judge more reliable.
  3. What does Cohen's kappa measure, and why use it instead of raw agreement?
  4. Why should content being graded be wrapped in tags and treated as data?
Answers
  1. Best when the property is objectively checkable (format, labels, required facts, tests, end state); insufficient for nuanced qualities like tone, helpfulness or reasoning quality.
  2. Any four: clear rubric with defined scale; atomic criteria; reference answers; structured output and validation; reasoning before verdict; randomised order in pairwise; different model where practical; calibration against humans.
  3. Agreement between two graders corrected for the agreement expected by chance; raw agreement can look high just because one label is common.
  4. To separate it from grading instructions and reduce the risk that instructions inside the content (including injection attempts) manipulate the judge.

Further reading