← Back to DevBytes

LLM-as-a-Judge: How to Evaluate Models Automatically

Introduction to LLM-as-a-Judge

As large language models (LLMs) become embedded in more products and workflows, the question of how do we know they are performing well? grows harder to answer. Traditional evaluation relies on static benchmarks like MMLU or HumanEval, but these datasets leak into training corpora, become saturated quickly, and rarely reflect the messy reality of production prompts. LLM-as-a-Judge offers a pragmatic alternative: use a capable language model to grade the outputs of other models automatically, at scale, and on your own data.

In this tutorial, you will learn what LLM-as-a-Judge is, why it matters, how to implement it from scratch, and the best practices that separate a useful evaluator from a noisy one. By the end, you will have a working evaluation harness you can drop into your own projects.

What Is LLM-as-a-Judge?

LLM-as-a-Judge is an evaluation paradigm where one language model — the judge — scores, ranks, or critiques the responses produced by another model — the candidate. Instead of hand-labeling thousands of examples, you define a rubric in natural language and let the judge apply it consistently across a dataset.

Common judging patterns include:

The approach was popularized by work such as Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena (Zheng et al., 2023), which showed that strong judges like GPT-4 agree with human raters around 80% of the time on pairwise tasks — comparable to inter-annotator agreement between humans themselves.

Why It Matters

Manual evaluation is slow, expensive, and hard to reproduce. A single prompt change might require re-labeling hundreds of examples. LLM-as-a-Judge addresses several pain points:

That said, LLM-as-a-Judge is not a silver bullet. Judges exhibit known biases — preferring verbose answers, favoring their own outputs, and struggling with math or factual verification. Understanding these limitations is essential, and we cover them in the best practices section.

How to Use It: A Practical Implementation

Let us build a minimal but production-ready evaluation harness in Python. We will use the OpenAI client, but the same pattern works with Anthropic, Mistral, or any OpenAI-compatible endpoint. The harness will support both single-answer grading and pairwise comparison.

1. Installing Dependencies

pip install openai pydantic tqdm

2. Defining the Judge Prompt

The rubric is the heart of any judge. A vague rubric produces vague scores. Be explicit about what each score level means, what to reward, and what to penalize.

SINGLE_ANSWER_RUBRIC = """You are an expert evaluator grading the quality of an AI assistant's response.

You will be given:
- A user question
- The assistant's response

Grade the response on a scale of 1 to 5 using the following rubric:

1 - Poor: Irrelevant, incorrect, or unhelpful. Major errors or hallucinations.
2 - Fair: Partially relevant but contains notable flaws, omissions, or inaccuracies.
3 - Good: Mostly correct and helpful, but could be clearer, more complete, or better structured.
4 - Very Good: Correct, helpful, and well-structured with only minor issues.
5 - Excellent: Accurate, complete, well-structured, and insightful. Exemplary response.

Respond with ONLY a JSON object in this exact format:
{"score": <integer 1-5>, "rationale": "<one sentence explanation>"}
"""

3. Building the Judge Client

import json
import os
from openai import OpenAI
from pydantic import BaseModel

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

class Judgment(BaseModel):
    score: int
    rationale: str

def judge_single_answer(
    question: str,
    response: str,
    model: str = "gpt-4o",
    reference: str | None = None,
) -> Judgment:
    user_content = f"Question: {question}\n\nResponse: {response}"
    if reference:
        user_content += f"\n\nReference answer: {reference}"

    completion = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": SINGLE_ANSWER_RUBRIC},
            {"role": "user", "content": user_content},
        ],
        temperature=0.0,
        response_format={"type": "json_object"},
    )

    payload = json.loads(completion.choices[0].message.content)
    return Judgment(**payload)

Setting temperature=0.0 reduces randomness so the same input yields consistent scores. Using response_format={"type": "json_object"} forces structured output, which makes parsing reliable.

4. Pairwise Comparison

Pairwise comparison is often more reliable than absolute scoring because judges are better at relative judgments than at calibrating a 1–5 scale.

PAIRWISE_RUBRIC = """You are an expert evaluator comparing two AI assistant responses to the same question.

Compare Response A and Response B. Decide which is better based on correctness,
completeness, clarity, and helpfulness. If they are roughly equal, call it a tie.

Respond with ONLY a JSON object:
{"winner": "A" | "B" | "tie", "rationale": "<one sentence explanation>"}
"""

def judge_pairwise(
    question: str,
    response_a: str,
    response_b: str,
    model: str = "gpt-4o",
) -> dict:
    user_content = (
        f"Question: {question}\n\n"
        f"Response A:\n{response_a}\n\n"
        f"Response B:\n{response_b}"
    )

    completion = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": PAIRWISE_RUBRIC},
            {"role": "user", "content": user_content},
        ],
        temperature=0.0,
        response_format={"type": "json_object"},
    )

    return json.loads(completion.choices[0].message.content)

5. Running an Evaluation Over a Dataset

Now we tie everything together. Suppose you have a dataset of questions and two candidate models you want to compare.

from tqdm import tqdm

dataset = [
    {
        "question": "Explain the difference between TCP and UDP.",
        "reference": "TCP is connection-oriented and reliable; UDP is connectionless and faster but unreliable.",
    },
    {
        "question": "What is the time complexity of binary search?",
        "reference": "O(log n) for a sorted array of n elements.",
    },
    # ... more examples
]

def get_candidate_response(question: str, model: str) -> str:
    completion = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": question}],
        temperature=0.7,
    )
    return completion.choices[0].message.content

results = []
for item in tqdm(dataset, desc="Evaluating"):
    q = item["question"]
    ref = item["reference"]

    resp_a = get_candidate_response(q, model="gpt-4o-mini")
    resp_b = get_candidate_response(q, model="gpt-3.5-turbo")

    pairwise = judge_pairwise(q, resp_a, resp_b)
    score_a = judge_single_answer(q, resp_a, reference=ref)
    score_b = judge_single_answer(q, resp_b, reference=ref)

    results.append({
        "question": q,
        "score_a": score_a.score,
        "score_b": score_b.score,
        "pairwise_winner": pairwise["winner"],
        "rationale": pairwise["rationale"],
    })

avg_a = sum(r["score_a"] for r in results) / len(results)
avg_b = sum(r["score_b"] for r in results) / len(results)
wins_a = sum(1 for r in results if r["pairwise_winner"] == "A")
wins_b = sum(1 for r in results if r["pairwise_winner"] == "B")
ties = sum(1 for r in results if r["pairwise_winner"] == "tie")

print(f"Model A avg score: {avg_a:.2f}")
print(f"Model B avg score: {avg_b:.2f}")
print(f"Pairwise - A wins: {wins_a}, B wins: {wins_b}, ties: {ties}")

6. Handling Position Bias in Pairwise Judging

Pairwise judges suffer from position bias: they tend to prefer whichever response appears first. A simple and effective mitigation is to run each comparison twice with the order swapped, and only declare a winner if both runs agree.

def judge_pairwise_debiased(question, response_a, response_b, model="gpt-4o"):
    run1 = judge_pairwise(question, response_a, response_b, model)
    run2 = judge_pairwise(question, response_b, response_a, model)

    winner1 = run1["winner"]
    # Invert the second run because we swapped A and B
    winner2 = {"A": "B", "B": "A", "tie": "tie"}[run2["winner"]]

    if winner1 == winner2:
        return {"winner": winner1, "rationale": run1["rationale"]}
    return {"winner": "tie", "rationale": "Order-swapped runs disagreed; calling it a tie."}

Best Practices

Choose the Right Judge Model

Your judge should be at least as capable as the models it evaluates. A common rule of thumb: use the strongest available model (for example, GPT-4o or Claude 3.5 Sonnet) as the judge, even if you are evaluating smaller or cheaper models. A weak judge cannot reliably grade a strong candidate.

Write Precise Rubrics

Ambiguity is the enemy of consistency. Define each score level concretely. If correctness matters more than tone, say so. If certain failure modes (hallucination, unsafe content) should cap the score at 1 regardless of other qualities, state that explicitly. Iterate on the rubric by inspecting disagreements between the judge and your own spot-checks.

Use Few-Shot Examples

Adding two or three labeled examples inside the system prompt dramatically improves calibration. Show the judge a question, a response, the expected score, and a short rationale. This anchors the judge to your specific quality bar.

FEW_SHOT_EXAMPLES = """
Example 1:
Question: What is the capital of France?
Response: The capital of France is Paris.
{"score": 5, "rationale": "Correct, concise, and directly answers the question."}

Example 2:
Question: What is the capital of France?
Response: I think it might be Lyon, but I'm not entirely sure.
{"score": 1, "rationale": "Factually incorrect; the capital is Paris, not Lyon."}
"""

Guard Against Known Biases

Validate Against Human Labels

Before trusting a judge at scale, label 50–100 examples yourself and measure agreement. Compute Cohen's kappa or simple agreement rate. If agreement is below 70%, refine the rubric or switch judge models. Treat the human-labeled subset as your ongoing calibration set.

Log Everything

Store the full prompt, the judge's raw output, the parsed score, and the rationale for every judgment. When scores look suspicious, you need to be able to inspect the exact input and output. Structured logging also lets you re-run evaluations after rubric changes and compare deltas.

Use Reference Answers When Available

For factual or retrieval tasks, providing a gold reference answer dramatically improves judge accuracy. The judge shifts from subjective grading to verification, which is a much easier task. Even a rough reference helps the judge catch hallucinations it would otherwise miss.

Consider Cost and Latency

Judging every production response in real time is expensive. A common pattern is to judge a random sample (say 5–10%) of traffic continuously, and run full evaluations on every prompt or model change. Batch APIs can cut judging costs by 50% or more for offline evaluation runs.

Conclusion

LLM-as-a-Judge has become a foundational tool for modern AI development because it makes rigorous, rubric-based evaluation feasible at the speed and scale that iterative development demands. By combining a strong judge model, a precise rubric, structured output, and bias mitigation techniques like order swapping, you can build an evaluation pipeline that tracks closely with human judgment at a fraction of the cost. The key is to treat the judge itself as a system under test: validate it against human labels, log its decisions, refine the rubric continuously, and never blindly trust a single score. Done well, LLM-as-a-Judge becomes the feedback loop that lets you ship model and prompt improvements with confidence.

— Ad —

Google AdSense will appear here after approval

← Back to all articles