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 readscore.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_templatewhen you need to). - Rationales for free —
ScoredModelinjects a{field}_rationalefor every score, so every number ships with the model's reasoning. - Batch + statistics built in — judges are
await-able and.map()-able, andjudge_flowruns one (or many) across a dataset with bootstrap confidence intervals, quality gates, and MLflow logging. - Composable scoring —
is_eligibleskips rows a judge shouldn't grade,row_mapperreshapes a row before scoring, andcolumn_prefixkeeps 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¶
- Eval Flows — run judges across a dataset (
judge_flow,classification_flow) - Statistical Analysis — the statistics behind the scores
- ReAct Agent — the agent class
Judge/Classifierextend - Building Agents — the feature map for the whole stack