Instruction Following Evaluation › Automatic Evaluation

Michael BrenndoerferDecember 20, 202516 min read
Language AI HandbookAll chapters
Part 36 of 67Instruction Tuning
Chapter 271Instruction Following Evaluation › Automatic Evaluation
Reading section 4 of 9 · Automatic Evaluation · 16 minSection 4 of 9 · 16 min

Book contents

Section 4 of 9 · 16 min read

Instruction Following Evaluation › Automatic EvaluationOne section · Sections in this chapter4 / 9 ↓
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

Automatic evaluation methods let rapid iteration during model development. The core appeal is that automatic evaluation removes the human annotation bottleneck and gives fast, reproducible measurements that teams can run continuously as they iterate on model training. The most successful approach treats evaluation itself as a language modeling task, using powerful LLMs to judge response quality. This shows a broader trend in NLP where models become infrastructure for building and evaluating other models.

Think of automatic evaluation as hiring a very experienced graduate student to assess responses. They read extensively, have strong opinions, and can evaluate quickly. But they have their own stylistic preferences, their own blind spots, and they may unconsciously favor responses that sound like their own writing. The key is using their judgments wisely: trusting them on clear-cut cases while being skeptical of edge cases and checking their work against external ground truth when stakes are high.

The history of automatic evaluation in NLP shows a recurring pattern: a new metric is proposed, it correlates well with human judgments initially, researchers optimize against it, and over time the metric becomes less predictive as models game it. BLEU scores in machine translation went through this cycle, and LLM-as-judge approaches are beginning to show similar dynamics. Understanding this pattern helps you use automatic metrics appropriately: as rapid indicators of direction rather than ground truth measures of quality.

LLM-as-a-Judge

The LLM-as-a-Judge paradigm uses a capable language model (typically GPT-4 or a similar frontier model) to evaluate responses. This approach assumes that if a model can generate high-quality responses, it can also recognize quality in responses from other models. This mirrors how human expertise works: skilled writers can identify good writing, experienced programmers can spot elegant code, and domain experts can assess the accuracy of technical explanations:

In[17]:
Code
def create_judge_prompt(instruction, response_a, response_b):
    """
    Create a prompt for LLM-as-judge pairwise comparison.
    """
    prompt = f"""You are an impartial judge evaluating the quality of two AI assistant responses.

[User Instruction]
{instruction}

[Response A]
{response_a}

[Response B]
{response_b}

[Task]
Compare the two responses based on:
1. Helpfulness: Does it address the user's request?
2. Accuracy: Is the information correct?
3. Clarity: Is it well-written and easy to understand?
4. Relevance: Does it stay on topic?

Provide your evaluation in the following format:
Analysis: <brief analysis of each response>
Winner: <A, B, or Tie>
Confidence: <high, medium, low>"""

    return prompt


# Example usage
instruction = "What are three benefits of regular exercise?"
response_a = """Regular exercise offers numerous benefits:
1. Improved cardiovascular health - strengthens heart and lungs
2. Better mental health - reduces anxiety and depression
3. Weight management - helps maintain healthy body weight"""

response_b = """Exercise is good for you. It helps your heart and makes you feel better. You should try to exercise every day if you can."""

judge_prompt = create_judge_prompt(instruction, response_a, response_b)
Out[18]:
Console
Judge Prompt Structure:
============================================================
You are an impartial judge evaluating the quality of two AI assistant responses.

[User Instruction]
What are three benefits of regular exercise?

[Response A]
Regular exercise offers numerous benefits:
1. Improved cardiovascular health - strengthens heart and lungs
2. Better mental health - reduces anxiety and depression
3. Weight management - helps maintain healthy body weight

[Response B]
Exercise is good for you. It helps your heart and makes you feel better. You should try to exercise ever...

The judge model would analyze both responses and select a winner.

The structured prompt forces the judge to break down the evaluation into specific criteria before declaring a winner, which improves consistency compared to asking for a simple score. This structure serves multiple purposes: it guides the judge toward complete evaluation rather than snap judgments, it gives interpretable reasoning that can be audited for systematic errors, and it aligns the evaluation process with the multiple dimensions of response quality. Requiring the judge to give an Analysis before the Winner also encourages chain-of-thought reasoning, which tends to produce more reliable final judgments.

Research shows that GPT-4 as a judge reaches 80%+ agreement with human preferences on many instruction-following tasks. However, this agreement rate varies materially by task category: judges are more reliable for factual questions with clear correct answers than for stylistic judgments in creative writing. You should always validate judge agreement rates on your specific task distribution before trusting automatic evaluation results at scale.

Position Bias and Mitigation

LLM judges exhibit position bias: they tend to favor the response presented first (or sometimes second, depending on the model). This bias likely emerges from patterns in the training data, where examples presented earlier in a list or conversation may have been systematically different from later examples. Regardless of its origin, position bias is a significant confound that can distort evaluation results. We can mitigate this by evaluating each pair twice with swapped positions:

In[19]:
Code
def position_debiased_evaluation(
    instruction, response_a, response_b, judge_function
):
    """
    Evaluate with position swapping to reduce position bias.

    judge_function: callable that returns 'A', 'B', or 'Tie'
    """
    # First evaluation: A first, B second
    result_ab = judge_function(instruction, response_a, response_b)

    # Second evaluation: B first, A second
    result_ba = judge_function(instruction, response_b, response_a)
    # Flip the result to maintain consistent meaning
    result_ba_flipped = {"A": "B", "B": "A", "Tie": "Tie"}[result_ba]

    # Aggregate results
    if result_ab == result_ba_flipped:
        return result_ab, "consistent"
    else:
        return "Tie", "inconsistent"


# Simulating evaluation results
evaluations = [
    {
        "instruction": "Explain photosynthesis",
        "ab": "A",
        "ba": "A",
    },  # Consistent
    {"instruction": "Write a haiku", "ab": "A", "ba": "B"},  # Inconsistent
    {"instruction": "List prime numbers", "ab": "B", "ba": "B"},  # Consistent
    {"instruction": "Summarize WWII", "ab": "A", "ba": "A"},  # Consistent
]

# Process results
processed_evaluations = []
for eval_item in evaluations:
    ab_result = eval_item["ab"]
    ba_result = eval_item["ba"]
    ba_flipped = {"A": "B", "B": "A", "Tie": "Tie"}[ba_result]

    if ab_result == ba_flipped:
        final = ab_result
        status = "Consistent"
    else:
        final = "Tie"
        status = "Position bias detected"

    processed_evaluations.append(
        {
            "instruction": eval_item["instruction"],
            "ab": ab_result,
            "ba": ba_result,
            "final": final,
            "status": status,
        }
    )
Out[20]:
Console
Position-Debiased Evaluation Results:

Instruction: Explain photosynthesis
  A-first: A, B-first: A -> Final: Tie (Position bias detected)

Instruction: Write a haiku
  A-first: A, B-first: B -> Final: A (Consistent)

Instruction: List prime numbers
  A-first: B, B-first: B -> Final: Tie (Position bias detected)

Instruction: Summarize WWII
  A-first: A, B-first: A -> Final: Tie (Position bias detected)
Out[21]:
Visualization
Bar chart showing LLM judge preferences by position without swapping: first position 58%, second 32%, ties 10%.
LLM judge preferences without position swapping. The first position is favored approximately 58% of the time, revealing a systematic bias that would distort model comparisons if left uncorrected. This bias is large enough to change the apparent winner in closely contested comparisons.
Out[22]:
Visualization
Bar chart showing 72% consistent judgments versus 28% inconsistent judgments after applying position swapping.
Consistency rates after applying position swapping. By evaluating each pair twice with reversed order, we can identify judgments that are unreliable due to position effects. The 72% consistent judgment rate is the fraction of comparisons where the judge reached the same conclusion regardless of ordering.

When results conflict between position orderings, declaring a tie is conservative but honest. The inconsistency itself reveals uncertainty in the evaluation. This approach doubles the computational cost of evaluation but gives an important quality check. The rate of inconsistent judgments is also diagnostic: if position swapping frequently changes the outcome, it suggests either that the responses are similar in quality or that the judge model is unreliable for this type of instruction. High inconsistency rates on specific instruction categories are a signal to invest more heavily in human evaluation for those categories.

Verbosity Bias

LLM judges also exhibit verbosity bias, preferring longer responses even when they contain unnecessary repetition or padding. This bias shows a tendency to conflate quantity with quality, a pattern that likely exists in both human preferences and training data. Understanding this bias is important because it can systematically favor models that generate verbose outputs over those that give concise, direct answers:

In[23]:
Code
# Demonstrating verbosity bias
concise_response = "The capital of France is Paris."

verbose_response = """That's a great question! The capital of France is Paris,
which is located in the northern part of the country. Paris has been the capital
for many centuries and is known for landmarks like the Eiffel Tower, the Louvre
Museum, and Notre-Dame Cathedral. It's also called the "City of Light" and is
one of the most visited cities in the world. So to directly answer your question,
the capital of France is Paris."""

# Calculate response lengths
concise_words = len(concise_response.split())
verbose_words = len(verbose_response.split())
Out[24]:
Console
Verbosity Bias Example

Instruction: What is the capital of France?

CONCISE (6 words):
  The capital of France is Paris.

VERBOSE (73 words):
  That's a great question! The capital of France is Paris,
which is located in the northern part of the country. Paris has been the capital
for many centuries and is known for landmarks like the Eiffel Tower, the Louvre
Museum, and Notre-Dame Cathedral. It's also called the "City of Light" and is
one of the most visited cities in the world. So to directly answer your question,
the capital of France is Paris.
Out[25]:
Visualization
Bar chart comparing word counts of concise and verbose responses to a factual question about France's capital.
Word count comparison showing the large difference between concise and verbose responses to the same factual question. Both responses contain the same core information, but the verbose version uses far more words.
Out[26]:
Visualization
Grouped bar chart showing preference rates for concise versus verbose responses across three evaluator types, all showing systematic verbosity preference.
Preference rates for concise versus verbose responses across human evaluators and two LLM judges. All three evaluator types show a systematic preference for verbose responses despite identical factual content, suggesting this length bias is a basic feature of how both humans and LLMs assess response quality.

Both responses are factually correct, but LLM judges (and humans) often prefer the verbose version despite the concise one being more direct. This "length bias" mistakes verbosity for quality. The verbose response includes tangentially related information that, while accurate, does not address the question more effectively. In fact, if a user simply needed a quick factual answer, the verbose response wastes their time and may obscure the key information they sought.

To mitigate verbosity bias, some evaluation protocols explicitly instruct the judge to prefer concise responses when both are equally correct. Others use length-controlled comparisons or normalize scores by response length. A more advanced approach involves asking judges to evaluate whether each piece of information in a response directly contributes to answering the question, penalizing padding and tangents explicitly. This more surgical evaluation is slower but produces results that better predict user satisfaction on factual tasks.

Win Rate Calculation

After collecting pairwise judgments, we calculate win rates to compare models. Win rates summarize performance by showing the fraction of comparisons a model wins against its opponents. This metric directly answers the question: how often would a user prefer this model's output over alternatives?

In[27]:
Code
import numpy as np


def calculate_win_rates(results, model_names):
    """
    Calculate win rates from pairwise comparison results.

    results: list of dicts with 'model_a', 'model_b', 'winner'
    """
    wins = {model: 0 for model in model_names}
    losses = {model: 0 for model in model_names}
    ties = {model: 0 for model in model_names}

    for result in results:
        model_a = result["model_a"]
        model_b = result["model_b"]
        winner = result["winner"]

        if winner == "A":
            wins[model_a] += 1
            losses[model_b] += 1
        elif winner == "B":
            wins[model_b] += 1
            losses[model_a] += 1
        else:  # Tie
            ties[model_a] += 1
            ties[model_b] += 1

    win_rates = {}
    for model in model_names:
        total = wins[model] + losses[model] + ties[model]
        if total > 0:
            non_tie_total = wins[model] + losses[model]
            if non_tie_total > 0:
                win_rates[model] = wins[model] / non_tie_total
            else:
                win_rates[model] = 0.5  # All ties
        else:
            win_rates[model] = None

    return wins, losses, ties, win_rates


# Simulated comparison results
np.random.seed(42)
models = ["GPT-4", "Claude", "LLaMA-2-70B", "Mistral-7B"]
results = []

comparison_probs = {
    ("GPT-4", "Claude"): (0.45, 0.40, 0.15),
    ("GPT-4", "LLaMA-2-70B"): (0.55, 0.30, 0.15),
    ("GPT-4", "Mistral-7B"): (0.60, 0.25, 0.15),
    ("Claude", "LLaMA-2-70B"): (0.50, 0.35, 0.15),
    ("Claude", "Mistral-7B"): (0.55, 0.30, 0.15),
    ("LLaMA-2-70B", "Mistral-7B"): (0.45, 0.40, 0.15),
}

for (model_a, model_b), (p_a, p_b, p_tie) in comparison_probs.items():
    for _ in range(100):  # 100 comparisons per pair
        r = np.random.random()
        if r < p_a:
            winner = "A"
        elif r < p_a + p_b:
            winner = "B"
        else:
            winner = "Tie"
        results.append(
            {"model_a": model_a, "model_b": model_b, "winner": winner}
        )

wins, losses, ties, win_rates = calculate_win_rates(results, models)

sorted_models = sorted(models, key=lambda m: win_rates.get(m, 0), reverse=True)
Out[28]:
Console
Model Comparison Results (simulated)

Model                  Wins   Losses   Ties   Win Rate
----------------------------------------------------
GPT-4                   161       91     48      63.9%
Claude                  139      113     48      55.2%
LLaMA-2-70B             104      146     50      41.6%
Mistral-7B               94      148     58      38.8%
Out[29]:
Visualization
Horizontal bar chart ranking four models by win rate with 95% confidence interval error bars and a dashed 50% baseline.
Model win rates from simulated pairwise comparisons, with GPT-4 achieving the highest performance. Win rate excludes ties and is the fraction of contested comparisons won. Error bars show 95% confidence intervals computed via bootstrap resampling, showing the uncertainty in these estimates from a finite number of comparisons.

The results show a clear ranking based on head-to-head performance. Win rates give a simple summary, but they do not account for which opponents each model faced. A model that only competed against weak opponents would have an inflated win rate compared to one that faced stronger competition. More advanced ranking systems like Elo or Bradley-Terry give better rankings when not all pairs are equally compared. These systems model each comparison as evidence about underlying model strength, accounting for the difficulty of each opponent faced. We will explore these methods in detail in the upcoming chapter on preference modeling.

The confidence intervals on win rates are important to interpret correctly. With 100 comparisons per model pair, the error bars are relatively wide. Differences that look real in bar charts may not be statistically significant. Always check whether the confidence intervals of two models overlap before concluding that one is better than the other.

Reference-Based Metrics

For some instruction types, reference-based metrics remain useful. Code generation tasks can be evaluated by running tests against a reference implementation. This is a fundamentally different evaluation paradigm: rather than asking whether a response seems good, we verify whether it works. This functional evaluation gives an objective ground truth that neither human evaluators nor LLM judges can reach for tasks with verifiable outputs:

In[30]:
Code
def evaluate_code_response(generated_code, test_cases):
    """
    Evaluate generated code by running test cases.
    Returns pass rate and detailed results.
    """
    results = []

    for test in test_cases:
        try:
            namespace = {}
            exec(generated_code, namespace)

            function_name = test["function"]
            inputs = test["inputs"]
            expected = test["expected"]

            if function_name in namespace:
                actual = namespace[function_name](*inputs)
                passed = actual == expected
            else:
                passed = False
                actual = "Function not found"

        except Exception as e:
            passed = False
            actual = str(e)

        results.append(
            {
                "inputs": inputs,
                "expected": expected,
                "actual": actual,
                "passed": passed,
            }
        )

    pass_rate = sum(r["passed"] for r in results) / len(results)
    return pass_rate, results


# Example: Testing a Fibonacci implementation
generated_code = """
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)
"""

test_cases = [
    {"function": "fibonacci", "inputs": (0,), "expected": 0},
    {"function": "fibonacci", "inputs": (1,), "expected": 1},
    {"function": "fibonacci", "inputs": (5,), "expected": 5},
    {"function": "fibonacci", "inputs": (10,), "expected": 55},
]

pass_rate, test_results = evaluate_code_response(generated_code, test_cases)
Out[31]:
Console
Code Evaluation Results:

Generated code:

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)


Test Results:
  Test 1: fibonacci(0,) = 0 (expected 0) PASS
  Test 2: fibonacci(1,) = 1 (expected 1) PASS
  Test 3: fibonacci(5,) = 5 (expected 5) PASS
  Test 4: fibonacci(10,) = 55 (expected 55) PASS

Pass Rate: 100%

With a 100% pass rate, we can confirm the model's solution is functionally correct. For tasks with verifiable outputs (math problems, factual questions, code), combining functional tests with qualitative LLM-as-judge evaluation gives complete assessment. The functional tests verify correctness, while qualitative evaluation can assess code readability and efficiency as well as adherence to best practices that pass/fail tests alone cannot capture.