What Is a Rubric?
A rubric is a structured scoring guide. It decomposes subjective quality into named dimensions, each with a defined scale and concrete descriptions of what every score level looks like.
Instead of "rate this answer 1β10", a rubric asks: "Is it factually accurate? Does it fully address the question? Is the tone on-brand? Is it safe?" β and scores each independently, then combines them with weights.
Rubrics are most powerful with LLM-as-judge: a strong model applies the rubric to thousands of outputs, calibrated against human-scored anchors so its judgments stay trustworthy.
"A grading guide that turns 'good' into a set of explicit, weighted, independently-scored criteria."
- A teacher's essay grading sheet
- Judges' scorecards in a competition
- A code-review checklist
Why Rubrics Matter
Beyond Pass/Fail
Capture partial credit and nuance that binary matching throws away.
Diagnostic Detail
A per-dimension breakdown shows why an answer scored low, not just that it did.
Consistency
Explicit level descriptions make scores repeatable across judges and over time.
Scales with LLM Judges
Grade thousands of open-ended outputs that no exact matcher could handle.
Encodes Priorities
Weights let you say safety matters more than eloquence β explicitly and measurably.
Aligns Teams
Writing the rubric forces agreement on what quality actually means.
Anatomy of a Rubric
Each dimension has a weight and a scale whose levels are described concretely, so a judge has no room for vague interpretation.
name: support_reply_quality
dimensions:
- id: accuracy
weight: 0.4
scale: [0, 1, 2, 3]
levels:
0: "Contains a factual error."
1: "Mostly correct, minor omission."
2: "Correct and complete."
3: "Correct, complete, and anticipates follow-ups."
- id: tone
weight: 0.2
scale: [0, 1, 2]
levels:
0: "Rude or robotic."
1: "Neutral and professional."
2: "Warm, on-brand, empathetic."
- id: safety
weight: 0.4
critical: true # score 0 here fails the whole item
scale: [0, 1]
levels:
0: "Leaks data or gives unsafe advice."
1: "Fully compliant and safe."
Dimensions
The independent qualities being judged. Keep them orthogonal so scores don't double-count.
Anchored Scales
Every level has a concrete description β the key to consistent, repeatable scoring.
Weights
Express relative importance; they combine into a single comparable overall score.
Critical Gates
Some dimensions (safety) are veto criteria: failing them fails the whole output.
How Rubric Scoring Works
| Dimension | Weight | Score | Weighted | Rationale |
|---|---|---|---|---|
| Accuracy | 0.40 | 2 / 3 | 0.27 | Correct decision, omitted the exact window. |
| Tone | 0.20 | 2 / 2 | 0.20 | Warm and on-brand. |
| Safety | 0.40 | 1 / 1 | 0.40 | No leakage; compliant. |
Overall: 0.87 (normalized). Safety gate passed, so the item is valid.
Step-by-Step Implementation
Model the rubric
Represent dimensions, scales, weights, and critical gates as data.
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Dimension:
"""One scored quality dimension within a rubric.
Args:
id: Machine-readable dimension name.
weight: Relative importance in the overall score.
levels: Mapping of score value to its concrete description.
critical: If true, a zero score fails the entire item.
"""
id: str
weight: float
levels: dict[int, str]
critical: bool = False
@property
def max_score(self) -> int:
return max(self.levels)
@dataclass(frozen=True)
class Rubric:
"""A weighted collection of scoring dimensions."""
name: str
dimensions: list[Dimension]
Prompt the LLM judge
Ask for structured, per-dimension scores with rationales β never a single opaque number.
import json
from .models import Rubric
JUDGE_PROMPT = """You are a strict, consistent grader.
Score the RESPONSE against each rubric dimension.
Return JSON: {{"scores": {{"": }}, "rationale": {{"": ""}}}}.
RUBRIC:
{rubric}
QUESTION: {question}
RESPONSE: {response}"""
def render_rubric(rubric: Rubric) -> str:
"""Render a rubric into a compact, judge-friendly description."""
lines = []
for dim in rubric.dimensions:
levels = "; ".join(f"{k}={v}" for k, v in sorted(dim.levels.items()))
lines.append(f"- {dim.id} (0..{dim.max_score}): {levels}")
return "\n".join(lines)
def judge_response(rubric: Rubric, question: str, response: str, llm) -> dict:
"""Grade a single response with an LLM judge.
Args:
rubric: The rubric to apply.
question: The original user question for context.
response: The agent output being graded.
llm: Callable mapping a prompt to a JSON string response.
Returns:
A dict with ``scores`` and ``rationale`` per dimension.
Raises:
ValueError: If the judge returns malformed JSON.
"""
raw = llm(JUDGE_PROMPT.format(
rubric=render_rubric(rubric), question=question, response=response,
))
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"Judge returned invalid JSON: {raw!r}") from exc
Aggregate with weights & gates
Normalize each dimension, apply weights, and enforce critical gates.
from .models import Rubric
def aggregate(rubric: Rubric, scores: dict[str, int]) -> dict:
"""Combine per-dimension scores into a single normalized result.
Args:
rubric: The rubric that produced the scores.
scores: Raw integer score per dimension id.
Returns:
A dict with the ``overall`` score (0.0β1.0) and a ``passed`` flag
that is false when any critical dimension scored zero.
"""
total_weight = sum(d.weight for d in rubric.dimensions)
overall = 0.0
passed = True
for dim in rubric.dimensions:
value = scores.get(dim.id, 0)
if dim.critical and value == 0:
passed = False
overall += (value / dim.max_score) * (dim.weight / total_weight)
return {"overall": round(overall, 3), "passed": passed}
Calibrate against humans
Trust the judge only after measuring its agreement with human scores on a labeled sample.
def test_judge_agrees_with_humans():
"""The LLM judge must correlate strongly with human anchor scores."""
human, model = [], []
for anchor in load_anchor_set("rubrics/anchors.jsonl"):
result = judge_response(RUBRIC, anchor.question, anchor.response, llm)
model.append(aggregate(RUBRIC, result["scores"])["overall"])
human.append(anchor.human_overall)
assert pearson(human, model) >= 0.8, "Judge is not calibrated to humans"
Use Cases
Reply Quality
Score accuracy, tone, and safety of drafted replies before they reach a customer.
Faithfulness & Relevance
Grade whether answers are grounded in retrieved context and actually on-topic.
Writing Quality
Evaluate clarity, structure, and brand voice for generated marketing copy.
Code Review Rubric
Judge correctness, readability, and test coverage of agent-written code.
Best Practices & Pitfalls
Do
- Anchor every level. Concrete descriptions per score make judging repeatable.
- Ask for rationale. Requiring a reason improves and audits the judge's scores.
- Calibrate to humans. Verify judgeβhuman correlation before trusting it at scale.
- Keep dimensions orthogonal. Avoid criteria that secretly measure the same thing.
Avoid
- Vague scales. "Rate 1β10" without anchors produces noise, not signal.
- Judge = generator. Using the same model to grade itself invites bias.
- Too many dimensions. Ten overlapping criteria dilute focus and slow judging.
- Ignoring position bias. In pairwise judging, randomize order to avoid A/B skew.
FAQ
Can I trust an LLM to grade fairly?
Only after calibration. Measure agreement with human scores on an anchor set; if correlation is high, the judge is reliable for that rubric. Re-check when you change models.
Absolute scoring or pairwise comparison?
Pairwise ("is A better than B?") is often more reliable than absolute scores for subjective quality. Use rubrics for both β as scoring criteria or as comparison criteria.
How does this relate to golden sets?
Golden sets (Layer 1) define correctness for closed tasks. Rubrics grade open-ended quality where there's no single right answer. Many systems use both together.