Free-form answers graded by an LLM judge

The use case

Not every LLM workflow is a classifier with gold categories. You might summarize
support tickets, rewrite answers for tone, or grade free-form responses for
faithfulness. There is no single label_field to compute F1 against. So the team
adds an LLM-as-judge: a second model scores each output against a written
rubric, and you treat the mean score like a quality metric for migrations.

That solves the "how do we score this?" problem and creates a new one. If you
migrate or refine against the judge, you are optimizing prompts toward
whatever that second model prefers. If the judge is noisy, biased, or drifts when
you change the grading model, you can ship a "successful" migration that humans
would reject — with beautiful charts and a green holdout on a bad proxy.

The use case is the decision before the repair loop: when is a judge trusted
enough to optimize against?
You need a human calibration set, a quantitative
agreement check, and a hard stop when agreement fails — not vibes that "the
rubric looks good."

What driftless does here: treat the judge as a grading mode with an optional
human calibration gate. Measure judge↔human agreement (judge-check) and
refuse to optimize when MAE / correlation miss your bar.

The support-classifier testbed is label-F1 first. This post uses that as
contrast, then shows the judge contract shape, CLI, and CI scaffold from the
product itself.


Three grading modes (pick one)

Mode Contract Testbed example Gate
Classification eval.label_field support_classifier → macro-F1 min_f1
Customer score / pass score_field / pass_field quick_triage → escalate yes/no min_score
LLM-as-judge eval.judge (your summarization / RAG write-up) min_score + optional MAE/corr

quick_triage is closer to a rubric in spirit (binary escalate) but still
uses gold labels, not a second model. Jump to judge only when humans cannot
pre-label every row cheaply — and then calibrate.

Posts 12
stay on the F1 path. This post is for everyone else.


Why judge trust is a first-class problem

Putting a model inside the trust loop means:

Driftless keeps the judge injectable (deterministic stubs in tests), normalizes
scores to 0..1, and exposes judge_agreement() against a human-scored JSONL.


Contract shape

workflows:
  support_summary:
    run:
      command: python evals/run_summary.py
      input_path: evals/tickets.inputs.jsonl
      output_path: evals/summary.outputs.jsonl
    model:
      current: gpt-4o-mini
      env_var: SUMMARY_MODEL
    files:
      editable: [prompts/summary.md]
    eval:
      judge:
        rubric: |
          Award full marks if the summary is faithful to the ticket,
          names the category of ask, and stays under 3 sentences.
          Deduct for hallucinations or leaked internal IDs.
        scale_max: 5              # rubric 0..5 → normalized to 0..1
        # pass_threshold: 0.6     # optional per-row pass
        # input_field: text
        # output_field: summary   # if output is JSON; else raw text
        calibration_path: evals/judge_calibration.jsonl
        max_mae: 0.15             # gate: refuse migrate/compare if exceeded
        min_correlation: 0.80     # Pearson r vs human scores
    thresholds:
      min_score: 0.85
    migration:
      holdout_required: true

Calibration JSONL — one human-scored example per line:

{"input": "Please reverse the payment on my latest invoice.", "output": "Customer wants a charge reversed on the latest invoice.", "score": 5}
{"input": "App crashes on login.", "output": "The weather in Paris is lovely.", "score": 0}

score is on the rubric's scale_max (here 0..5). Driftless normalizes both
human and judge scores to 0..1 before MAE / correlation.

Scaffold comments for this block also live in driftless init templates.


Measure before you optimize

driftless judge-check -w support_summary
# prints records, MAE, correlation, and gate status (ok/FAIL) when configured

driftless judge-check -w support_summary --enforce
# same gates migrate/compare/refine apply — exit non-zero on failure

Example shape of a passing check:

support_summary — judge calibration check

  records: 40
  MAE: 0.112
  correlation: 0.86
  gates: max_mae=0.15 (ok), min_correlation=0.8 (ok)

gates passed — judge vs. human on 40 records: MAE=0.112, corr=0.86

If MAE is 0.22 against max_mae: 0.15, stop. Fix the rubric, calibration
set, or judge model — do not run migrate.

With gates set on the contract, compare / migrate / refine call
require_judge_agreement() automatically. You do not need --enforce for
those commands; judge-check --enforce is the preflight you run in CI and
locally.


CI: path-filtered judge-check

driftless init-ci --judge-check

Emits a workflow that re-runs when the rubric or calibration file changes, using
the composite Action:

- uses: driftless-dev/driftless@v0.3.1
  with:
    command: judge-check
    workflow: support_summary
    args: "--enforce"
  env:
    OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

Same Dependabot-shaped idea as post 3:
the file that defines "good" is what triggers the check.


Evidence in the migration PR

When a judge-graded migrate succeeds, the report and driftless view include:

That is the same evidence path as classification migrations — different oracle.


Honest limits

Risk Mitigation
Judge drifts when grading model changes Re-run judge-check --enforce; bump calibration
Thin calibration set Agreement undefined / weak — add rows before gating
Optimizing to game the judge Keep humans in the loop; refresh calibration from production spot-checks
Confusing judge trust with label trust Classification → post 5; free-form → this post

Judge trust ≠ "the numbers look smooth." It means documented agreement with
humans
before the repair loop spends tokens.


How this relates to the testbed

Workflow Grading Preflight
support_classifier F1 on categories audit-labels
quick_triage pass_field: escalate gold labels (no judge)
Your free-form workflow eval.judge judge-check --enforce

A future testbed addition could ship a tiny summarization workflow + calibration
file for a one-command demo. Until then, start from driftless init comments or
a RAG/agent fixture that already uses score_field /
docs/rag-and-agents.md and graduate to eval.judge
when you need rubrics.


Next steps