Evaluation Frameworks for LLM Outputs
Learn what to evaluate in LLM outputs, automated metrics (BLEU/ROUGE/METEOR), LLM-as-judge, and human evaluation workflows.
What to Evaluate
LLM output quality is multi-dimensional. These are the five core axes:
Dimension | What It Measures | Example Failure | Detection Method |
|---|---|---|---|
Correctness | Factual accuracy against a ground truth | "Paris is the capital of Italy" | KB lookup, verified sources |
Relevance | Whether the output addresses the user's query | Answering tomorrow's weather when asked about today | Embedding similarity |
Groundedness | Whether claims are supported by provided context | Hallucinating citations | Context overlap scoring |
Safety | Absence of toxic, biased, or harmful content | Generating hateful speech | Toxicity classifiers |
Fluency | Grammatical and stylistic quality | Fragmented or incoherent prose | Perplexity, grammar checkers |
Correctness alone is not sufficient. A response can be factually correct but irrelevant, unsafe, or ungrounded. Always evaluate across multiple dimensions. A 100% correct answer that is toxic or off-topic is still a failure.
Evaluation Pipeline
Evaluation should be a systematic, repeatable process. The following pipeline shows how data flows through an evaluation system:
Metric Selection Decision Tree
Choosing the right evaluation metric depends on your use case:
Evaluation Datasets
A high-quality evaluation dataset is the foundation of any evaluation framework. Invest time in creating a representative, well-annotated dataset.
{
"dataset": "customer-support-eval",
"version": "1.0",
"created": "2026-01-15",
"total_examples": 500,
"distribution": {
"password_reset": 100,
"refund_status": 100,
"shipping_inquiry": 100,
"product_question": 100,
"complaint": 100
},
"examples": [
{
"id": "cs-001",
"query": "How do I reset my password?",
"context": "User is on the login page and forgot credentials.",
"reference": "Click 'Forgot Password' on the login screen, enter your email, and follow the reset link.",
"expected_tools": ["send_reset_email"],
"tags": ["password", "authentication"],
"difficulty": "easy"
},
{
"id": "cs-002",
"query": "What is my refund status?",
"context": "User ordered item #ORD-4521 on March 10. The item was delivered on March 15 and returned on March 18.",
"reference": "Your refund for order ORD-4521 is being processed and will appear in 5-7 business days.",
"expected_tools": ["check_refund_status"],
"tags": ["refund", "order-status"],
"difficulty": "medium"
},
{
"id": "cs-003",
"query": "I want to speak to a manager right now!",
"context": "User is angry about a delayed order.",
"reference": "I understand your frustration. Let me escalate this to our support team who will contact you within 2 hours.",
"expected_tools": ["escalate_to_human"],
"tags": ["escalation", "angry-user"],
"difficulty": "hard"
}
]
}Creating an Evaluation Dataset Programmatically
# create_eval_dataset.py
import json
import random
from typing import List, Dict
class EvalDatasetBuilder:
"""Build evaluation datasets from templates."""
def __init__(self):
self.examples = []
def add_example(
self,
query: str,
context: str = "",
reference: str = "",
expected_tools: List[str] = None,
tags: List[str] = None,
difficulty: str = "medium",
) -> "EvalDatasetBuilder":
example = {
"id": f"ex-{len(self.examples) + 1:04d}",
"query": query,
"context": context,
"reference": reference,
"expected_tools": expected_tools or [],
"tags": tags or [],
"difficulty": difficulty,
}
self.examples.append(example)
return self
def save(self, path: str):
dataset = {
"dataset": "custom-eval",
"version": "1.0",
"total_examples": len(self.examples),
"examples": self.examples,
}
with open(path, "w") as f:
json.dump(dataset, f, indent=2)
print(f"Saved {len(self.examples)} examples to {path}")
# === Usage ===
builder = EvalDatasetBuilder()
builder.add_example(
query="What is your return policy?",
reference="We offer 30-day returns with free shipping.",
expected_tools=["lookup_policy"],
tags=["returns", "policy"],
difficulty="easy",
)
builder.add_example(
query="I was charged twice for the same order!",
reference="I apologize for the inconvenience. Let me investigate the duplicate charge.",
expected_tools=["check_charges", "issue_refund"],
tags=["billing", "complaint"],
difficulty="hard",
)
builder.save("eval_dataset.json")Automated Evaluation Metrics
BLEU (Bilingual Evaluation Understudy)
Measures n-gram precision between generated and reference text. Range: 0 to 1. Originally designed for machine translation.
# bleu_example.py
from nltk.translate.bleu_score import (
sentence_bleu,
corpus_bleu,
SmoothingFunction,
)
# Individual sentence score
reference = "the cat sat on the mat".split()
candidate = "the cat sat on a rug".split()
# Use smoothing for short sentences
smoothie = SmoothingFunction().method4
score = sentence_bleu([reference], candidate, smoothing_function=smoothie)
print(f"BLEU score: {score:.3f}")
# BLEU score: ~0.586 (3/4 unigrams match, 2/3 bigrams match)
# Corpus-level BLEU
references = [
["the cat sat on the mat".split()],
["the dog ran in the park".split()],
]
candidates = [
"the cat sat on a rug".split(),
"a dog runs in a park".split(),
]
corpus_score = corpus_bleu(references, candidates, smoothing_function=smoothie)
print(f"Corpus BLEU: {corpus_score:.3f}")ROUGE (Recall-Oriented Understudy for Gisting Evaluation)
Measures recall of n-grams and longest common subsequences. Common variants: ROUGE-1 (unigrams), ROUGE-2 (bigrams), ROUGE-L (longest common subsequence).
# rouge_example.py
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(
["rouge1", "rouge2", "rougeL"],
use_stemmer=True # handles walked/walking etc.
)
reference = "The quick brown fox jumps over the lazy dog"
candidate = "A quick brown fox jumped over a lazy dog"
scores = scorer.score(reference, candidate)
for metric, result in scores.items():
print(f"{metric}: P={result.precision:.3f} R={result.recall:.3f} F1={result.fmeasure:.3f}")
# ROUGE-1: P=0.875 R=0.778 F1=0.824
# ROUGE-2: P=0.571 R=0.500 F1=0.533
# ROUGE-L: P=0.875 R=0.778 F1=0.824METEOR (Metric for Evaluation of Translation with Explicit ORdering)
Extends BLEU by incorporating recall, stemming, synonym matching, and word order.
# meteor_example.py
from nltk.translate.meteor_score import meteor_score
reference = "the cat sat on the mat"
candidate = "a cat sits on a rug"
score = meteor_score([reference], candidate)
print(f"METEOR score: {score:.3f}")
# METEOR score: ~0.642 (benefits from synonym and stemming matching)BERTScore
Leverages pre-trained BERT embeddings to compute semantic similarity between candidate and reference: captures meaning even when wording differs completely.
# bertscore_example.py
from bert_score import score as bertscore
# List of candidate and reference strings
candidates = [
"The cat is sitting on the mat",
"The weather is nice today",
]
references = [
"A cat sits on a rug",
"It is sunny outside",
]
# Compute BERTScore
P, R, F1 = bertscore(
candidates,
references,
model_type="microsoft/deberta-xlarge-mnli",
lang="en",
verbose=True,
)
for i, (p, r, f) in enumerate(zip(P, R, F1)):
print(f"Example {i + 1}: P={p:.3f} R={r:.3f} F1={f:.3f}")
# BERTScore captures that "cat sitting on mat" ≈ "cat sits on rug" semantically,
# even with zero n-gram overlap on some tokens.Limitations of N-Gram Metrics
- BLEU and ROUGE correlate poorly with human judgment for creative or open-ended tasks
- METEOR improves on synonym handling but still misses semantic meaning entirely
- None detect hallucinations, toxicity, or factual accuracy directly
- All require reference texts, which are expensive to produce at scale
- All are vulnerable to stylistic differences: a perfectly valid paraphrase may score poorly
LLM-as-Judge
Use a strong LLM (e.g., GPT-4, Claude) to evaluate another LLM's outputs. This approach captures semantic quality that n-gram metrics miss. It is especially valuable for open-ended tasks where reference answers do not exist.
# llm_as_judge.py
import json
from openai import OpenAI
client = OpenAI()
def judge_output(
query: str,
generated: str,
context: str = "",
rubric: dict = None,
) -> dict:
"""
Evaluate generated output using an LLM judge.
Args:
query: Original user query
generated: LLM-generated response to evaluate
context: Optional context provided to the LLM
rubric: Scoring criteria
Returns:
dict with scores, reasoning, and overall verdict
"""
if rubric is None:
rubric = {
"correctness": "Is every claim factually accurate?",
"relevance": "Does the response directly address the query?",
"groundedness": "Are claims supported by provided context?",
"helpfulness": "Does the response actually help the user?",
}
rubric_text = "\n".join(
f"- {dim}: {desc}"
for dim, desc in rubric.items()
)
judge_prompt = f"""You are an expert evaluator of AI assistant responses.
## Scoring Rubric
{rubric_text}
## User Query
{query}
{'## Context' + context if context else ''}
## Generated Response
{generated}
## Task
Score the response on a scale of 1-5 for each dimension.
Provide a brief justification for each score.
Output JSON with keys: "scores" (dict), "reasoning" (str), "overall_score" (float).
"""
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": judge_prompt}],
response_format={"type": "json_object"},
temperature=0.0, # deterministic for reproducibility
)
return json.loads(response.choices[0].message.content)
# === Usage ===
result = judge_output(
query="Explain quantum entanglement in simple terms",
generated="Quantum entanglement is a phenomenon where two particles become linked so that measuring one instantly affects the other, no matter how far apart they are.",
rubric={
"correctness": "Is the physics explanation accurate?",
"clarity": "Is the explanation accessible to a beginner?",
"conciseness": "Is the response free of unnecessary detail?",
},
)
print(json.dumps(result, indent=2))
# Example output:
# {
# "scores": {"correctness": 5, "clarity": 4, "conciseness": 5},
# "reasoning": "...",
# "overall_score": 4.7
# }When using LLM-as-judge, set temperature=0 for deterministic scoring and use response_format={"type": "json_object"} to get structured output. Always validate the judge's reasoning for a sample of cases to detect judge-model bias (e.g., favoring longer responses, or favoring its own style).
BERTScore for Semantic Similarity
BERTScore computes token-level similarity using contextual embeddings. Unlike n-gram metrics, it can recognize paraphrases and semantically equivalent statements.
# semantic_evaluation.py
import torch
from bert_score import BERTScorer
class SemanticEvaluator:
"""
Evaluate LLM outputs using BERTScore for semantic similarity
and an LLM judge for qualitative dimensions.
"""
def __init__(self, model_type="microsoft/deberta-xlarge-mnli"):
self.scorer = BERTScorer(
model_type=model_type,
lang="en",
device="cuda" if torch.cuda.is_available() else "cpu",
)
def compute_similarity(self, candidates: list, references: list) -> dict:
"""Compute BERTScore metrics."""
P, R, F1 = self.scorer.score(candidates, references)
return {
"precision": P.mean().item(),
"recall": R.mean().item(),
"f1": F1.mean().item(),
"individual_scores": [
{"precision": p.item(), "recall": r.item(), "f1": f.item()}
for p, r, f in zip(P, R, F1)
],
}
# Usage
evaluator = SemanticEvaluator()
scores = evaluator.compute_similarity(
candidates=["The feline rests upon the floor covering"],
references=["The cat sits on the rug"],
)
print(f"Semantic similarity (F1): {scores['f1']:.3f}")
# Despite zero word overlap, BERTScore captures the semantic equivalenceHuman Evaluation
Automated metrics are not a substitute for human judgment, especially for subjective dimensions like tone and helpfulness.
Approach | Cost per 1000 | Speed per 1000 | Consistency | Best For |
|---|---|---|---|---|
N-gram metrics | ~$0.01 | Seconds | High | Translation, summarization |
BERTScore | ~$1.00 | Minutes | High | Semantic similarity |
LLM-as-judge | ~$5.00 | Minutes | Medium | Open-ended QA, reasoning |
Human eval | ~$500 | Hours-Days | Low | Safety, brand voice, UX |
Designing a Human Evaluation Rubric
# human_eval_rubric.py
human_rubric = {
"accuracy": {
"description": "Are all factual claims correct?",
"scale": [
(1, "Multiple factual errors"),
(2, "One significant error"),
(3, "Minor inaccuracies"),
(4, "All claims accurate"),
(5, "Perfect accuracy with citations"),
],
},
"helpfulness": {
"description": "Does this response actually solve the user's problem?",
"scale": [
(1, "Completely unhelpful / off-topic"),
(2, "Partially addresses the query"),
(3, "Addresses most aspects"),
(4, "Fully addresses the query"),
(5, "Exceeds expectations with additional value"),
],
},
"tone": {
"description": "Is the tone appropriate for the context?",
"scale": [
(1, "Rude, aggressive, or inappropriate"),
(2, "Impersonal or robotic"),
(3, "Neutral and professional"),
(4, "Friendly and warm"),
(5, "Perfectly calibrated to user's emotional state"),
],
},
}Comparison Table
Metric | Type | Measures | Range | Strengths | Weaknesses | Best Use Case |
|---|---|---|---|---|---|---|
BLEU | N-gram | Precision | 0 to 1 | Fast, deterministic | Ignores recall, semantics | Machine translation |
ROUGE | N-gram | Recall | 0 to 1 | Good for summarization | Ignores fluency, order | Summarization |
METEOR | N-gram+syn | F-score | 0 to 1 | Synonym handling, stem, order | Complexity, still surface-level | Translation, captioning |
BERTScore | Embedding | Semantic F1 | 0 to 1 | Captures paraphrases, meaning | GPU required, slower | Semantic similarity |
LLM-as-judge | Model | Semantic quality | 1 to 5 | Captures nuance, customizable | Expensive, biased by judge | Open-ended QA, chat |
Human | Manual | All dimensions | Subjective | Gold standard for quality | Slow, expensive, inconsistent | Safety, brand, UX |
Practice Questions
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A response receives a high BLEU score but users report it is unhelpful. What is the most likely explanation?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A team wants to evaluate whether an LLM's claims are supported by the provided context documents. Which evaluation dimension does this target?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
An evaluation team has 10,000 production conversations but budget to evaluate only 1,000. What sampling strategy is recommended?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A development team uses GPT-4 to score another LLM's outputs on a 1-5 scale. What is a known drawback of this LLM-as-judge approach?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A team is evaluating a summarization system and needs a metric that considers recall, stemming, and synonym matching. Which metric should they use?
Continuous Evaluation Workflow
# run_eval_pipeline.py
"""Automated evaluation pipeline combining multiple metrics."""
import json
from typing import List, Dict
class EvalPipeline:
"""Run multiple evaluation methods and aggregate results."""
def __init__(self, dataset_path: str):
with open(dataset_path) as f:
self.dataset = json.load(f)["examples"]
def run_all(self) -> Dict:
"""Run all evaluation methods on the dataset."""
results = {
"total": len(self.dataset),
"by_metric": {},
"examples": [],
}
for example in self.dataset:
# Simulate generating a response from your LLM
generated = self._generate_response(example["query"])
# Compute n-gram metrics
bleu = self._compute_bleu(example["reference"], generated)
rouge = self._compute_rouge(example["reference"], generated)
# LLM-as-judge evaluation
judge_result = self._llm_judge(
example["query"], generated, example["context"]
)
example_result = {
"id": example["id"],
"bleu": bleu,
"rouge": rouge,
"judge_scores": judge_result,
}
results["examples"].append(example_result)
# Aggregate
results["by_metric"]["avg_bleu"] = sum(
e["bleu"] for e in results["examples"]
) / len(results["examples"])
return results
def _generate_response(self, query: str) -> str:
# Placeholder — in production, call your LLM here
return f"Response to: {query}"
def _compute_bleu(self, reference: str, generated: str) -> float:
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction
smoothie = SmoothingFunction().method4
return sentence_bleu(
[reference.split()], generated.split(),
smoothing_function=smoothie
)
def _compute_rouge(self, reference: str, generated: str) -> Dict:
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=True)
result = scorer.score(reference, generated)
return {
"precision": result["rougeL"].precision,
"recall": result["rougeL"].recall,
"f1": result["rougeL"].fmeasure,
}
def _llm_judge(self, query: str, generated: str, context: str) -> Dict:
# Placeholder — in production, call GPT-4/Claude
return {
"correctness": 4,
"relevance": 5,
"overall": 4.5,
}Key Takeaways
- Evaluate LLM outputs across five dimensions: correctness, relevance, groundedness, safety, and fluency.
- N-gram metrics (BLEU, ROUGE, METEOR) are fast but surface-level; they miss semantics and hallucinations.
- BERTScore captures semantic similarity using embeddings; it detects paraphrases that n-gram metrics miss.
- LLM-as-judge captures semantic quality but introduces cost and judge-model bias; use temperature=0 for reproducibility.
- Human evaluation is the gold standard but scales poorly; use it strategically for subjective dimensions.
- A representative evaluation dataset is more important than a large one; invest in quality annotation with stratified sampling.
- Combine multiple evaluation methods: no single metric tells the full story. Use n-gram metrics for speed, BERTScore for semantics, and LLM-as-judge for nuanced quality.
- Automate the evaluation pipeline to run on every release, tracking trends over time and alerting on regressions.