Evaluation · Checklist
A Practical Checklist for Evaluating LLM Outputs
A repeatable way to decide whether your LLM feature is getting better or worse: define quality, build a representative eval set, layer cheap and expensive checks, and read results honestly.
- Author
- Jordan Lee (sample)
- Published
- Reading time
- 8 min read
- groundedness
- format validity
Most LLM features ship after someone tries a dozen prompts by hand and decides the output “looks good.” That works until the first prompt tweak, model upgrade, or data change, when nobody can say whether things improved or regressed. Evaluation replaces that impression with a repeatable measurement.
This checklist is deliberately practical. It assumes you have a working feature and want a lightweight evaluation loop you can run locally and in CI, then grow over time.
Define what “good” means first
Before writing any test, write down the qualities that matter for this specific feature. “Accurate and helpful” is too vague to grade. Break quality into properties that a person, or a script, could check on a single output.
- Correctness: does the answer agree with the source or reference answer?
- Grounding: is every claim supported by the provided context, with no invented details?
- Format: does the output parse, match the schema, and respect length limits?
- Scope: does the feature decline questions it shouldn't answer?
- Safety and policy: does it avoid content your product must never produce?
Rank these. When two properties conflict — for example, completeness versus brevity — you need to know which wins before you can interpret a score.
For each property, write a pass condition concrete enough that two reviewers would usually agree. “Mentions the 30-day refund window when the question is about refunds” is gradable; “explains the refund policy well” is not. If you can't phrase a pass condition, that property probably needs human review rather than an automated check, and it's better to know that up front.
Build a representative eval set
An eval set is a versioned collection of inputs plus whatever you need to judge the output: a reference answer, required phrases, forbidden phrases, or a flag saying the feature should refuse. Store it as JSONL next to your code so changes are reviewed like any other change.
{ "id": "refund-window-01", "question": "Can I still get a refund 20 days after purchase?", "must_include": ["30 days"], "must_not_include": ["guaranteed"], "should_refuse": false, "tags": ["billing", "policy"]}- Pull real inputs from logs (with sensitive data removed) rather than inventing tidy examples.
- Cover the common cases, then deliberately add edge cases: ambiguous questions, out-of-scope requests, empty or very long inputs.
- Add every production bug you fix as a new case so it can't quietly return.
- Tag cases by category so you can see where quality changes, not just whether the overall number moved.
Layer checks from cheap to expensive
Deterministic checks
Start with assertions a script can evaluate exactly: the output parses as JSON, required fields exist, a required phrase appears, cited IDs are a subset of retrieved IDs, length is under a limit. These checks are fast, free, and unambiguous, and they catch a surprising share of regressions.
Model-graded checks
For properties like grounding or relevance, a second model call with a narrow rubric can act as a grader. Keep the rubric specific (“Is every factual claim in the answer supported by the passages? Answer yes or no, then list unsupported claims.”) and calibrate it: grade a sample by hand and check that the model grader agrees with you often enough to be useful.
Human review
Humans remain the reference for nuanced quality. You don't need to review everything; review a rotating sample, plus every case where automated checks disagree with each other. Record decisions so they can become new eval cases.
A minimal eval harness
The harness below uses pytest so each case becomes an individually reported test. It assumes your application exposes an answer_question function that returns the text plus retrieval metadata. Adapt the names to your code.
import jsonfrom pathlib import Path import pytest # Your application entry point. Assumed to return an object with# .text, .refused, .cited_ids and .retrieved_ids attributes.from app.answer import answer_question CASES_PATH = Path("evals/support_cases.jsonl")CASES = [json.loads(line) for line in CASES_PATH.read_text().splitlines() if line.strip()] @pytest.mark.parametrize("case", CASES, ids=lambda c: c["id"])def test_support_answer(case: dict) -> None: result = answer_question(case["question"]) if case.get("should_refuse"): assert result.refused, "expected a refusal for an out-of-scope question" return text = result.text.lower() for phrase in case.get("must_include", []): assert phrase.lower() in text, f"missing required phrase: {phrase!r}" for phrase in case.get("must_not_include", []): assert phrase.lower() not in text, f"contains forbidden phrase: {phrase!r}" # Grounding: the answer may only cite passages that were actually retrieved. assert set(result.cited_ids) <= set(result.retrieved_ids), "cited an unretrieved source"Read results without fooling yourself
LLM outputs vary between runs, so a single pass rate is a noisy signal. A change from, say, 82% to 85% on a small set may be noise. Run flaky cases several times, compare per-tag results, and always read the actual failing outputs before celebrating or panicking.
- Pin the model version, temperature, and prompt version for each run, and record them alongside results.
- Diff failures between runs: which cases newly fail and which newly pass matters more than the aggregate.
- Watch for eval-set overfitting. If you tune prompts against the same cases for weeks, hold out a set you don't look at during iteration.
Track latency and token usage alongside quality in the same report. A prompt change that fixes two cases but doubles the average response length is a trade-off the team should decide on explicitly, not discover later on an invoice or a latency dashboard.
The checklist
- Quality properties are written down and ranked for this feature.
- The eval set is versioned, sourced from real inputs, and tagged by category.
- Edge cases and out-of-scope requests are represented, not just happy paths.
- Deterministic checks run on every commit.
- Model-graded checks use a narrow rubric and have been spot-checked against human judgment.
- Model, prompt, and configuration versions are recorded with every eval run.
- Results are compared per case and per tag, not only as one aggregate number.
- Every fixed production bug becomes a new eval case.
Examples and figures in this article are illustrative. Adapt thresholds and tooling to your own system and data.