Ensemble¶
Multi-judge evaluation with configurable aggregation strategies.
Overview¶
Ensemble judging combines verdicts from multiple LLM judges to improve robustness and reduce individual model biases. All graders return EnsembleEvaluationReport for a consistent interface (single LLM is treated as "ensemble of 1").
Research Background
Verga et al. (2024) demonstrate in "Replacing Judges with Juries" that aggregating independent judgments from diverse models reduces systematic errors. Cross-family judging (using models from different providers) is particularly effective at mitigating self-preference bias documented by He et al. (2025).
Quick Example¶
from autorubric import LLMConfig
from autorubric.graders import CriterionGrader, JudgeSpec
# Ensemble with multiple judges
grader = CriterionGrader(
judges=[
JudgeSpec(LLMConfig(model="gemini/gemini-3-flash-preview"), "gemini", weight=1.0),
JudgeSpec(LLMConfig(model="anthropic/claude-sonnet-4-5-20250929"), "claude", weight=1.2),
JudgeSpec(LLMConfig(model="openai/gpt-4.1-mini"), "gpt", weight=1.0),
],
aggregation="weighted",
)
result = await rubric.grade(to_grade=response, grader=grader)
# Ensemble-specific fields. score / mean_agreement are `float | None`: score is None if
# the grade failed or no criterion was left to score, mean_agreement if nothing was
# measured (an empty rubric). Guard before formatting.
print(f"Score: {result.score:.3f}" if result.score is not None else "Score: n/a")
print(
f"Mean Agreement: {result.mean_agreement:.1%}"
if result.mean_agreement is not None
else "Mean Agreement: n/a"
)
print(f"Judge Scores: {result.judge_scores}")
# Per-criterion vote breakdown
for cr in result.report:
print(f"{cr.criterion.requirement}")
for vote in cr.votes:
print(f" {vote.judge_id}: {vote.verdict} ({vote.reason[:50]}...)")
Keyword Form¶
JudgeSpec takes the judge's config, its judge_id, and an optional weight, in that order. A
weight must be a positive, finite number (default 1.0); zero, negative, NaN and infinite weights
raise ValueError, because the weighted and threshold strategies sum and compare judge weights. When
you pass the config by keyword, call it judge_model_config:
spec = JudgeSpec(
judge_model_config=LLMConfig(model="openai/gpt-4.1-mini"),
judge_id="gpt",
weight=1.0,
)
spec.judge_model_config # read/write; the same object as spec.llm_config
The underlying dataclass field is still named llm_config, so JudgeSpec(llm_config=...) and
spec.llm_config keep working without a warning. Passing both keywords raises ValueError.
Judge IDs¶
Give every judge in judges= its own judge_id. A judge_id is the key that separates judges,
so judges that share one are merged. Judges of the same kind all call the model of the last one.
They also share a single judge_scores entry, one set of per-judge metrics, and the same option
order and few-shot examples. CriterionGrader still accepts a repeated judge_id for now, but
it emits a FutureWarning naming the repeated ids. A repeated judge_id will raise ValueError
in the next major version. A cascade already rejects repeats (see
Decision Models).
To poll one model several times, LLM or decision model, give each copy its own id. Each copy then
gets its own response-cache entries with cache_enabled=True, so a rerun replays each copy's own
answers, and each copy of an LLM judge also gets its own option shuffle:
Aggregation Strategies¶
| Strategy | Description |
|---|---|
majority |
Head count of MET vs UNMET votes; the larger count wins |
weighted |
Weighted vote using judge weights |
unanimous |
All non-abstaining judges must vote MET |
any |
Any judge voting MET results in MET |
These apply to binary criteria only and are independent of multi-choice aggregation
(ordinal_aggregation / nominal_aggregation). Conceptually, binary unanimous ≡ the
min over the {0, 1} option values and any ≡ the max; the ordinal analogs are the
min / max strategies (see the multi-choice cookbook).
Every strategy counts only MET and UNMET votes. CANNOT_ASSESS votes, including those of judge
calls that failed with an API or parse error, are set aside first, and a criterion whose votes
all abstain is CANNOT_ASSESS. A majority or weighted tie goes to the verdict that scores
lowest for the criterion's weight sign: UNMET for a positive (or zero) weight, MET for a
negative one.
A judge whose every call on an item failed judged nothing there, so its judge_scores entry
for that item is None, whatever its stand-in verdicts would score. When every judge
failed on every criterion, the item has no score at all: score and raw_score are None,
and the report's error begins Every criterion's judgment failed:.
AggregationStrategy¶
Enum for binary verdict aggregation strategies.
AggregationStrategy
module-attribute
¶
Strategy for aggregating votes from multiple judges (binary criteria).
- majority: Head count of MET vs UNMET votes; the larger count wins
- weighted: Weighted vote based on judge weights
- unanimous: All non-abstaining judges must vote MET
- any: Any judge voting MET results in MET
Only MET and UNMET votes count: CANNOT_ASSESS votes (including failed judge calls) are set aside first, and a criterion whose votes all abstain is CANNOT_ASSESS.
Tie-breaking (majority head-count tie or weighted equal-weight tie) resolves to
the score-minimizing verdict by weight sign: UNMET for weight ≥ 0 (earns 0), MET for
weight < 0 (applies the full penalty) — the binary analog of
Criterion.worst_scored_option. unanimous/any are thresholds, not ties.
EnsembleEvaluationReport¶
Evaluation result from ensemble grading with per-judge breakdown.
EnsembleEvaluationReport
¶
Bases: BaseModel
Evaluation report with ensemble voting details.
Extends EvaluationReport with per-judge breakdown and agreement metrics.
| ATTRIBUTE | DESCRIPTION |
|---|---|
score |
The final aggregated score (0-1 if normalized).
TYPE:
|
raw_score |
The unnormalized weighted sum.
TYPE:
|
llm_raw_score |
Same as raw_score (for compatibility with EvaluationReport).
TYPE:
|
report |
Per-criterion breakdown with ensemble voting details.
TYPE:
|
judge_scores |
Each judge's own score over the whole rubric (from its own verdicts),
keyed by
TYPE:
|
mean_agreement |
Average agreement across all criteria, or None when there are no criteria to agree on (empty rubric) / agreement was not measured.
TYPE:
|
cannot_assess_count |
Number of criteria with CANNOT_ASSESS final verdict.
TYPE:
|
token_usage |
Total token usage across all judges.
TYPE:
|
completion_cost |
Total cost across all judges.
TYPE:
|
error |
Error message if grading failed: no judge results, or "Every criterion's
judgment failed: ..." when each criterion's verdict stands in for failed calls
(each keeps its own error). When set,
TYPE:
|
EnsembleCriterionReport¶
Per-criterion result with individual judge votes.
EnsembleCriterionReport
¶
Bases: BaseModel
A criterion report with ensemble voting details.
Supports both binary and multi-choice criteria:
- Binary: Use final_verdict and votes (list of JudgeVote)
- Multi-choice: Use final_multi_choice_verdict and multi_choice_votes
| ATTRIBUTE | DESCRIPTION |
|---|---|
criterion |
The criterion being evaluated.
TYPE:
|
final_verdict |
Aggregated binary verdict from all judges. None for multi-choice.
TYPE:
|
final_reason |
Combined reasoning from judges: each aggregated (non-
TYPE:
|
votes |
Individual binary votes from each judge. Empty for multi-choice.
TYPE:
|
agreement |
Proportion of the aggregated (non-
TYPE:
|
final_multi_choice_verdict |
Aggregated multi-choice verdict. None for binary.
TYPE:
|
multi_choice_votes |
Individual multi-choice votes. Empty for binary.
TYPE:
|
error |
Set (with a category prefix) when the final verdict was driven entirely by
judge-call failures (every aggregated, non-
TYPE:
|
escalated |
True when this criterion was escalated: the vote first cast on it (a
decision model's) was judged insufficient because it errored, abstained, or its
TYPE:
|
JudgeVote¶
Individual judge's verdict for a criterion.
JudgeVote
¶
Bases: BaseModel
A single judge's vote on a criterion.
| ATTRIBUTE | DESCRIPTION |
|---|---|
judge_id |
Identifier for the judge (e.g., "gpt-4", "claude-sonnet").
TYPE:
|
verdict |
The judge's verdict (MET/UNMET).
TYPE:
|
reason |
The judge's brief justification for the verdict (the conclusion distilled
from
TYPE:
|
weight |
Judge's voting weight (default 1.0).
TYPE:
|
error |
Set (with a category prefix) when this vote's verdict was synthesized because the judge call failed. None for genuine votes.
TYPE:
|
reasoning |
The judge's verbose extended-thinking deliberation trace (populated
only when thinking is enabled; None otherwise).
TYPE:
|
probabilities |
The judge's probability for each verdict it was offered, keyed by
verdict value (
TYPE:
|
confidence |
The judge's support for the verdict it selected, in [0, 1]:
TYPE:
|
superseded |
True when this vote was recorded but not aggregated: a vote judged
insufficient (e.g. a decision model's low-confidence vote on a criterion that
was escalated to other judges) whose criterion's final verdict comes from the
other votes. It is kept for diagnostics and excluded from
TYPE:
|
is_error
property
¶
Whether this vote's verdict was synthesized due to a judge-call failure.
Use this instead of inspecting reason to distinguish error-induced
votes from genuine judgments.
References¶
He, J., Shi, J., Zhuo, T. Y., Treude, C., Sun, J., Xing, Z., Du, X., and Lo, D. (2025). LLM-as-a-Judge for Software Engineering: Literature Review, Vision, and the Road Ahead. arXiv:2510.24367.
Verga, P., Hofstatter, S., Althammer, S., Su, Y., Piktus, A., Arkhangorodsky, A., Xu, M., White, N., and Lewis, P. (2024). Replacing Judges with Juries: Evaluating LLM Generations with a Panel of Diverse Models. arXiv:2404.18796.