TAI BUI
Back to curriculum
BuildPython

Evaluation: Benchmarks, Evals, LM Harness

Goodhart's Law: when a measure becomes a target, it ceases to be a good measure. Every frontier lab tunes for benchmarks. MMLU scores go up while models still miscount the R's in "strawberry." The eval you can trust most is your own, on your task, with your data.

TIME: ~90 minutesPREREQUISITES: Phase 10, Lessons 01-05 (LLMs from Scratch)

Learning Objectives

  • Build a custom evaluation harness that runs multiple-choice and open-ended benchmarks against a language model
  • Explain why standard benchmarks (MMLU, HumanEval) saturate and fail to differentiate frontier models
  • Implement task-specific evals with proper metrics: exact match, F1, BLEU, and LLM-as-judge scoring
  • Design a custom evaluation suite targeting your specific use case rather than relying solely on public leaderboards

The Problem

MMLU was published in 2020 with 15,908 questions across 57 subjects. Within three years, frontier models saturated it. GPT-4 scored 86.4%. Claude 3 Opus scored 86.8%. Llama 3 405B scored 88.6%. The leaderboard compressed into a 3-point range where differences are statistical noise, not real capability gaps.

Meanwhile, those same models fail at tasks that a 10-year-old handles without thinking. Claude 3.5 Sonnet, scoring 88.7% on MMLU, initially could not count the letters in "strawberry". The task needs no world knowledge and no reasoning, just character-level iteration. HumanEval tests code generation with 164 problems. Models score 90%+ on it while still producing code that crashes on edge cases a junior developer would catch.

The gap between benchmark performance and real-world reliability is the central problem of LLM evaluation. Benchmarks tell you how a model performs on the benchmark. They say very little about how that model will perform on your specific task, with your specific data, under your specific failure modes. If you are building a customer support bot, MMLU is irrelevant. If you are building a code assistant, HumanEval only covers function-level generation. It says nothing about debugging, refactoring, or explaining code across files.

You need custom evals. Benchmarks are still useful for rough model selection, but the final evaluation has to match your deployment conditions.

Warm-up0/3
1. During pretraining, a model's loss falls from 3.0 to 2.0. What does that tell you about its predictions?Warm-up
A model's pretraining loss shown dropping from 3.0 to 2.0 between two points in training, with the meaning of that drop left blank.
2. Why do pretraining pipelines hold out a validation split that the model never trains on?Warm-up
A training-text box and a separate held-out-text box that the model never trains on, with a question mark for why pretraining keeps them apart.
3. A spam filter flags 10 emails and 8 of them really are spam. What is its precision?Warm-up
Ten flagged emails in a row, eight marked as really spam and two marked as not spam, with a blank for precision as the share of flagged emails that are really spam.
Check: The Problem0/4
1. What is MMLU?Define
One illustrative multiple-choice knowledge question with four lettered options, presented as one item from a larger academic benchmark.
Illustrative question, not drawn from the real benchmark's item bank.
2. What does the HumanEval benchmark test?Define
A single Python function signature and docstring with the body left for the model to complete, presented as one task from a larger suite.
Illustrative function-completion task in the suite's style.
3. Why can a model score 88% on MMLU and still be a poor fit for your support bot?Explain
A model scoring 88 percent on MMLU next to a customer-support deployment with real tickets and unknown failure modes, with whether that score predicts performance there left blank.
4. A new model scores 89% on MMLU and last year's scored 87%, with every frontier model between 85% and 90%. Which reading is most defensible?Analyze
A new model's 89% MMLU score and last year's 87% score, both inside a shaded 85 to 90 percent band where every frontier model currently lands, with how to read the 2-point gap left blank.

The Concept

The Eval Landscape

There are three categories of evaluation, each with different cost and signal quality.

Benchmarks are standardized test suites. MMLU, HumanEval, SWE-bench, MATH, ARC, HellaSwag. You run a model against the benchmark and get a score. The advantage: everyone uses the same test, so you can compare models. The disadvantage: models and training data increasingly contaminate these benchmarks. Labs train on data that includes benchmark questions. Scores go up. Capability may not.

Custom evals are test suites you build for your specific use case. You define the inputs, the expected outputs, and the scoring function. A legal document summarizer gets evaluated on legal documents. A SQL generator gets evaluated on your database schema. These are expensive to create but they are the only evaluation that predicts production performance.

Human evals use paid annotators to judge model outputs on criteria like helpfulness, correctness, fluency, and safety. The gold standard for open-ended tasks where automated scoring fails. Chatbot Arena has collected over 2 million human preference votes across 100+ models. The downside: cost ($0.10-$2.00 per judgment) and speed (hours to days).

Mermaid diagram

Why Benchmarks Break

Three mechanisms cause benchmark scores to stop reflecting real capability.

Data contamination. Training corpora scrape the internet. Benchmark questions live on the internet. Models see the answers during training. Labs do not include benchmark data on purpose, but web-scale scraping makes it nearly impossible to keep out.

Teaching to the test. Labs optimize training mixtures for benchmark performance. If 5% of the training mix is MMLU-style multiple choice, the model learns the format and the answer distribution. MMLU is 4-way multiple choice. Models learn that the answer distribution is approximately uniform across A/B/C/D, which helps even when the model does not know the answer.

Saturation. When every frontier model scores 85-90% on a benchmark, the benchmark stops discriminating. The remaining 10-15% of questions may be ambiguous, mislabeled, or require obscure domain knowledge. Improving from 87% to 89% on MMLU may mean the model memorized two more obscure questions, not that it got smarter.

Perplexity: A Quick Health Check

Perplexity measures how surprised a model is by a sequence of tokens. Formally, it is the exponentiated average negative log-likelihood:

PPL = exp(-1/N * sum(log P(token_i | context)))

A perplexity of 10 means the model is, on average, as uncertain as choosing uniformly among 10 options at each token position. Lower is better. GPT-2 gets a perplexity of ~30 on WikiText-103. GPT-3 gets ~20. Llama 3 8B gets ~7.

Perplexity is useful for comparing models on the same test set, but it has blind spots. A model can have low perplexity by being good at predicting common patterns while being terrible at rare but important patterns. It also says nothing about instruction following, reasoning, or factual accuracy. Use it as a sanity check, not a final verdict.

LLM-as-Judge

Use a strong model to evaluate a weaker model's output. The idea is simple: ask GPT-4o or Claude Sonnet to rate a response on a 1-5 scale for correctness, helpfulness, and safety. This costs about $0.01 per judgment with GPT-4o-mini and correlates surprisingly well with human judgments, at around 80% agreement on most tasks.

The scoring prompt matters more than the model. A vague prompt ("Rate this response") produces noisy scores. A structured prompt with a rubric ("Score 5 if the answer is factually correct and cites a source, 4 if correct but unsourced, 3 if partially correct...") produces consistent, reproducible scores.

Failure modes: judge models exhibit position bias (prefer the first response in pairwise comparisons), verbosity bias (prefer longer responses), and self-preference (GPT-4 rates GPT-4 outputs higher than equivalent Claude outputs). Mitigations: randomize order, normalize for length, use a different judge than the model being evaluated.

ELO Ratings from Pairwise Comparisons

Chatbot Arena's approach. Show two responses to the same prompt from different models. A human (or LLM judge) picks the better one. From thousands of these comparisons, compute an ELO rating for each model, the same system used in chess.

ELO advantages: relative ranking is more reliable than absolute scoring, handles ties gracefully, and converges with fewer comparisons than scoring every output independently. As of early 2026, Chatbot Arena ranks show GPT-4o, Claude 3.5 Sonnet, and Gemini 1.5 Pro within 20 ELO points of each other at the top.

Mermaid diagram

Eval Frameworks

lm-evaluation-harness (EleutherAI): the standard open-source eval framework. Supports 200+ benchmarks. Run any Hugging Face model against MMLU, HellaSwag, ARC, etc. with one command. Used by the Open LLM Leaderboard.

RAGAS: evaluation framework specifically for RAG pipelines. Measures faithfulness (does the answer match the retrieved context?), relevance (is the retrieved context relevant to the question?), and answer correctness.

promptfoo: config-driven eval for prompt engineering. Define test cases in YAML, run against multiple models, get a pass/fail report. Useful for regression testing prompts, so a prompt change does not break existing test cases.

Building Custom Evals

This is the eval that predicts production behavior. The process:

  1. Define the task. What exactly should the model do? Be precise. "Answer questions" is too vague. "Given a customer complaint email, extract the product name, issue category, and sentiment" is a task you can evaluate.

  2. Create test cases. Minimum 50 for a prototype eval, 200+ for production. Each test case is an (input, expected_output) pair. Include edge cases: empty inputs, adversarial inputs, ambiguous inputs, inputs in other languages.

  3. Define scoring. Exact match for structured outputs. BLEU/ROUGE for text similarity. LLM-as-judge for open-ended quality. F1 for extraction tasks. Combine multiple metrics with weights.

  4. Automate. Every eval runs with one command. No manual steps. Store results in a format that enables comparison over time.

  5. Track over time. An eval score is meaningless in isolation. You need the trendline. Did the score improve after the last prompt change? Did it regress after switching models? Version your eval alongside your prompts.

Eval TypeCost per judgmentAgreement with humansBest for
Exact match~$0100% (when applicable)Structured output, classification
BLEU/ROUGE~$0~60%Translation, summarization
LLM-as-judge~$0.01~80%Open-ended generation
Human eval$0.10-$2.00N/A (is the ground truth)Ambiguous, high-stakes tasks
Check: The Concept0/21
1. What is SWE-bench?Define
One GitHub-style bug report naming a real open-source repository and requiring the fix to pass that project's own tests, presented as one issue from a larger set.
Illustrative issue in the suite's style.
2. What does a perplexity of 10 mean for a language model?Define
The perplexity formula alongside a reported perplexity of 10, with the meaning of that number for the model's per-token uncertainty left blank.
3. What does an ELO rating express?Define
Model A beating Model B in a head-to-head match, with each model's resulting rating number left unspecified.
4. What is LLM-as-judge?Define
A structured scoring rubric with graded criteria, next to one model output, with who or what applies the rubric left blank.
5. A model scores 88% on MMLU. Which situation best matches data contamination?Define
An observed 88 percent MMLU score, with a blank for which situation best explains it.
6. What is Chatbot Arena?Define
A prompt with two anonymous responses, A and B, and a person choosing the better one, with what happens to that vote left blank.
7. What separates a custom eval from a benchmark?Define
A generic benchmark box labelled the same test for every lab, next to a custom eval box built on legal documents for one summarizer, with what makes the second one different left blank.
8. Where does LLM-as-judge sit on the cost and quality trade-off?Explain
The eval-type cost and agreement table from the lesson with every row filled in except LLM-as-judge, whose cost and agreement cells are left as question marks.
9. Why does a benchmark like MMLU stop separating frontier models?Explain
Three frontier models' real MMLU scores, 86.4, 86.8 and 88.6 percent, clustered tightly together, with why the leaderboard can no longer separate them left blank.
10. Why is contamination hard to prevent even when labs avoid using benchmark data on purpose?Explain
A flow from public internet to web-scale scrape to training corpus, with a note that benchmark questions live on the internet and labs do not include them on purpose.
11. Why does a rubric prompt give steadier judge scores than 'Rate this response'?Explain
A vague prompt reading rate this response next to a structured rubric prompt with graded criteria, with why the rubric gives steadier scores left blank.
12. How does using a different judge than the model under test help?Explain
The same model used both to generate a response and to judge that response, with what risk this creates left blank.
13. Which sequence matches the five steps for building a custom eval?Explain
Five boxes in a row, naming the steps define task, create cases, define scoring, automate, and track over time, with no arrows reordered and nothing else marked.
The five steps are the lesson's own list; this restates their order without testing recall of it visually.
14. Your pairwise LLM judge picks whichever response is shown first in about 70% of comparisons, whatever the models are. Which fix targets this?Analyze
A bar chart showing a pairwise judge picks the response shown first 70% of the time and the one shown second 30% of the time, regardless of which model produced it, with which fix targets this left blank.
15. A team uses GPT-4 as the only judge and finds GPT-4 outputs beat equivalent Claude outputs. Which failure and fix fit?Analyze
A team using GPT-4 as the only judge, finding that GPT-4's own outputs beat equivalent-quality Claude outputs, with which failure and which fix left blank.
16. A judge's scores rise with response length even when the content is equivalent. Which mitigation fits?Analyze
An illustrative rising line of judge score against response length even though content quality is held equal across responses, with which mitigation fits left blank.
Illustrative trend matching the stem's claim, not measured data.
17. After a prompt change your eval reports 0.82, and no earlier runs were stored. What can you conclude?Analyze
A single plotted eval score of 82 with no earlier runs on the timeline to compare it against, and what can be concluded left blank.
18. A model has low perplexity on WikiText but often ignores instructions. Which explanation fits?Analyze
Two bars for the same model, a low perplexity score on WikiText and a poor instruction-following score, with how both can be true at once left blank.
Illustrative bar heights for contrast, not measured values.
19. You are building a feature that extracts dates from legal contracts, and your candidate model scores 90% on MMLU. What should you do next?Design
A date-extraction task over legal contracts with a candidate model that scores 90 percent on MMLU, and what should happen next left blank.
20. You must score a model that extracts product name, issue category and sentiment from complaint emails. Which scoring fits?Design
A complaint email broken into three fields to extract, product name, issue category and sentiment, each left blank, with which scoring approach fits this task left blank.
21. You need to rank two chat models on helpfulness, where many answers are valid, with little annotation budget. Which design fits?Design
Two chat models being compared on helpfulness where many answers are valid, with a note that little annotation budget is available, and which evaluation design fits left blank.

Build It

Step 1: A Minimal Eval Framework

Define the core abstractions. An eval case has an input, an expected output, and an optional metadata dict. A scorer takes a prediction and a reference and returns a score between 0 and 1.

python
import json
from collections import Counter
 
class EvalCase:
    def __init__(self, input_text, expected, metadata=None):
        self.input_text = input_text
        self.expected = expected
        self.metadata = metadata or {}
 
class EvalSuite:
    def __init__(self, name, cases, scorers):
        self.name = name
        self.cases = cases
        self.scorers = scorers
 
    def run(self, model_fn):
        results = []
        for case in self.cases:
            prediction = model_fn(case.input_text)
            scores = {}
            for scorer_name, scorer_fn in self.scorers.items():
                scores[scorer_name] = scorer_fn(prediction, case.expected)
            results.append({
                "input": case.input_text,
                "expected": case.expected,
                "prediction": prediction,
                "scores": scores,
            })
        return results

Step 2: Scoring Functions

Build exact match, token F1, and a simulated LLM-as-judge scorer.

python
def exact_match(prediction, expected):
    return 1.0 if prediction.strip().lower() == expected.strip().lower() else 0.0
 
def token_f1(prediction, expected):
    pred_tokens = set(prediction.lower().split())
    exp_tokens = set(expected.lower().split())
    if not pred_tokens or not exp_tokens:
        return 0.0
    common = pred_tokens & exp_tokens
    precision = len(common) / len(pred_tokens)
    recall = len(common) / len(exp_tokens)
    if precision + recall == 0:
        return 0.0
    return 2 * (precision * recall) / (precision + recall)
 
def llm_judge_simulated(prediction, expected):
    pred_words = set(prediction.lower().split())
    exp_words = set(expected.lower().split())
    if not exp_words:
        return 0.0
    overlap = len(pred_words & exp_words) / len(exp_words)
    length_penalty = min(1.0, len(prediction) / max(len(expected), 1))
    return round(overlap * 0.7 + length_penalty * 0.3, 3)

Step 3: ELO Rating System

Implement pairwise comparisons with ELO updates. This is exactly the system Chatbot Arena uses to rank models.

python
class ELOTracker:
    def __init__(self, k=32, initial_rating=1500):
        self.ratings = {}
        self.k = k
        self.initial_rating = initial_rating
        self.history = []
 
    def _ensure_player(self, name):
        if name not in self.ratings:
            self.ratings[name] = self.initial_rating
 
    def expected_score(self, rating_a, rating_b):
        return 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
 
    def record_match(self, player_a, player_b, outcome):
        self._ensure_player(player_a)
        self._ensure_player(player_b)
 
        ea = self.expected_score(self.ratings[player_a], self.ratings[player_b])
        eb = 1 - ea
 
        if outcome == "a":
            sa, sb = 1.0, 0.0
        elif outcome == "b":
            sa, sb = 0.0, 1.0
        else:
            sa, sb = 0.5, 0.5
 
        self.ratings[player_a] += self.k * (sa - ea)
        self.ratings[player_b] += self.k * (sb - eb)
 
        self.history.append({
            "a": player_a, "b": player_b,
            "outcome": outcome,
            "rating_a": round(self.ratings[player_a], 1),
            "rating_b": round(self.ratings[player_b], 1),
        })
 
    def leaderboard(self):
        return sorted(self.ratings.items(), key=lambda x: -x[1])

Step 4: Perplexity Calculation

Compute perplexity using token probabilities. In practice you would get these from the model's logits. Here we simulate with a probability distribution.

python
import numpy as np
 
def perplexity(log_probs):
    if not log_probs:
        return float("inf")
    avg_neg_log_prob = -np.mean(log_probs)
    return float(np.exp(avg_neg_log_prob))
 
def token_log_probs_simulated(text, model_quality=0.8):
    np.random.seed(hash(text) % 2**31)
    tokens = text.split()
    log_probs = []
    for i, token in enumerate(tokens):
        base_prob = model_quality
        if len(token) > 8:
            base_prob *= 0.6
        if i == 0:
            base_prob *= 0.7
        prob = np.clip(base_prob + np.random.normal(0, 0.1), 0.01, 0.99)
        log_probs.append(float(np.log(prob)))
    return log_probs

Step 5: Aggregate Results

Compute summary statistics across an eval run: mean, median, pass rate at a threshold, and per-metric breakdowns.

python
def summarize_results(results, threshold=0.8):
    all_scores = {}
    for r in results:
        for metric, score in r["scores"].items():
            all_scores.setdefault(metric, []).append(score)
 
    summary = {}
    for metric, scores in all_scores.items():
        arr = np.array(scores)
        summary[metric] = {
            "mean": round(float(np.mean(arr)), 3),
            "median": round(float(np.median(arr)), 3),
            "std": round(float(np.std(arr)), 3),
            "min": round(float(np.min(arr)), 3),
            "max": round(float(np.max(arr)), 3),
            "pass_rate": round(float(np.mean(arr >= threshold)), 3),
            "n": len(scores),
        }
    return summary
 
def print_summary(summary, suite_name="Eval"):
    print(f"\n{'=' * 60}")
    print(f"  {suite_name} Summary")
    print(f"{'=' * 60}")
    for metric, stats in summary.items():
        print(f"\n  {metric}:")
        print(f"    Mean:      {stats['mean']:.3f}")
        print(f"    Median:    {stats['median']:.3f}")
        print(f"    Std:       {stats['std']:.3f}")
        print(f"    Range:     [{stats['min']:.3f}, {stats['max']:.3f}]")
        print(f"    Pass rate: {stats['pass_rate']:.1%} (threshold >= 0.8)")
        print(f"    N:         {stats['n']}")

Step 6: Run the Full Pipeline

Wire everything together. Define a task, create test cases, simulate two models, run evals, compute ELO from pairwise comparisons, and print the leaderboard.

python
def demo_model_good(prompt):
    responses = {
        "What is the capital of France?": "Paris",
        "What is 2 + 2?": "4",
        "Who wrote Hamlet?": "William Shakespeare",
        "What language is PyTorch written in?": "Python and C++",
        "What is the boiling point of water?": "100 degrees Celsius",
    }
    return responses.get(prompt, "I don't know")
 
def demo_model_bad(prompt):
    responses = {
        "What is the capital of France?": "Paris is the capital city of France",
        "What is 2 + 2?": "The answer is four",
        "Who wrote Hamlet?": "Shakespeare",
        "What language is PyTorch written in?": "Python",
        "What is the boiling point of water?": "212 Fahrenheit",
    }
    return responses.get(prompt, "Unknown")
 
cases = [
    EvalCase("What is the capital of France?", "Paris"),
    EvalCase("What is 2 + 2?", "4"),
    EvalCase("Who wrote Hamlet?", "William Shakespeare"),
    EvalCase("What language is PyTorch written in?", "Python and C++"),
    EvalCase("What is the boiling point of water?", "100 degrees Celsius"),
]
 
suite = EvalSuite(
    name="General Knowledge",
    cases=cases,
    scorers={
        "exact_match": exact_match,
        "token_f1": token_f1,
        "llm_judge": llm_judge_simulated,
    },
)
 
results_good = suite.run(demo_model_good)
results_bad = suite.run(demo_model_bad)
 
print_summary(summarize_results(results_good), "Model A (concise)")
print_summary(summarize_results(results_bad), "Model B (verbose)")

The "good" model gives exact answers. The "bad" model gives verbose paraphrases. Exact match punishes the verbose model severely. Token F1 and LLM-as-judge are more forgiving. This illustrates why metric choice matters: the same model looks great or terrible depending on how you score it. In this five-case run, Model B scores 0.000 on exact match, 0.283 mean token F1 and 0.481 mean judge score, with a judge pass rate of 20%. main.py extends this to eight cases and adds a random third model.

Step 7: ELO Tournament

Run pairwise comparisons between models across multiple rounds.

python
elo = ELOTracker(k=32)
 
for case in cases:
    pred_a = demo_model_good(case.input_text)
    pred_b = demo_model_bad(case.input_text)
 
    score_a = token_f1(pred_a, case.expected)
    score_b = token_f1(pred_b, case.expected)
 
    if score_a > score_b:
        outcome = "a"
    elif score_b > score_a:
        outcome = "b"
    else:
        outcome = "tie"
 
    elo.record_match("model_a_concise", "model_b_verbose", outcome)
 
print("\nELO Leaderboard:")
for name, rating in elo.leaderboard():
    print(f"  {name}: {rating:.0f}")

Step 8: Perplexity Comparison

Compare perplexity across "models" of different quality levels.

python
test_text = "The quick brown fox jumps over the lazy dog in the garden"
 
for quality, label in [(0.9, "Strong model"), (0.7, "Medium model"), (0.4, "Weak model")]:
    log_probs = token_log_probs_simulated(test_text, model_quality=quality)
    ppl = perplexity(log_probs)
    print(f"  {label} (quality={quality}): perplexity = {ppl:.2f}")

The values are simulated, so they land far below the real-model numbers in the concept section (roughly 1.2, 1.6 and 2.9 here). What matters is the ordering: lower quality gives higher perplexity. The seed comes from hash(text), so set PYTHONHASHSEED if you want repeatable numbers.

Check: Build It0/15
1. What is an eval suite?Define
One input-expected pair bundled with three named scorer functions under a version tag, presented as a single case from a larger collection.
2. What is a pass rate in an eval summary?Define
Four illustrative case scores as bars with a dashed threshold line at 0.8, and no pass rate computed.
Illustrative scores, not from the lesson.
3. In an ELO tracker, how is a rating updated after a pairwise match?Explain
Player P with a lower rating beating Player Q with a higher rating, an upset result, with how much and why each rating moves left blank.
4. Why does EvalSuite.run take the model as a function and the scorers as a dict?Explain
Three different model functions, two demo models and a real model, all feeding into the same suite.run call with the same cases and scorers dict, with why it is built this way left blank.
5. Why is pass rate more actionable than the mean score?Explain
Two illustrative eval suites with different individual scores that both average to the same mean of 0.65, with which one is more reliable at a 0.8 threshold left blank.
Illustrative scores, not from the lesson.
6. How do exact match and token F1 differ in the Build It scorers?Explain
A prediction that restates the expected answer with extra words, next to the short expected answer, with both the exact_match and token_f1 scores for this pair left blank.
7. What does exact_match(" PARIS ", "paris") return?Apply
The exact_match function given the strings space PARIS space and paris, with a note that both get stripped and lowercased before comparing, and the return value left blank.
8. What does token_f1("Shakespeare", "William Shakespeare") return, to two decimals?Apply
The token_f1 function given Shakespeare as the prediction and William Shakespeare as the expected text, with precision 1.0 and recall 0.5 shown, and the final F1 value left blank.
9. Two new players start at 1500 with k=32. After record_match(a, b, "a"), what are the ratings?Apply
Two players starting at equal ratings of 1500 with k=32, an expected score of 0.5 for each, and a recorded match where a wins, with both resulting ratings left blank.
10. What does expected_score(1900, 1500) return, to three decimals?Apply
The expected_score function given rating_a 1900 and rating_b 1500, with the formula shown and the return value left blank.
11. Every token in a text has log probability log(0.1). What does perplexity(log_probs) return?Apply
The perplexity function given a list where every token's log probability is log of 0.1, with the formula shown and the return value left blank.
12. summarize_results gets scores [0.9, 0.85, 0.5, 0.2] for one metric with threshold 0.8. What is pass_rate?Apply
The summarize_results function given four case scores, 0.9, 0.85, 0.5 and 0.2, at a threshold of 0.8, with the pass_rate formula shown and the final value left blank.
13. What does llm_judge_simulated("The answer is four", "4") return?Apply
The llm_judge_simulated function given the answer is four as the prediction and the digit 4 as expected, with the blend formula shown and no overlap or length-penalty values computed.
14. In the ELO tournament loop that compares demo_model_good and demo_model_bad with token_f1, which outcomes are recorded?Apply
A table of five prompts with the concise model's and verbose model's real predictions shown side by side, and the token_f1 score column for each model left blank for every row.
15. Model B's answers are mostly correct but verbose, yet it scores 0.000 on exact match. What explains this?Analyze
The verbose model's real prediction naming Paris correctly, scored by the real exact_match function at 0.000, with why a correct answer still scores zero left blank.
Real exact_match output.

Use It

lm-evaluation-harness (EleutherAI)

The standard tool for running benchmarks on any model.

python
# pip install lm-eval
# Command line:
# lm_eval --model hf --model_args pretrained=meta-llama/Llama-3.1-8B --tasks mmlu --batch_size 8
 
# Python API:
# import lm_eval
# results = lm_eval.simple_evaluate(
#     model="hf",
#     model_args="pretrained=meta-llama/Llama-3.1-8B",
#     tasks=["mmlu", "hellaswag", "arc_easy"],
#     batch_size=8,
# )
# print(results["results"])

promptfoo

Config-driven eval for prompt engineering. Define tests in YAML and run against multiple providers.

yaml
# promptfoo.yaml
providers:
  - openai:gpt-4o-mini
  - anthropic:claude-3-haiku
 
prompts:
  - "Answer in one word: {{question}}"
 
tests:
  - vars:
      question: "What is the capital of France?"
    assert:
      - type: contains
        value: "Paris"
  - vars:
      question: "What is 2 + 2?"
    assert:
      - type: equals
        value: "4"

RAGAS for RAG evaluation

python
# pip install ragas
# from ragas import evaluate
# from ragas.metrics import faithfulness, answer_relevancy, context_precision
#
# result = evaluate(
#     dataset,
#     metrics=[faithfulness, answer_relevancy, context_precision],
# )
# print(result)

RAGAS measures what generic evals miss: whether the model's answer is grounded in the retrieved context, not just whether the answer is "correct" in the abstract.

Check: Use It0/2
1. Your RAG bot sounds fluent but may state things its retrieved documents do not support. Which tool measures this?Design
Retrieved documents feeding into a generated answer, with a note that the answer sounds fluent but may state things the documents do not support, and which tool measures this left blank.
2. Prompt edits must not break existing cases, tested across two providers from a file in your repo. Which tool fits?Design
A prompt edit that must not break existing test cases, tested across two providers from a config file in the repo, with which tool fits left blank.

Ship It

This lesson produces outputs/prompt-eval-designer.md, a reusable prompt that designs custom eval suites for any task. Give it a task description and it generates test cases, scoring functions, and a pass/fail threshold recommendation.

It also produces outputs/skill-llm-evaluation.md, a decision framework for choosing the right evaluation strategy based on your task type, budget, and latency requirements.

Author's Note

I'd run main.py and read the last block of output first. It scores six prediction and reference pairs with exact match, token F1, and the simulated judge side by side, so you can watch "The capital of France is Paris" get 0 from exact match and a nonzero score from the other two. That disagreement is the argument for picking metrics per task.

Nothing in the file calls a model. The judge is word overlap plus a length term, and the perplexity log-probs come from a seeded random generator, so those numbers show how the functions behave and say nothing about a real model. The seed comes from hash(text), and Python salts string hashes per process, so the perplexity values and the random model in the ELO demo can change between runs unless you set PYTHONHASHSEED.

Exercise 5, the model diff, is the one I'd build next. summarize_results collapses each metric into summary statistics, but the records EvalSuite.run returns already hold the input, prediction, and scores for each case, which is all you need to see what moved.

Exercises

  1. Add a "consistency" scorer that runs the same input through the model 5 times and measures how often the outputs match. Inconsistent answers on deterministic inputs reveal fragile prompts or high temperature settings.

  2. Extend the ELO tracker to support multiple judge functions (exact match, F1, LLM-as-judge) and weight them. Compare how the leaderboard changes when you weight exact match heavily versus F1 heavily.

  3. Build an eval suite for a specific task: email classification into 5 categories. Create 100 test cases with diverse examples including edge cases (emails that could belong to multiple categories, empty emails, emails in other languages). Measure how different "models" (rule-based, keyword matching, simulated LLM) perform.

  4. Implement contamination detection: given a set of eval questions and a training corpus, check what percentage of eval questions (or close paraphrases) appear in the training data. This is how researchers audit benchmark validity.

  5. Build a "model diff" tool. Given eval results from two model versions, highlight which specific test cases improved, which regressed, and which stayed the same. This is the eval equivalent of a code diff, and you need it to understand whether a change helped or hurt.

Key Terms

TermWhat people sayWhat it actually means
MMLU"The benchmark"Massive Multitask Language Understanding -- 15,908 multiple choice questions across 57 subjects, saturated above 88% by 2025
HumanEval"Code eval"164 Python function-completion problems from OpenAI, tests only isolated function generation
SWE-bench"Real coding eval"2,294 GitHub issues from 12 Python repos, measures end-to-end bug fixing including test generation
Perplexity"How confused the model is"exp(-avg(log P(token_i given context))) -- lower means the model assigns higher probability to the actual tokens
ELO rating"Chess ranking for models"A relative skill rating computed from pairwise win/loss records, used by Chatbot Arena to rank 100+ models
LLM-as-judge"Using AI to grade AI"A strong model scores a weaker model's outputs against a rubric, ~80% agreement with human judges at ~$0.01/judgment
Data contamination"The model saw the test"Training data includes benchmark questions, inflating scores without improving real capability
Eval suite"A bunch of tests"A versioned collection of (input, expected_output, scorer) triples that measure a specific capability
Pass rate"What percentage it gets right"Fraction of eval cases scoring above a threshold -- more actionable than mean score because it measures reliability
Chatbot Arena"Model ranking website"LMSYS platform with 2M+ human preference votes, producing the most trusted LLM leaderboard via ELO ratings

Further Reading