1:1 mentoring with Big Tech AI engineers
Back to blog
By AgenticPrep TeamUpdated 12 min read

LLM Evaluation Metrics That Actually Matter

The number went up and the complaints went up with it. That is not a paradox: it is what happens when you measure one thing and ship another.

evaluationmetricsllm-judgeragasobservability
Key idea

“How do you evaluate it?” is three questions: is this change better, did we break something we fixed, and what is happening in production now. Pick metrics in pairs so neither can be gamed alone, score retrieval before the answer, and treat an LLM judge as a classifier you must validate against people.

Three layers, each answering a question the others cannot
THREE QUESTIONS, THREE SYSTEMS Offline eval “Is this change better than what we have?” Regression suite “Did we break something we already fixed?” Production signals “What is happening right now?” BEFORE EVERY SHIP golden set, code graders first ON EVERY CHANGE, IN CI one new case per incident CONTINUOUS sampled judge, proxies, no labels Dashed: confirmed production failures become new cases The third layer is where the first one’s test set comes from.

Most teams build the first, skip the second and bolt on the third after an incident. On a phone the figure pans sideways.

Almost every team reports the same thing sooner or later: eval scores rising while user complaints rise too. It is rarely a broken metric. It is a test set that stopped looking like production weeks ago and kept returning a number. When the score and the complaints disagree, treat the complaints as ground truth and the eval as the suspect.

Choose metrics in pairs

MetricAnswersBlind to
Task successdid it do the thing?how: including a path that broke policy
Recall@kwas the answer reachable?whether the model used it
Faithfulnessis each claim in the sources?whether the sources are right
Escalation ratehow often it hands offwhether handing off was right
Cost per taskis it viable?quality, entirely
Thumbs downare voters unhappy?the users who just leave

The pairing rule matters more than any row. Success without cost hides an agent that got expensive; cost without success hides one that got cheap by giving up. So count cost per resolved task:

CurrentTrimmed
Tokens per task60K in, 3K out45K in, 3K out
Cost per task$0.30$0.24
Resolved, of 1,000800600
Per resolved task$0.375$0.40

Claude Opus 5.5 list prices, $4 in and $20 out per million tokens, no caching. The variant is 20% cheaper per task and 7% dearer per outcome, before the 200 extra tasks a person now handles (Metrics & Dashboards).

Score retrieval before the answer

Recall@5AnswersLook at
lowlowchunking, search, filters: the model never saw it
highlowreranker cut, prompt assembly, generation
highhigh, users unhappythe test set no longer looks like traffic

In a RAG system the first question is not “was the answer good” but “was it reachable”. Recall@k needs no judge: label each golden question with the source span that answers it, and count how often that span is in the top k. Store spans, not chunk ids, because ids change with every re-chunk and a stale label reads as a retrieval failure. MRR and nDCG add rank order; precision@k says how much of what the model reads is useful (RAG Evaluation). The usual cause of a low first row is a boundary in the wrong place: RAG chunking strategies.

Use RAGAS for the claims a rule cannot check

RAGAS metricReference?Tells you
Faithfulnessnoshare of claims the contexts support
Answer relevancynoon topic; never checks truth
Context precisionyesuseful chunks ranked first
Context recallyesthe reference’s claims were retrieved
import anthropic
from ragas.llms.base import llm_factory
from ragas.metrics.collections import ContextRecall, Faithfulness

client = anthropic.AsyncAnthropic()           # ascore needs an async client
judge = llm_factory("claude-sonnet-5-5", provider="anthropic", client=client)
for arg in ("temperature", "top_p"):          # ragas sends both; current models refuse them
    judge.model_args.pop(arg, None)
# ragas parses content[0].text: no thinking first (Sonnet 5.5 rejects "disabled")
judge.model_args["thinking"] = {"type": "between_tools"}

async def score(row):
    f = await Faithfulness(llm=judge).ascore(
        user_input=row["q"], response=row["answer"],
        retrieved_contexts=row["contexts"])
    r = await ContextRecall(llm=judge).ascore(
        user_input=row["q"], retrieved_contexts=row["contexts"],
        reference=row["reference"])          # a human-written answer
    return f.value, r.value                   # floats from 0 to 1, or NaN
# asyncio.run(score(row)) for one row; asyncio.gather for a batch

RAGAS 0.4.3 is still the newest release on PyPI (checked 2026-10-21); install it with pip install "ragas==0.4.3" "langchain-community==0.4.1", because langchain-community 0.4.2 removed a module ragas imports. Run end to end in a clean install with a scripted judge, not a live API call; the Sonnet 5.5 thinking settings are from Anthropic’s thinking docs. Make one live call and check the block types before a batch.

Each metric is a judge reading fields of one logged row, so a run has a price: about 11 judge calls per row for the four core metrics at k = 5 (RAGAS). Context precision and recall need a reference a person wrote, so they run on the golden set only; faithfulness and answer relevancy can run on sampled live traffic. A reply with no claims scores NaN, not 1: print the NaN count beside every mean, or an alert on the mean never fires.

Validate the judge like the classifier it is

from sklearn.metrics import accuracy_score, cohen_kappa_score

human = ["pass"] * 180 + ["fail"] * 20          # 200 human-labelled answers
lazy  = ["pass"] * 200                          # a judge that always says pass
judge = ["pass"] * 172 + ["fail"] * 8 + ["pass"] * 10 + ["fail"] * 10

for name, j in (("always-pass", lazy), ("judge", judge)):
    print(f"{name:>11}  accuracy {accuracy_score(human, j):.0%}"
          f"  kappa {cohen_kappa_score(human, j):.2f}")
# always-pass  accuracy 90%  kappa 0.00
#       judge  accuracy 91%  kappa 0.48
90%accuracy of a judge that never fails anything
0.00its kappa: no better than chance
0.48kappa of a judge at 91% accuracy that catches half the fails

Same 200 answers, 20 of them failures by human label; run on scikit-learn 1.9.1.

On a skewed set, accuracy flatters a judge, so measure agreement with Cohen’s kappa, which subtracts the agreement chance alone would give. Before a judge gates a release, have people label a few dozen to a hundred cases; the MT-Bench authors found a strong judge agreed with human raters over 80% of the time, about as often as humans agreed with each other (The Eval Stack).

BiasWhat happensFix
Positionthe first answer shown wins morerun both orders; a win counts only if both agree
Verbositylonger answers score highersay length is not a criterion; check conciseness
Self-preferencea judge favours its own stylenever judge a model with itself

Give it one yes/no question per criterion with a worked example, not “rate helpfulness 1 to 10”. Pin the judge model and the rubric version, and re-measure agreement when either changes: a judge upgrade silently moves every historical score.

Grade agents on the end state, k times

99.8%pass@4: at least one of 4 runs succeeds, at 0.8 per run
41%pass^4: all 4 succeed, same agent
0.604 → 0.383τ-bench retail, gpt-4o agent: pass^1 to pass^4

An agent takes a different path each run and can land on the right result by luck. Grade the world it left behind in code (exactly one refund row, the right amount), then check the path against constraints such as “no issue_refund before verify_identity”, and run each task several times. For anything customer-facing, gate on pass^k, how often it succeeds every time (Grading Agents). Keep cost and steps per task in the same report: a prompt change can hold success at 92% and double the cost.

Size the set for the change you need to see

Questions95% marginGood for
50±9.9 pointsa first look
100±7.0 pointsa smoke set
200±4.9 pointsa release gate
500±3.1 pointsa fine comparison
1,000±2.2 pointssmall effects

Margin on a pass rate near 0.85, 1.96 × √(p(1 − p) / n); the same table as RAG Evaluation.

A 3-point gain on 200 questions is inside the noise. Compare two systems question by question instead: count the questions only A gets right and only B gets right. Twelve disagreements split nine to three are not yet a result; thirty split twenty-five to five are. A launch gate should test the lower bound of the interval, not the point estimate (RAG Best Practices).

Build the golden set; it is the artifact

  1. Draw from trafficLogs, tickets, transcripts. Questions written from a document flatter search.
  2. Over-sample the strangeStratify by intent; keep about one case in five an edge case or an attack.
  3. Grow from incidentsEvery confirmed failure becomes a permanent case.
  4. Version itWith the prompt and the judge: a score means nothing without its set.
{"id": "refund-017",
 "source": "prod trace 2026-08-14, redacted",
 "input": "I was charged twice for order A-1042.",
 "checks": {"must_call": ["get_order"], "must_not_call": ["issue_refund"]},
 "rubric": ["explains the duplicate-charge policy"]}

Use the cheapest grader that can decide each check: a tool call happened or it did not, so code grades it; only tone and policy need a judge. Teams over-invest in harnesses and under-invest in these few hundred cases, which decide whether any harness tells the truth. Never write reference answers with the current model: they reward imitating it and punish a better successor.

Read production signals for how each one lies

SignalSuggestsMisleads when
Thumbs up / downsatisfactionfew vote, and the unhappy vote more
Re-ask within a minutethe answer missedit was a real follow-up
Escalationit could not resolvepolicy required a human
Sampled judge, 1–5%rubric quality liveit was never calibrated

Divide by tasks, not by votes, and size the judged sample before you set an alert: at 100 judged answers a day, a fixed 0.80 line on a true 0.85 fires by chance on about 7% of days (RAG Monitoring). Pin exact model ids, cluster live queries weekly for new topics, and send each confirmed failure back down the dashed arrow in the first figure.

Common mistakes
  • One number. An average over gates or tasks hides the one that failed.
  • Running each case once and reading a 3-point move as a win.
  • An uncalibrated judge gating releases, or scores compared across judge versions.
  • RAGAS v0.1 code (evaluate() with context_relevancy): that metric is gone in 0.4, and evaluate() returns one value per row, not a float.
Interview angle

“How would you know a prompt change made our assistant better?” Walk the three layers: a CI smoke set with code graders, the full golden set with a calibrated pairwise judge, then a canary and an A/B test on resolution with cost and escalations as guardrails, pre-registered. Close with sampled live judging and failures becoming cases. Model answers: when to trust an LLM judge and evaluating a non-deterministic agent.

Check yourself
Your judge agrees with human labels 92% of the time on a set where 90% of answers pass. Is it ready to gate releases?

Not on that number. A judge that passes everything scores 90% here. Compute Cohen’s kappa, look at how many of the human fails it catches, and test both answer orders for position bias.

Faithfulness averages 0.91 on 400 sampled answers, but the dashboard alert below 0.8 has never fired, even on a bad day. What might be wrong?

NaN rows. Replies with no claims score NaN; a plain mean of a list with a NaN is NaN, and NaN < 0.8 is false, so the alert cannot fire. Use a NaN-aware mean, show the NaN count and coverage, and alert on them too.

Agents A and B both pass 80% of single runs. A’s pass^4 is 0.41, B’s is 0.62. Which goes to customers?

B. A fails about one run in five on nearly every task, so it sits at 0.84 = 0.41. B’s failures cluster on a few tasks it fails reliably, which you can detect and route to a person.

Recap
  • Three layers: offline before ship, regression on every change, production all the time; production feeds the other two.
  • Pick metrics in pairs; count cost per resolved task.
  • Score retrieval with spans and recall@k before judging answers; use RAGAS 0.4.3 for claims, and count the NaNs.
  • A judge is a classifier: kappa against human labels, both orders, pinned and versioned.
  • Size the set for the change you need to see, and compare systems question by question.

Next: The Eval Stack: Offline to Online — CI gates, judge calibration, A/B tests and the traces every layer reads. For retrieval specifically, RAG Evaluation.

Enjoyed this post? The full curriculum has 149 sections, system design problems, and AI-reviewed practice runs.

See the full guide