Why Evaluation Is Hard
Evaluating LLMs is harder than evaluating classical ML models. A classifier predicting churn has a clear ground truth. An LLM writing a summary, answering a question, or generating code has:
- Multiple valid outputs: "Paris" and "The capital of France is Paris" are both correct
- Context-dependent quality: a brief answer may be good for a chatbot, bad for a report
- Emergent capabilities: new abilities appear at scale, requiring new tests
- Gaming risk: models may overfit to benchmarks
Good evaluation requires combining automatic metrics for scale with human evaluation or LLM-as-judge for nuance.
Intrinsic Metrics
Perplexity
Perplexity measures how well a model predicts a held-out test set. Lower = better.
- Perplexity = 1: perfect prediction (every next token is certain)
- Perplexity = V (vocab size): random guessing
- Typical LLM perplexity on web text: 5–20
Limitations: Perplexity measures fluency, not factuality or usefulness. A model can have low perplexity while hallucinating. It's useful for comparing models on the same test set but not for downstream task performance.
Reference-Based Metrics
BLEU (Bilingual Evaluation Understudy)
Originally for machine translation; measures n-gram overlap between generated text and reference(s).
where p_n is the precision of n-grams (usually n=1..4) and BP is a brevity penalty.
- Range: 0–1 (or 0–100)
- Good BLEU score: depends heavily on task; >40 is often considered good for MT
- Limitations: penalizes valid paraphrases; doesn't measure fluency or semantics
ROUGE (Recall-Oriented Understudy for Gisting Evaluation)
Common for summarization; measures recall of n-grams from reference.
| Variant | Measures |
|---|---|
| ROUGE-1 | Unigram overlap |
| ROUGE-2 | Bigram overlap |
| ROUGE-L | Longest common subsequence |
BERTScore
Uses contextual embeddings (from BERT) to compute semantic similarity between generated and reference text — more robust than n-gram overlap.
from rouge_score import rouge_scorer
from bert_score import score as bert_score
# Reference summaries and model outputs
references = [
"The study found that exercise reduces the risk of heart disease by 30%.",
"Renewable energy sources now account for 20% of global electricity.",
]
hypotheses = [
"Research shows physical activity decreases heart disease risk by roughly a third.",
"Green energy represents a fifth of worldwide power generation.",
]
# ROUGE
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
for ref, hyp in zip(references, hypotheses):
scores = scorer.score(ref, hyp)
print(f"ROUGE-1: {scores['rouge1'].fmeasure:.3f} "
f"ROUGE-2: {scores['rouge2'].fmeasure:.3f} "
f"ROUGE-L: {scores['rougeL'].fmeasure:.3f}")
# ROUGE-1: 0.286 ROUGE-2: 0.154 ROUGE-L: 0.214
# ROUGE-1: 0.231 ROUGE-2: 0.000 ROUGE-L: 0.231
# (low scores even though the hypotheses are semantically correct)
# BERTScore — captures semantic similarity better
P, R, F1 = bert_score(hypotheses, references, lang="en", rescale_with_baseline=True)
for i, (p, r, f) in enumerate(zip(P, R, F1)):
print(f"Pair {i+1}: Precision={p:.3f}, Recall={r:.3f}, F1={f:.3f}")
# Pair 1: Precision=0.872, Recall=0.891, F1=0.881 (correctly high)
# Pair 2: Precision=0.845, Recall=0.862, F1=0.853 (correctly high)LLM-as-a-Judge
A powerful modern approach: use a strong LLM to evaluate another LLM's outputs. Widely used because it:
- Scales to thousands of examples cheaply
- Handles diverse outputs without reference texts
- Correlates well with human preferences (~70–85% agreement)
Single-Answer Grading
Ask the judge to score a single response on a rubric:
Judge system: You are an expert evaluator. Rate the response on:
- Correctness (1-5)
- Completeness (1-5)
- Clarity (1-5)
Return JSON: {"correctness": N, "completeness": N, "clarity": N, "rationale": "..."}
Pairwise Comparison (A/B Testing)
Present two responses and ask which is better. More reliable than absolute scoring.
Limitations
- Position bias: LLMs prefer response A over B regardless of quality
- Self-enhancement: a model may prefer its own style
- Mitigate: randomize order, use CoT reasoning before judging, use a different family model as judge
import anthropic
import json
client = anthropic.Anthropic()
JUDGE_SYSTEM = """You are an expert evaluator for AI-generated answers.
Evaluate the response on three criteria (1-5 each):
- accuracy: Is the information correct and factual?
- helpfulness: Does it actually answer the question asked?
- clarity: Is it well-written and easy to understand?
Respond ONLY with valid JSON in this exact format:
{"accuracy": N, "helpfulness": N, "clarity": N, "rationale": "brief explanation"}"""
def evaluate_response(question: str, response: str) -> dict:
"""Use Claude to judge a response quality."""
result = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
system=JUDGE_SYSTEM,
messages=[{
"role": "user",
"content": f"Question: {question}\n\nResponse to evaluate:\n{response}"
}],
)
return json.loads(result.content[0].text)
def pairwise_compare(question: str, response_a: str, response_b: str) -> dict:
"""Compare two responses to determine which is better."""
result = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
system="""Compare two responses and determine which is better.
Return JSON: {"winner": "A" or "B" or "tie", "reasoning": "brief explanation"}""",
messages=[{
"role": "user",
"content": f"Question: {question}\n\nResponse A:\n{response_a}\n\nResponse B:\n{response_b}"
}],
)
return json.loads(result.content[0].text)
# Example usage
question = "What is the capital of Australia?"
good_response = "The capital of Australia is Canberra, not Sydney as often mistakenly believed."
bad_response = "Australia's capital is Sydney, which is also its largest city."
scores = evaluate_response(question, good_response)
print(f"Scores: {scores}")
comparison = pairwise_compare(question, good_response, bad_response)
print(f"Winner: {comparison['winner']} — {comparison['reasoning']}")Standard Benchmarks
Knowledge & Reasoning
| Benchmark | What It Tests | Format |
|---|---|---|
| MMLU | 57 subject areas (math, science, law, medicine, etc.) | 4-choice MCQ |
| HellaSwag | Common sense completion | 4-choice MCQ |
| ARC | Grade-school science | 4-choice MCQ |
| TruthfulQA | Truthfulness, avoiding false beliefs | Generation |
Math & Reasoning
| Benchmark | What It Tests |
|---|---|
| GSM8K | Grade school math word problems |
| MATH | Competition-level math (algebra, geometry, etc.) |
| BBH (BIG-Bench Hard) | Difficult reasoning, logic, and multi-step tasks |
Code
| Benchmark | What It Tests |
|---|---|
| HumanEval | Python function completion (164 problems) |
| MBPP | Mostly basic Python problems |
| SWE-bench | Real GitHub issues — requires working code changes |
Comprehensive
- HELM: 42 scenarios across tasks and metrics with standardized evaluation
- LMSYS Chatbot Arena: real-world human preference ranking (ELO)
- OpenLLM Leaderboard: open-weight model comparisons on MMLU, ARC, etc.
Task-Specific RAG Evaluation
For RAG systems, evaluation covers retrieval and generation separately:
Retrieval metrics:
- Context Recall: fraction of relevant ground-truth facts found in retrieved chunks
- Context Precision: fraction of retrieved chunks that are actually relevant
Generation metrics:
- Faithfulness: does the answer only use information from retrieved context?
- Answer Relevancy: how relevant is the answer to the question?
RAGAS is a popular framework for automated RAG evaluation using LLM-as-judge:
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall, context_precision
results = evaluate(
dataset=eval_dataset,
metrics=[faithfulness, answer_relevancy, context_recall, context_precision],
)
print(results)
Knowledge check
Why does BERTScore often correlate better with human judgment than BLEU for text generation tasks?
Summary
- Perplexity measures language modeling quality but not task performance or factuality
- BLEU and ROUGE measure n-gram overlap — useful but penalize valid paraphrases
- BERTScore uses semantic embeddings to capture meaning, correlating better with human judgment
- LLM-as-judge scales evaluation cheaply and handles diverse outputs — watch for position and self-enhancement bias
- Key benchmarks: MMLU (knowledge), GSM8K (math), HumanEval (code), LMSYS Arena (human preference)
- RAG evaluation requires separate metrics for retrieval (context recall/precision) and generation (faithfulness, relevancy)
Final chapter: Guardrails — building safety and quality controls into production AI systems.