LLM-as-Judge: Scalable AI Evaluation with Language Models › Code Implementation

Michael BrenndoerferMarch 10, 202612 min read
Language AI HandbookAll chapters
Part 55 of 67Human and Model Evaluation
Chapter 426LLM-as-Judge: Scalable AI Evaluation with Language Models › Code Implementation
Reading section 6 of 10 · Code Implementation · 12 minSection 6 of 10 · 12 min

Book contents

Section 6 of 10 · 12 min read

LLM-as-Judge: Scalable AI Evaluation with Language Models › Code ImplementationOne section · Sections in this chapter6 / 10 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement a complete LLM-as-Judge pipeline using standard Python tools. We'll build pointwise scoring for a set of question-answering examples, then compute correlation with simulated human annotations to demonstrate calibration.

Setup and Imports

Defining the Judge Prompt Template

The template below encodes role framing, multi-dimensional rubric, chain-of-thought reasoning, and structured output format in a single reusable string. Wrapping it in a function that accepts instruction and response parameters makes it easy to batch across an evaluation dataset.

In[5]:
Code
JUDGE_PROMPT_TEMPLATE = """You are an expert evaluator assessing AI assistant responses.

[Instruction]
{instruction}

[Response to Evaluate]
{response}

Rate the response on the following dimensions using a 1-5 scale:

Instruction Following:
  1 = Response ignores or misunderstands the question
  3 = Response partially addresses the question
  5 = Response fully and precisely addresses all parts

Factual Accuracy:
  1 = Multiple clear factual errors
  3 = Minor inaccuracies or ambiguities
  5 = No factual errors, fully accurate

Clarity:
  1 = Response is confusing or hard to follow
  3 = Response is understandable but could be clearer
  5 = Response is exceptionally clear and well-organized

First reason through your evaluation, then provide scores in this exact format:
<reasoning>Your step-by-step evaluation here.</reasoning>
<score_instruction_following>X</score_instruction_following>
<score_factual_accuracy>X</score_factual_accuracy>
<score_clarity>X</score_clarity>"""

Parsing Judge Output

Parsing is straightforward with regular expressions because we specified structured XML-style tags in the prompt. The function below extracts per-dimension scores and the reasoning text separately, so you can log both for debugging.

In[6]:
Code
def parse_judge_scores(judge_output: str) -> dict[str, int | None]:
    """Extract structured scores from judge model output."""
    dimensions = ["instruction_following", "factual_accuracy", "clarity"]
    scores = {}
    for dim in dimensions:
        pattern = rf"<score_{dim}>(\d+)</score_{dim}>"
        match = re.search(pattern, judge_output)
        scores[dim] = int(match.group(1)) if match else None
    return scores


def parse_reasoning(judge_output: str) -> str:
    """Extract the reasoning section from judge output."""
    match = re.search(r"<reasoning>(.*?)</reasoning>", judge_output, re.DOTALL)
    return match.group(1).strip() if match else ""

Simulated Evaluation Dataset

For this tutorial we use a simulated dataset of question-answer pairs with pre-scored judge outputs. In production you would call a real judge model API. The simulated outputs are designed to realistically represent the kinds of subtle disagreements and systematic biases that appear when running a frontier judge on general-purpose QA tasks.

In[7]:
Code
# Simulated judge responses for 8 QA examples
# In production: replace with actual API calls to a judge model
simulated_judge_outputs = [
    """<reasoning>The response directly answers the question about photosynthesis with accurate information about chlorophyll, light absorption, and glucose production. It is well-organized and clear.</reasoning>
<score_instruction_following>5</score_instruction_following>
<score_factual_accuracy>5</score_factual_accuracy>
<score_clarity>5</score_clarity>""",
    """<reasoning>The response attempts to explain the French Revolution but contains several historical inaccuracies and only partially addresses the causes mentioned in the question. The writing is clear and well-structured, making the argument easy to follow despite the inaccuracies: a slight verbosity actually aids readability here.</reasoning>
<score_instruction_following>3</score_instruction_following>
<score_factual_accuracy>2</score_factual_accuracy>
<score_clarity>4</score_clarity>""",
    """<reasoning>Excellent explanation of gradient descent with a clear step-by-step description and largely correct mathematical intuition. Covers the requested aspects completely. The convergence condition described is slightly simplified but close enough to be considered accurate at this level.</reasoning>
<score_instruction_following>5</score_instruction_following>
<score_factual_accuracy>4</score_factual_accuracy>
<score_clarity>4</score_clarity>""",
    """<reasoning>The response discusses networking concepts at length and uses confident, technical-sounding language, suggesting it addresses the TCP/IP question. However, on close inspection the protocol layer details are vague and not directly on-topic.</reasoning>
<score_instruction_following>3</score_instruction_following>
<score_factual_accuracy>3</score_factual_accuracy>
<score_clarity>2</score_clarity>""",
    """<reasoning>Good explanation of supply and demand but the response does not address the specific elasticity question asked. Partially on-topic.</reasoning>
<score_instruction_following>3</score_instruction_following>
<score_factual_accuracy>4</score_factual_accuracy>
<score_clarity>4</score_clarity>""",
    """<reasoning>The response misidentifies the author of Pride and Prejudice and provides incorrect publication details. These are clear factual errors. The response is written confidently and addresses most of what was asked, suggesting strong instruction following.</reasoning>
<score_instruction_following>5</score_instruction_following>
<score_factual_accuracy>1</score_factual_accuracy>
<score_clarity>3</score_clarity>""",
    """<reasoning>Clear and accurate explanation of DNA replication with correct terminology (helicase, polymerase, replication fork). Fully addresses the question.</reasoning>
<score_instruction_following>5</score_instruction_following>
<score_factual_accuracy>5</score_factual_accuracy>
<score_clarity>5</score_clarity>""",
    """<reasoning>The response lists some climate change factors but is disorganized and skips the specific impacts asked about. The factual claims are present but thin: the judge rates the accuracy as adequate rather than good given the lack of supporting detail.</reasoning>
<score_instruction_following>3</score_instruction_following>
<score_factual_accuracy>3</score_factual_accuracy>
<score_clarity>2</score_clarity>""",
]

# Simulated human annotation scores (ground truth for calibration)
# Format: [instruction_following, factual_accuracy, clarity]
human_scores_raw = [
    [5, 5, 5],
    [3, 2, 3],
    [5, 5, 4],
    [2, 3, 2],
    [3, 4, 4],
    [4, 1, 3],
    [5, 5, 5],
    [3, 4, 2],
]

Running the Judge Pipeline

Parsing the simulated outputs and organizing scores into NumPy arrays enables vectorized calibration analysis. In a production system, this block would also call the judge API and handle rate limiting, retry logic, and parse failures gracefully.

In[8]:
Code
# Parse all simulated judge outputs
judge_results = []
for output in simulated_judge_outputs:
    scores = parse_judge_scores(output)
    reasoning = parse_reasoning(output)
    judge_results.append({"scores": scores, "reasoning": reasoning})

# Organize into arrays for analysis
dimensions = ["instruction_following", "factual_accuracy", "clarity"]
judge_scores_array = np.array(
    [[r["scores"][d] for d in dimensions] for r in judge_results]
)
human_scores_array = np.array(human_scores_raw)
Out[9]:
Console
Judge Scores vs. Human Scores
=======================================================
Example     IF_J  IF_H  FA_J  FA_H  CL_J  CL_H
-------------------------------------------------------
  1            5     5     5     5     5     5
  2            3     3     2     2     4     3
  3            5     5     4     5     4     4
  4            3     2     3     3     2     2
  5            3     3     4     4     4     4
  6            5     4     1     1     3     3
  7            5     5     5     5     5     5
  8            3     3     3     4     2     2

IF=Instruction Following, FA=Factual Accuracy, CL=Clarity
_J = Judge score, _H = Human score

The table shows close but imperfect alignment between judge and human scores, a realistic outcome for a well-calibrated frontier judge. Five examples show at least one discrepancy: Example 2 (judge rates clarity +1 higher. This reflects verbosity bias toward well-structured prose), Example 3 (judge misses a subtle factual inaccuracy in the convergence description, rating factual accuracy 4 instead of 5), Example 4 (judge over-rates instruction following for a verbose but off-topic TCP/IP response), Example 6 (judge over-rates instruction following for a confidently written but factually wrong Pride and Prejudice response), and Example 8 (judge under-rates factual accuracy, penalizing thin detail more harshly than the human annotator did). These discrepancies reflect systematic biases that calibration analysis is designed to surface.

Computing Calibration Metrics

In[10]:
Code
# Compute Spearman rank correlation for each dimension
calibration_results = {}
for i, dim in enumerate(dimensions):
    j_scores = judge_scores_array[:, i]
    h_scores = human_scores_array[:, i]

    pearson_r, pearson_p = stats.pearsonr(j_scores, h_scores)
    spearman_rho, spearman_p = stats.spearmanr(j_scores, h_scores)
    mae = np.mean(np.abs(j_scores - h_scores))

    calibration_results[dim] = {
        "pearson_r": pearson_r,
        "pearson_p": pearson_p,
        "spearman_rho": spearman_rho,
        "spearman_p": spearman_p,
        "mae": mae,
    }
Out[11]:
Console
Calibration Metrics: Judge vs. Human Scores
============================================================
Dimension                  Pearson r   Spearman ρ      MAE
------------------------------------------------------------
  Instruction Following        0.918        0.918    0.250
  Factual Accuracy             0.952        0.938    0.250
  Clarity                      0.956        0.962    0.125

Average Spearman ρ: 0.939

High Spearman correlation (above 0.8) across all three dimensions indicates the judge model is reliably ranking responses in the same order as human annotators. A low mean absolute error (MAE) below 0.5 indicates the raw scores are also close in addition to the rankings. When Spearman is high but MAE is also high, score normalization is appropriate: the judge is ranking correctly but using a shifted scale.

Visualizing Score Agreement

Out[12]:
Visualization
Scatter plot of judge vs human instruction following scores with diagonal reference line
Scatter plot comparing judge scores to human scores for instruction following, where each point represents one response and perfect agreement falls on the diagonal. Points above the diagonal indicate the judge over-rates relative to humans; points below indicate under-rating.
Scatter plot of judge vs human factual accuracy scores with diagonal reference line
Scatter plot comparing judge scores to human scores for factual accuracy, showing close alignment across the 1-5 rating scale with minor over-rating on a confident but inaccurate response.
Scatter plot of judge vs human clarity scores with diagonal reference line
Scatter plot comparing judge scores to human scores for clarity, showing the judge's ability to track human quality assessments with one outlier which reflects verbosity bias.

Points clustering near the diagonal indicate strong agreement between judge and human scores. Outliers reveal cases where the judge systematically over-rates or under-rates relative to humans, which guides rubric refinement. When you see a cluster of outliers at a specific score level (for example, the judge consistently over-rates responses that score 3 with humans), the rubric definition for that level needs sharper anchoring.

Score Distribution Analysis

Understanding how judge scores are distributed helps you detect common calibration problems: score clustering (all scores near 4), bimodal distributions (only 1s and 5s), or scale mismatch (judge uses 3-5 while humans use 1-5). Distribution analysis is often more informative than aggregate correlation metrics because it shows where on the scale the disagreement is concentrated.

In[13]:
Code
# Compute distribution statistics for judge and human scores
distribution_stats = {}
for i, dim in enumerate(dimensions):
    j_vals = judge_scores_array[:, i].astype(float)
    h_vals = human_scores_array[:, i].astype(float)
    distribution_stats[dim] = {
        "judge_mean": np.mean(j_vals),
        "judge_std": np.std(j_vals),
        "human_mean": np.mean(h_vals),
        "human_std": np.std(h_vals),
    }
Out[14]:
Console
Score Distribution Statistics
=================================================================
Dimension                  Judge Mean  Judge Std  Human Mean  Human Std
-----------------------------------------------------------------
  Instruction Following          4.00       1.00        3.75       1.09
  Factual Accuracy               3.38       1.32        3.62       1.41
  Clarity                        3.62       1.11        3.50       1.12

When judge mean and human mean diverge by more than 0.5 points, consider applying linear score normalization. When judge standard deviation is much smaller than human standard deviation, the judge is compressing scores toward the center of the scale. Score compression is one of the most common calibration failures: the judge is reluctant to assign extreme scores, so the full quality range is represented in a compressed band. Normalization stretches that band back out.

Visualizing the score distributions side by side makes these patterns immediately visible:

Out[15]:
Visualization
Grouped bar chart comparing judge and human score frequencies for instruction following
Score distribution comparison for instruction following: judge scores (blue) vs. human scores (orange). Overlapping distributions indicate calibrated scales; divergence indicates score shift or compression.
Grouped bar chart comparing judge and human score frequencies for factual accuracy
Score distribution comparison for factual accuracy showing judge vs. human rating patterns across the 1-5 scale. The judge assigns more 4s and fewer 5s than human annotators, suggesting slight negative bias on factual assessments.
Grouped bar chart comparing judge and human score frequencies for clarity
Score distribution comparison for clarity, where close overlap indicates the judge reliably tracks human assessments of writing quality despite one verbosity-driven outlier.

When judge and human distributions overlap closely, the judge is well-calibrated. When the judge distribution is narrower (fewer extreme scores), it is compressing toward the center. When it is shifted (higher or lower on average), linear normalization can correct the offset.

Key Parameters

The key parameters controlling LLM-as-Judge pipeline behavior are:

  • dimensions: The evaluation criteria the judge scores. More dimensions give richer signal but require a longer prompt and more rubric design effort.
  • K (number of samples): How many independent judge outputs to average per example. Higher values reduce variance at a linear cost increase. Values of 3 to 5 work well in practice.
  • Temperature: Controls output stochasticity. Temperature 0 gives deterministic scores; temperature 0.7 gives more fine-grained reasoning but higher variance across runs.
  • Calibration set size: The number of human-annotated examples used to compute correlation metrics. Aim for 200 to 2000 examples; fewer examples make correlation estimates unreliable.
  • Score range: The numeric scale for pointwise ratings (e.g., 1-5 or 1-10). Narrower scales (1-5) are easier for judges to apply consistently; wider scales allow finer discrimination.