Skip to content

Judges & Scoring

LLM-as-a-judge turns a language model into a structured grader. A Judge takes a data row, prompts a model with a rubric, and returns a typed, validated score object — not free text — so the result drops straight into statistics, quality gates, and reports.

from typing import Annotated
from latent.agents import Judge
from latent.agents.scores import ScoredModel, OrdinalScore

class Quality(ScoredModel):
    quality: Annotated[int, OrdinalScore(scale=(1, 2, 3, 4, 5), pass_threshold=3)]

judge = Judge("quality", model="gpt-4o", output_type=Quality)
score = await judge.evaluate({"question": "...", "answer": "..."})
print(score.quality, score.quality_rationale)   # -> 4  "polite and on-topic..."

Why a Judge instead of a raw agent?

A Judge[T] is a ReActAgent specialized for evaluation. Compared to prompting a model yourself, it gives you:

  • Structured output, validated — the model is forced to return your Pydantic output_type, so you read score.quality: int, never a string you have to parse.
  • Auto-generated rubric — annotate fields with score types and the scoring prompt is generated for you (override with prompt_template when you need to).
  • Rationales for freeScoredModel injects a {field}_rationale for every score, so every number ships with the model's reasoning.
  • Batch + statistics built in — judges are await-able and .map()-able, and judge_flow runs one (or many) across a dataset with bootstrap confidence intervals, quality gates, and MLflow logging.
  • Composable scoringis_eligible skips rows a judge shouldn't grade, row_mapper reshapes a row before scoring, and column_prefix keeps multiple judges from colliding on shared field names.

Classifier[T] is the same machinery under a semantic name, used when the output model has a prediction field — pair it with classification_flow for accuracy / F1 / precision / recall.


Judge and Classifier

Judge[T] scores a data row against a Pydantic output schema. Classifier[T] is a semantic alias with identical behavior, used when the output model has a prediction field.

Construction

from latent.agents import Judge, Classifier

judge = Judge(
    name="qa_judge",
    model="gpt-4o",
    output_type=MyScoreModel,       # Pydantic model (required)
    prompt_template=None,            # auto-generated from score annotations if omitted
    system_prompt=None,              # defaults to "You are an expert evaluator..."
    temperature=0.0,
    max_tokens=4096,
    is_eligible=None,                # Callable[[dict], bool] | None — skip rows that don't match a predicate
    row_mapper=None,                 # Callable[[dict], dict] | None — transform a row before scoring
    column_prefix=None,              # str | None — prefix output columns (avoids collisions when scoring with multiple judges)
)

classifier = Classifier(
    name="intent_classifier",
    model="gpt-4o",
    output_type=IntentPrediction,
    prompt_template="Classify this message: {text}\nLabels: {labels}",
)

evaluate(row)

Formats the prompt_template with the row dict, calls the LLM with structured output, and parses the response:

score = await judge.evaluate({"conversation": "...", "question": "..."})
# score is an instance of output_type (e.g. MyScoreModel)

await Judge.__call__(row) delegates to evaluate(), making judges compatible with await task.map(...). Both evaluate() and __call__ are coroutines.


ScoredModel

A BaseModel subclass that auto-injects rationale fields. For every field annotated with BinaryScore, OrdinalScore, or ContinuousScore, a corresponding {field}_rationale: str field is created so the LLM can explain its reasoning.

from typing import Annotated
from latent.agents import ScoredModel, OrdinalScore, BinaryScore, ContinuousScore

class QAScores(ScoredModel):
    faithfulness: Annotated[int, OrdinalScore(
        scale=(1, 2, 3, 4, 5),
        pass_threshold=3,
        labels={1: "Hallucinated", 3: "Partial", 5: "Faithful"},
    )]
    relevance: Annotated[int, OrdinalScore(scale=(1, 2, 3, 4, 5))]
    factual: Annotated[int, BinaryScore(description="All claims are factually correct")]
    similarity: Annotated[float, ContinuousScore(min_value=0.0, max_value=1.0)]

This generates four additional fields automatically: faithfulness_rationale, relevance_rationale, factual_rationale, similarity_rationale -- all str with default "".

Score annotations

Annotation Type hint Parameters Use case
BinaryScore int description Pass/fail (0 or 1)
OrdinalScore int scale, labels, pass_threshold, description Likert scales (e.g. 1--5)
ContinuousScore float min_value, max_value, description Float ranges (e.g. 0.0--1.0)

Prompt generation

When prompt_template is omitted, Judge auto-generates a scoring rubric from the annotations using build_scoring_prompt():

from latent.agents import build_scoring_prompt

prompt = build_scoring_prompt(QAScores)
print(prompt)
Score on the following dimensions:

- faithfulness (1-5): faithfulness
  [1=Hallucinated, 3=Partial, 5=Faithful]
- relevance (1-5): relevance
- factual (0 or 1): All claims are factually correct
- similarity (0.0-1.0): similarity

For each score, provide a brief rationale explaining your reasoning:
- faithfulness_rationale: explain your faithfulness score
- relevance_rationale: explain your relevance score
- factual_rationale: explain your factual score
- similarity_rationale: explain your similarity score

Full example

from typing import Annotated
from latent.agents import Judge, ScoredModel, OrdinalScore

class ResponseQuality(ScoredModel):
    quality: Annotated[int, OrdinalScore(
        scale=(1, 2, 3, 4, 5),
        pass_threshold=3,
        description="Overall response quality",
    )]

judge = Judge("quality_judge", model="gpt-4o", output_type=ResponseQuality)
score = await judge.evaluate({"conversation": "User: Hi\nAssistant: Hello!"})

print(score.quality)              # 4
print(score.quality_rationale)    # "The response is polite and appropriate..."

Pre-built judges

The latent.agents.judges module provides ready-to-use judges for common evaluation patterns. Each is a factory function that returns a configured Judge instance.

Judge Input columns Score type What it measures
FaithfulnessJudge context, response ContinuousScore(0-1) Proportion of claims supported by context
HallucinationJudge context, response ContinuousScore(0-1) Hallucination rate
RelevanceJudge query, response OrdinalScore(1-5) Response relevance to query
CompletenessJudge query, response OrdinalScore(1-5) Completeness of the answer
ConcisenessJudge response OrdinalScore(1-5) Brevity without losing information
ConsistencyJudge response_a, response_b ContinuousScore(0-1) Consistency between two responses
StyleJudge response OrdinalScore(1-5) Writing style and tone
InstructionFollowingJudge instruction, response OrdinalScore(1-5) How well instructions were followed
RecoveryJudge context, response OrdinalScore(1-5) Error recovery quality
GuardrailsJudge response BinaryScore Safety/policy compliance
from latent.agents.judges import FaithfulnessJudge, RelevanceJudge

faith_judge = FaithfulnessJudge(model="gpt-4o")
score = await faith_judge.evaluate({
    "context": "The Eiffel Tower is 330 meters tall.",
    "response": "The Eiffel Tower is approximately 330 meters tall.",
})
print(f"Faithfulness: {score.faithfulness:.2f}")

Scoring a whole dataset

A single await judge.evaluate(row) scores one row. To score a dataset — with statistics, quality gates, and a rendered report — hand the judge to judge_flow (or classification_flow for a Classifier):

from latent.flows import judge_flow

result = await judge_flow(eval_data=df, judges=judge, gates={"quality": 3.5})
print(result["markdown"])      # statistical report with bootstrap CIs
assert result["all_passed"]    # gate check

judge_flow accepts a single judge or a list of judges (multi-judge scoring). See Eval Flows for the full pipeline and Statistical Analysis for what the numbers mean.


See Also