πŸ“‹
Layer 5 of 6 Β· Agent Evaluation Stack
Rubrics

Grade Quality Across
Every Dimension.

Not everything is pass/fail. Rubrics break "good" into explicit, weighted criteria β€” accuracy, completeness, tone, safety β€” and score each one, often with an LLM acting as a calibrated judge.

Criteria
Explicit dimensions
Scales
Defined score levels
Judges
LLM + human
Layer 5
Nuanced quality

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.

In One Sentence

"A grading guide that turns 'good' into a set of explicit, weighted, independently-scored criteria."


Analogy
  • 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.

rubrics/support_reply.yaml
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

LLM-as-Judge Pipeline
flowchart LR O["πŸ€– Agent output"] --> P["πŸ“ Build rubric prompt"] R["πŸ“‹ Rubric"] --> P P --> J["βš–οΈ LLM judge"] J --> S["πŸ”’ Per-dimension scores + rationale"] S --> W["βž— Apply weights & gates"] W --> F["πŸ“Š Overall quality score"] H["πŸ‘€ Human anchors"] -.calibrate.-> J
Example Scorecard
DimensionWeightScoreWeightedRationale
Accuracy0.402 / 30.27Correct decision, omitted the exact window.
Tone0.202 / 20.20Warm and on-brand.
Safety0.401 / 10.40No leakage; compliant.

Overall: 0.87 (normalized). Safety gate passed, so the item is valid.

Step-by-Step Implementation

1

Model the rubric

Represent dimensions, scales, weights, and critical gates as data.

rubrics/models.py
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]
2

Prompt the LLM judge

Ask for structured, per-dimension scores with rationales β€” never a single opaque number.

rubrics/judge.py
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
3

Aggregate with weights & gates

Normalize each dimension, apply weights, and enforce critical gates.

rubrics/score.py
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}
4

Calibrate against humans

Trust the judge only after measuring its agreement with human scores on a labeled sample.

tests/test_judge_calibration.py
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

Support

Reply Quality

Score accuracy, tone, and safety of drafted replies before they reach a customer.

RAG

Faithfulness & Relevance

Grade whether answers are grounded in retrieved context and actually on-topic.

Content

Writing Quality

Evaluate clarity, structure, and brand voice for generated marketing copy.

Coding

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.

Resources