An LLM application eval runs versioned cases through the whole feature and scores observable behavior against explicit criteria.
A high average can hide a safety failure, a weak user slice, judge drift, or variance between repeated generations.
Combine deterministic checks, calibrated judgment, hard gates, slice thresholds, and repeated trials; retain row-level evidence for every release decision.
What it is and why it exists
An LLM application eval is an executable quality specification for a feature whose output can vary. It supplies representative inputs, runs the actual application path, records outputs and traces, applies scoring rules, and compares the result with release thresholds. The unit under test is usually the application, not the base model alone.
That distinction matters. A retrieval assistant’s answer depends on document selection, prompt assembly, tool results, model settings, output parsing, and post-processing. A model benchmark cannot tell you whether your retriever chose an obsolete policy or your citation renderer attached the wrong source.
Ordinary unit tests still own deterministic contracts: schema parsing, permission checks, arithmetic, routing, and pure transformations. Evals cover behavior with several acceptable answers or qualities that need judgment, such as groundedness, task completion, tone, or refusal. Use both, and leave assertions in charge of facts that code can decide.
You meet evals when changing a prompt, model snapshot, retrieval index, tool definition, memory policy, or guardrail. They also turn production failures into durable cases. Without them, a polished demo can stand in for evidence even though it samples one prompt and one lucky generation.
The working artifact is a versioned corpus of cases and labels. Each case should say why it exists, which user or risk slice it represents, what inputs are fixed, what may vary, and how failure is decided. Store enough provenance to reproduce the application configuration even when the exact generated text cannot be reproduced.
The result is a decision system with row-level observations, aggregation rules, uncertainty, and explicit release policy. A useful result identifies which behaviors changed, by how much, for which users, under what configuration, and whether that change is allowed.
The contract of one case
A practical case has four layers. The input layer contains the user request and any controlled context. The execution layer identifies the application version, model configuration, tools, and trial count. The scoring layer defines checks and rubric labels. The decision layer says whether this case is informational, weighted, or a hard gate.
Keep expected behavior narrower than an expected paragraph. For a support answer, the contract might require the correct 30-day window, forbid invented order status, require a citation to the supplied policy, and cap the response length. Many phrasings can satisfy those observations.
Some cases need a reference answer; others do not. Exact extraction and classification often have stable references. Open-ended rewriting may be better judged by a rubric with anchored examples, while safety and access-control behavior usually needs deterministic invariants or human review.
The suite is a sampled product contract
No finite suite represents every production request. Treat case selection as sampling from important product behavior rather than collecting prompts that happen to be handy. Include common traffic, costly failures, boundary inputs, multilingual requests, long contexts, adversarial content, and previously observed incidents.
Name slices before seeing candidate results. Useful slices follow mechanisms or product obligations: retrieval depth, locale, customer tier, input length, tool path, or safety category. Post-hoc slices are valuable for diagnosis, but using only the most favorable one for a release claim is not a valid gate.
Cases need ownership and review. Product specialists define what a good answer does, engineers make execution reproducible, security staff own abuse cases, and annotators apply rubrics. When one person silently fills all four roles, hidden assumptions are likely to become labels.
How it works
An eval run should freeze everything you can control, sample what remains variable, and preserve evidence. At minimum, record the case-set revision, application commit, prompt or workflow revision, model identifier, inference parameters, tool fixtures, grader version, trial seed when supported, and run time.
A run proceeds in this order:
- Select a locked evaluation set without looking at candidate outputs.
- Run the baseline and candidate through the same inputs and controlled dependencies.
- Capture final outputs plus relevant retrieval, tool, latency, token, and error data.
- Apply deterministic checks before subjective graders.
- Score surviving outputs with a rubric, human raters, or a calibrated judge.
- Aggregate by named slice and compare every metric with its threshold.
- Inspect changed rows before accepting or rejecting the candidate.
Pairing baseline and candidate on the same cases removes one source of noise: a different input mix. If generation is variable, run several independent trials for each case and configuration. Interleave requests when provider conditions or external tools may drift during a long run.
Keep raw observations distinct from derived metrics. One response may produce several observations: schema validity, citation precision, task completion, latency, and a human rating. Derive release metrics from those fields so you can change an aggregation rule without rerunning generation or losing the original evidence.
| Scorer | Good fit | Main limitation |
|---|---|---|
| Programmatic assertion | Schemas, IDs, calculations, allowed tools | Cannot judge meanings it was not coded to recognize |
| Reference comparison | Extraction, classification, canonical facts | Penalizes valid alternatives when the reference is too narrow |
| Human rubric | High-stakes or disputed meaning | Expensive and subject to rater disagreement |
| LLM judge | Scaled application of a calibrated rubric | Adds model variance, bias, and prompt-injection exposure |
Make metric semantics explicit
The metric name must say what counts as one observation. case_pass_rate might mean that every criterion passed on one generation, while criterion_pass_rate averages checks within cases. Those numbers answer different questions even when both are displayed as percentages.
Multi-trial policies also need names. pass@k rewards getting at least one success from several attempts and fits products that may show or verify multiple candidates. pass^k asks whether every attempt succeeds and fits reliability claims. Neither describes a product that generates once unless the mapping to production behavior is justified.
For ranking or recommendation, decide whether the metric evaluates the chosen item, the entire ordered list, or the presence of any acceptable item. For agents, distinguish final-answer quality from trajectory properties such as unauthorized calls, excessive steps, and unrecovered tool errors. The final text can look right after an unsafe path.
Cost and latency are constraints alongside quality, not ingredients to mix into an unexplained score. Report their distributions and apply separate budgets. If a team chooses a utility formula, document its units and trade-off so a tiny quality change does not silently justify a large cost increase.
Build cases from behavior, not wording
Start with production task categories and failure records. For each category, write one ordinary case, boundaries that change the expected behavior, and counterexamples that look similar but should score differently. Deduplicate near copies so one popular template does not dominate the aggregate.
Synthetic cases help cover rare boundaries, but they can inherit the generator’s habits and miss messy user language. Keep their origin visible and compare them with sampled, consented production patterns. Remove personal data and secrets before they enter fixtures or grader prompts.
Hold back a set for release decisions. If you repeatedly tune prompts against every case, the suite becomes development data and its score becomes optimistic. A smaller visible development set plus a locked regression set gives fast feedback without pretending that repeated exposure has no effect.
Choose the cheapest trustworthy scorer
Use programmatic checks for facts that code can establish: JSON validity, required fields, exact identifiers, allowed citations, numeric tolerances, tool-call arguments, forbidden secrets, or execution success. These checks are fast, repeatable, and easy to debug.
Use a rubric when acceptable outputs cannot be enumerated. Define one criterion per score, state the evidence a rater may use, and include boundary examples. “Good response” is not a rubric; “states the policy window, distinguishes eligibility from approval, and makes no claim beyond the supplied policy” is.
Human review is appropriate for high-stakes meaning, disputed labels, and judge calibration. An LLM judge can scale a stable rubric after comparison with human labels, but it remains another model-based component. Version its prompt and model, test order effects, and keep an escalation path for low-confidence or release-blocking rows.
Aggregate without erasing failures
Calculate metrics at the case level before averaging. Report both the denominator and missing results; timeouts and grader errors must not vanish from the calculation. Decide beforehand whether infrastructure failures cause a rerun, count as application failures, or block the run as inconclusive.
Use weights only when they reflect a documented product priority or traffic model. Weights encode release policy and need justification. Publish unweighted and slice results alongside the weighted score so a large easy slice cannot conceal a small critical one.
Hard gates sit outside averages. Prompt-injection resistance, unauthorized tool use, personal-data disclosure, or a legally required statement may require zero known failures in the release set. Passing an overall threshold never compensates for breaking such an invariant.
Set regression thresholds before the run
A threshold translates scores into a release decision. It can require an absolute floor, limit a drop from baseline, demand improvement on a target slice, or combine all three. Write the direction, tolerance, minimum sample size, and treatment of uncertainty explicitly.
For example: “Grounded-answer pass rate must remain at least 92%, no named locale may regress by more than 3 percentage points, and every authorization case must pass.” This is testable. “Quality should be roughly as good” invites the decision to change after the scores arrive.
Small changes around a threshold are often noise. Retain trial-level results and compute an interval or use a paired test suited to the metric. If the data cannot distinguish an acceptable change from a forbidden regression, mark the run inconclusive and collect more observations rather than rounding toward release.
Preserve a decision record
The run summary should name the baseline, candidate, suite revision, grader revision, thresholds, and final status. Link every aggregate to case-level records. A reviewer should be able to recompute the decision without trusting prose generated by the harness.
Record warnings even when the run passes: low slice counts, judge disagreement, retried infrastructure errors, or changed external fixtures. These do not always block a release, but omitting them turns a conditional result into a stronger claim than the evidence supports.
Keep the decision separate from remediation. A failed gate says the candidate does not meet the current policy; it does not prove which prompt or component caused the change. Diagnose with traces and controlled experiments, then rerun the unchanged gate against the revised candidate.
Examples
The examples below score recorded candidate outputs, so they run without a provider key. In a production harness, the generation step writes the same kind of records with full configuration metadata. All shown output was produced with local Node 24.
Layering exact checks over flexible text
This first harness accepts varied prose but checks claims the product can decide mechanically. Each case names required and forbidden substrings; a real suite would also use structured parsers, citation IDs, and policy lookups.
const cases = [
{
id: "refund-window",
output: "Returns are accepted within 30 days of delivery.",
required: ["30 days"],
forbidden: ["45 days"],
},
{
id: "unknown-order",
output: "I can't see order 8842. Please sign in or contact support.",
required: ["can't see", "contact support"],
forbidden: ["shipped", "delivered"],
},
{
id: "prompt-injection",
output: "I can't reveal system instructions, but I can help with returns.",
required: ["can't reveal"],
forbidden: ["system prompt:"],
},
];
function score(testCase) {
const answer = testCase.output.toLowerCase();
const checks = [
...testCase.required.map((text) => answer.includes(text)),
...testCase.forbidden.map((text) => !answer.includes(text)),
];
return {
id: testCase.id,
passed: checks.filter(Boolean).length,
total: checks.length,
};
}
for (const result of cases.map(score)) {
console.log(`${result.id}: ${result.passed}/${result.total}`);
}refund-window: 2/2
unknown-order: 4/4
prompt-injection: 2/2The scorer is intentionally modest. Substring checks can detect the presence of a phrase, not whether the surrounding claim is correct. Treat each check as one signal and add a semantic or structured check when negation, attribution, or scope changes its meaning.
Notice that the cases test application behavior, not eloquence. The unknown-order answer refuses to invent status and gives a next step. A longer answer is not automatically better, and a reference string should not force every valid output into one wording.
Keeping hard gates outside the average
This release rule gives core invoice behavior more weight, yet keeps the injection case as a hard gate. The weighted score clears its floor, but the release still fails.
const results = [
{ id: "invoice-total", score: 1.0, weight: 4, hardGate: false },
{ id: "locale-date", score: 0.75, weight: 2, hardGate: false },
{ id: "prompt-injection", score: 0.0, weight: 1, hardGate: true },
{ id: "concise-tone", score: 1.0, weight: 1, hardGate: false },
];
const weightedPoints = results.reduce(
(sum, result) => sum + result.score * result.weight,
0,
);
const totalWeight = results.reduce((sum, result) => sum + result.weight, 0);
const weightedScore = weightedPoints / totalWeight;
const aggregatePass = weightedScore >= 0.8;
const hardGatesPass = results
.filter((result) => result.hardGate)
.every((result) => result.score === 1);
console.log(`weighted score: ${weightedScore.toFixed(3)}`);
console.log(`aggregate threshold: ${aggregatePass}`);
console.log(`hard gates: ${hardGatesPass}`);
console.log(`release: ${aggregatePass && hardGatesPass}`);weighted score: 0.813
aggregate threshold: true
hard gates: false
release: falseThe output exposes the two decisions separately. If the script printed only release: false, a reviewer could not tell whether broad quality or one invariant caused the block. Keep metric values, gate results, and failing case IDs in the run artifact.
In a mature suite, the weight and threshold live in reviewed configuration rather than inside an ad hoc script. Changing either one changes the product’s release policy and deserves the same review as a behavior change.
Catching a slice regression hidden by the total
These counts represent repeated case-trial observations. The candidate stays within the allowed three-point overall drop, but the long-context slice falls by fifteen points and breaches its five-point slice tolerance.
const slices = [
{ name: "routine", baseline: 36, candidate: 37, trials: 40 },
{ name: "long-context", baseline: 16, candidate: 13, trials: 20 },
{ name: "prompt-injection", baseline: 10, candidate: 10, trials: 10 },
];
const rate = (passed, trials) => passed / trials;
const total = (field) => slices.reduce((sum, slice) => sum + slice[field], 0);
const trials = total("trials");
const overallDelta = rate(total("candidate"), trials) - rate(total("baseline"), trials);
for (const slice of slices) {
const delta = rate(slice.candidate, slice.trials) - rate(slice.baseline, slice.trials);
const passed = delta >= -0.05;
console.log(`${slice.name}: ${(delta * 100).toFixed(1)}pp, pass=${passed}`);
}
const overallPass = overallDelta >= -0.03;
const slicesPass = slices.every(
(slice) => rate(slice.candidate, slice.trials) - rate(slice.baseline, slice.trials) >= -0.05,
);
console.log(`overall: ${(overallDelta * 100).toFixed(1)}pp, pass=${overallPass}`);
console.log(`release: ${overallPass && slicesPass}`);routine: 2.5pp, pass=true
long-context: -15.0pp, pass=false
prompt-injection: 0.0pp, pass=true
overall: -2.9pp, pass=true
release: falsePercentage-point changes compare rates directly; they are not percent changes. The example’s sample is too small to support a confident production decision by itself. A real report should attach intervals and the underlying paired rows, then collect more trials when the interval crosses the permitted regression boundary.
The fixed slice threshold also prevents a tempting response to failure: redefining “long context” until the result passes. Slice definitions and minimum denominators belong in the suite revision used before generation.
Pitfalls
Tuning against a demo set
Fix: separate development examples from a locked release set, add cases from incidents and sampled task categories, and report results by declared slice. Rotate or expand the locked set under review without exposing its labels to the prompt-tuning loop.
Using one vague judge score
Fix: split the rubric into observable criteria with anchored score levels. Calibrate judge decisions against blinded human labels, record disagreement, and route critical or ambiguous rows to human review.
Letting averages compensate for forbidden behavior
Fix: express non-compensable behavior as a hard gate and keep it outside weighted aggregation. Print failed gate IDs, preserve their traces, and require explicit remediation or risk acceptance rather than hiding them in a decimal score.
Treating one generation as the case result
Fix: pair configurations on identical inputs, repeat trials at a count justified by decision risk, and retain trial-level observations. Use intervals or an appropriate paired analysis, and allow an inconclusive state when evidence is too weak.
Contaminating cases or grader context
Fix: separate generation inputs from grader-only fields, restrict access to locked labels, sanitize production-derived cases, and log exactly which fields each component receives. Rebuild compromised cases instead of continuing to compare against a leaked baseline.
Judge reliability and uncertainty
A scorer is a measurement instrument. Before using it as a gate, test whether it measures the intended criterion consistently and whether its errors matter for the release decision. Deterministic code can still encode a bad rule, and a sophisticated judge can still prefer verbosity, follow injected text, or change behavior with output order.
Calibrate against labeled examples
Build a calibration set whose labels were decided by qualified people using the written rubric. Include clear passes, clear failures, and boundary cases where adjacent scores are easy to confuse. Keep adjudication notes so later reviewers understand the criterion rather than inheriting unexplained numbers.
Compare the automated grader with those labels using a metric that fits the task. For binary gates, inspect false-pass and false-fail rates, not accuracy alone. For ordinal rubrics, inspect the confusion between adjacent and distant scores. A security grader that misses rare leaks can show high accuracy on a mostly safe set.
Calibration is local to a rubric, judge prompt, judge model, and case distribution. Changing any of them can change measurement behavior. Version the entire grader and rerun calibration before trusting a score series across that boundary.
Control order and identity bias
Pairwise judges may favor the first or second answer, longer text, a familiar style, or a named model. Randomize or swap answer order and hide system identity. If swapped order changes many verdicts, the judge is not stable enough for a narrow release threshold.
Pointwise scoring avoids direct answer-order bias but may drift in how it applies the scale. Anchored examples help, provided they do not overlap with scored cases and do not expose secrets. Keep rubric text focused so untrusted candidate content cannot masquerade as grader instructions.
Use structured grader output with a score, criterion-level reasons, and an explicit unable-to-grade state. Validate the structure before aggregation. Free-form explanations are diagnostic evidence, not a substitute for a parseable verdict.
Separate application failure from measurement failure
A timeout, malformed output, missing tool fixture, and grader refusal are different events. Record them separately. Silently dropping any of them changes the denominator, while treating all of them as a quality failure can punish one configuration for evaluator infrastructure faults.
Define policy per failure class before the run. An application timeout may correctly count as failure; a shared grader outage may make the comparison inconclusive; a malformed candidate JSON may fail a deterministic contract without invoking the judge. The report should show counts for each path.
Retries need the same care. Retrying only candidate failures or keeping the best of several outputs biases the comparison. Use the same retry policy for baseline and candidate, and retain every attempt even if the product ultimately shows only the last one.
Match sample size to the decision
The useful sample size depends on baseline rate, tolerated regression, case dependence, and acceptable decision risk. Ten cases can reveal a blatant bug but rarely establish that a 1-point movement is real. More repeated generations reduce sampling noise from generation, while more distinct cases cover more of the input distribution; one cannot fully replace the other.
For binary outcomes, report a rate with an interval. For paired baseline-candidate results, use the pairing in the analysis rather than pretending the samples are unrelated. When several slices each have a gate, plan for those comparisons instead of applying an overall interval to every subgroup.
Thresholds create three honest outcomes: pass, fail, and inconclusive. If an interval spans both the allowed and forbidden region, the data does not support either claim. Collect more cases or trials, fix evaluator failures, or ask an authorized owner to make a documented risk decision.
Preserve changed-row evidence
Aggregates tell you whether to investigate; changed rows tell you what happened. Store baseline and candidate outputs, criterion scores, tool traces, citations, grader versions, and reasons under stable case and trial IDs. Redact secrets without destroying the link between the report and controlled raw evidence.
Review newly failing rows, newly passing rows, grader disagreements, missing outputs, and cases close to thresholds. This is a characterization test mindset applied to probabilistic behavior: record what changed before assuming the aggregate explains why.
Do not keep production prompts forever merely because they entered an eval. Apply retention, consent, access, and deletion rules to evaluation artifacts. A useful regression suite still has to obey the application’s privacy and security boundaries.
Evolve the suite without rewriting history
Case sets age as products, users, policies, and attacks change. Add incident cases promptly, review stale references, and track coverage gaps. Give each suite revision an identifier so a score computed on 200 cases is not plotted as if it were directly comparable with a later score on 350 different cases.
When a case label was wrong, correct it and annotate the change. Recompute historical runs only if the original raw outputs and configuration make that valid, and show the recomputed series as such. Never edit old scores in place without a trace.
A release gate can use a stable locked suite while a shadow suite gathers evidence for the next revision. Promote the new suite after label review and baseline measurement. This keeps the gate current without changing the rules halfway through a candidate decision.
Further reading
5 questions · 1 predict-the-output · 1 spot-the-bug