If you can’t measure whether your agent is getting better, you’re flying blind. Most teams building with LLMs spend weeks iterating on prompts, swapping models, and tuning parameters — then evaluate the results by vibes. That’s how you end up shipping regressions you don’t catch until a user complains. Evaluating LLM output quality metrics rigorously is what separates teams that ship reliable agents from teams that ship demos that fall apart in production.
This article gives you a concrete framework: which metrics actually matter for which tasks, how to run A/B tests on model outputs without losing your mind, and what good automated + human evaluation pipelines look like in practice. There’s working code throughout. The goal is that you leave here able to quantify whether your next prompt change is an improvement or a regression.
Why “It Looks Good to Me” Doesn’t Scale
Manual review works fine for 20 test cases. Once you’re running 500+ evaluations per deployment — or running evaluations on multiple models, prompt variants, or retrieval strategies simultaneously — you need something systematic. The failure mode I see most often: a team improves accuracy on the cases they’re actively looking at, while silently degrading performance on edge cases they’ve stopped checking.
The other problem is that “quality” means different things depending on your task type:
- Summarisation: faithfulness to source, coverage, length control
- Classification: precision, recall, F1 — standard ML metrics apply
- Code generation: syntactic validity, test pass rate, functional correctness
- Conversational agents: task completion rate, hallucination rate, tone consistency
- RAG pipelines: answer relevance, context utilisation, grounding accuracy
Pick the wrong metric and you’ll optimise for the wrong thing. I’ve seen teams chase BLEU score improvements on a summarisation task while their faithfulness scores quietly collapsed — producing fluent-sounding summaries that contradicted the source document.
The Metrics Worth Actually Using
Reference-Based Metrics: BLEU, ROUGE, and When to Skip Them
BLEU and ROUGE measure n-gram overlap between model output and a reference answer. They’re fast, deterministic, and cheap. They’re also frequently misleading for anything beyond narrow translation or extraction tasks.
ROUGE-L (longest common subsequence) is more forgiving about word order than ROUGE-N and works reasonably well for summarisation evaluation if you have high-quality reference summaries. BLEU is better suited to translation where references are more stable.
The real limitation: these metrics penalise valid paraphrases. If your reference says “the meeting was cancelled” and the model outputs “the meeting did not take place”, ROUGE scores this poorly despite it being a correct and natural rephrasing. For open-ended generation, these numbers have a low correlation with actual quality.
My take: Use ROUGE-L as a sanity check and regression detector, not as your primary quality signal. If ROUGE-L drops 15% between versions, investigate — but don’t assume a 5% ROUGE-L improvement means you’ve improved the product.
Semantic Similarity: Embeddings-Based Evaluation
BERTScore computes similarity using contextual embeddings rather than exact token overlap. It correlates better with human judgment for most generation tasks and handles paraphrasing correctly. It’s slower and more expensive than ROUGE but still cheap compared to LLM-as-judge approaches.
from bert_score import score
# candidates: list of model outputs
# references: list of ground truth answers
candidates = ["The meeting was cancelled due to illness."]
references = ["The meeting did not take place because someone was sick."]
P, R, F1 = score(candidates, references, lang="en", verbose=False)
print(f"BERTScore F1: {F1.mean().item():.4f}")
# Typically 0.85-0.95 for semantically equivalent outputs
For RAG evaluation specifically, I’d also look at answer relevance (does the output address the question?) and faithfulness (are claims in the output supported by the retrieved context?). The RAGAS library implements both with reasonable defaults.
LLM-as-Judge: The Most Practical Option for Complex Tasks
For tasks where ground truth is ambiguous or expensive to annotate — tone, helpfulness, reasoning quality, instruction following — using a stronger LLM to grade outputs has become the most practical approach. Claude Opus or GPT-4o as an evaluator against your Claude Haiku or Sonnet outputs works well, with a few caveats.
import anthropic
client = anthropic.Anthropic()
def llm_judge(question: str, model_output: str, rubric: str) -> dict:
"""
Returns a score (1-5) and reasoning for a given output.
rubric should describe what a good answer looks like.
"""
prompt = f"""You are evaluating an AI assistant's response. Score it 1-5 based on the rubric.
Question: {question}
Model output: {model_output}
Rubric: {rubric}
Respond in this exact format:
Score: [1-5]
Reasoning: [one sentence explanation]"""
message = client.messages.create(
model="claude-opus-4-5",
max_tokens=150,
messages=[{"role": "user", "content": prompt}]
)
response_text = message.content[0].text
lines = response_text.strip().split('\n')
return {
"score": int(lines[0].split(': ')[1]),
"reasoning": lines[1].split(': ', 1)[1]
}
# Example usage
result = llm_judge(
question="What causes inflation?",
model_output="Inflation is caused by too much money chasing too few goods...",
rubric="A good answer explains the core mechanism, mentions at least two causes, and avoids jargon."
)
print(result)
At current pricing, running 1,000 evaluations with Claude Opus as judge costs roughly $15-25 depending on output length. That’s affordable enough to run on every deployment. The catch: LLM judges have known biases toward longer outputs and toward their own style. Always validate your judge’s ratings against a human-annotated gold set before trusting it at scale.
Building an A/B Testing Framework for Agent Outputs
The mental model here is the same as A/B testing in web products: you have a control (current production prompt/model) and a treatment (your proposed change), you run both against the same input set, and you measure whether the treatment wins on your chosen metrics with statistical significance.
Setting Up Your Evaluation Dataset
Your eval set is probably the most important investment you’ll make. Rules of thumb from experience:
- Minimum 100 examples for any meaningful signal; 500+ for production confidence
- Include known hard cases and edge cases, not just typical inputs — easy cases don’t differentiate models
- Stratify by input type if your agent handles diverse tasks
- Version your eval set — adding examples over time is fine, but track which version you ran against
import json
import hashlib
from datetime import datetime
def run_evaluation(
eval_dataset: list[dict],
model_fn, # callable that takes input, returns output string
metrics: list, # list of metric functions
run_name: str
) -> dict:
"""
Runs a model against an eval dataset and returns scored results.
Each item in eval_dataset should have 'input' and 'reference' keys.
"""
results = []
for item in eval_dataset:
output = model_fn(item["input"])
scores = {}
for metric in metrics:
scores[metric.__name__] = metric(output, item["reference"])
results.append({
"input": item["input"],
"output": output,
"reference": item["reference"],
"scores": scores
})
# Aggregate scores
aggregated = {}
for metric in metrics:
name = metric.__name__
aggregated[name] = sum(r["scores"][name] for r in results) / len(results)
return {
"run_name": run_name,
"timestamp": datetime.utcnow().isoformat(),
"dataset_hash": hashlib.md5(json.dumps(eval_dataset).encode()).hexdigest()[:8],
"n_examples": len(results),
"aggregate_scores": aggregated,
"individual_results": results
}
Statistical Significance — Don’t Skip This
A 3% improvement in average score across 50 examples is meaningless noise. Before declaring a winner, run a paired t-test or Wilcoxon signed-rank test on the per-example scores. The scipy.stats library makes this trivial:
from scipy import stats
import numpy as np
def compare_runs(run_a: dict, run_b: dict, metric_name: str) -> dict:
scores_a = [r["scores"][metric_name] for r in run_a["individual_results"]]
scores_b = [r["scores"][metric_name] for r in run_b["individual_results"]]
# Wilcoxon is more robust than t-test for non-normal score distributions
statistic, p_value = stats.wilcoxon(scores_a, scores_b)
mean_a = np.mean(scores_a)
mean_b = np.mean(scores_b)
return {
"metric": metric_name,
"mean_a": round(mean_a, 4),
"mean_b": round(mean_b, 4),
"delta": round(mean_b - mean_a, 4),
"p_value": round(p_value, 4),
"significant": p_value < 0.05,
"winner": "B" if (mean_b > mean_a and p_value < 0.05) else "A" if (mean_a > mean_b and p_value < 0.05) else "inconclusive"
}
If your eval set is small (under 100 examples), you’ll rarely hit significance for anything under a 10-15% improvement. That’s not a framework problem — that’s the honest reality of small sample sizes. Invest in building a bigger eval set before running experiments.
Human Evaluation: Where to Spend It and Where to Automate
Automated metrics are fast and scalable. Human evaluation is slow and expensive but remains the ground truth for subjective quality. The right answer is a hybrid: use automated metrics for continuous regression testing on every change, and reserve human evaluation for periodic calibration and for cases your automated metrics flag as ambiguous.
Concretely, I’d structure it this way:
- Every deployment: Run full automated eval suite (ROUGE-L, BERTScore, LLM-judge score). Block deployment if any metric drops more than a threshold (e.g., 5% relative decline).
- Weekly or per major change: Human review of 20-30 examples sampled from production traffic, stratified by score decile. Pay special attention to cases where LLM judge gave high scores — this catches “confidently wrong” outputs your automated metrics miss.
- Quarterly: Full human annotation pass on a fresh sample to re-calibrate your LLM judge and update your eval dataset with newly discovered failure modes.
For human annotation tooling, Label Studio (open source) works well for small teams. Argilla is worth looking at if you want tighter Python integration. Both are free to self-host.
Production Monitoring: Eval Doesn’t Stop at Deployment
Your eval set represents the inputs you anticipated. Production traffic will surprise you. Set up logging from day one so you can sample real inputs and outputs for retrospective evaluation. Log at minimum: the input, the output, model version, prompt version, latency, and any user feedback signals (thumbs up/down, corrections, session abandonment).
User feedback signals are a weak but free proxy for quality. A sudden drop in thumbs-up rate or a spike in regeneration requests often precedes a formal quality regression you’d catch in evaluation — treat them as early warning signals, not definitive metrics.
One genuinely useful pattern: run your LLM judge asynchronously on a random 10% sample of production outputs, store the scores, and alert if the rolling 7-day average drops. This costs roughly $1-3/day at moderate traffic and has caught real regressions in production before users noticed.
Which Approach for Which Team
Solo founder or small team moving fast: Start with LLM-as-judge (it’s the fastest path to useful signal), build a 100-example eval set around your most critical use cases, and run it manually before each major prompt change. Don’t over-engineer the infrastructure — a spreadsheet tracking eval results per version is fine early on.
Team with an existing product and real users: Add automated eval to your CI pipeline. The code above is close to production-ready. Prioritise building out your eval dataset using sampled production data — it’ll be more representative than synthetically generated examples.
Enterprise or regulated use cases: Human evaluation isn’t optional. You need documented eval processes, annotator agreement rates (Cohen’s kappa), and audit trails. Invest in proper annotation tooling and treat your eval dataset like production data.
The bottom line on evaluating LLM output quality metrics: there’s no single metric that works for everything, automated evaluation is necessary but not sufficient, and the teams that build reliable agents are the ones that treat evaluation as a first-class engineering concern — not an afterthought. Start measuring something today, even if it’s imperfect. Iteration on a flawed metric beats having no signal at all.
Editorial note: API pricing, model capabilities, and tool features change frequently — always verify current details on the vendor’s website before building in production. Code examples are tested at time of writing; pin your dependency versions to avoid breaking changes. Some links in this article may be affiliate links — we may earn a commission if you sign up, at no extra cost to you.

