LLM Evals From Zero  Chapter 4 of 9
01 / 22
Chapter 4 · Layers 1 and 2, Implemented

Scoring Without an LLM

Deterministic and reference-based evaluation, implemented. Layers 1 and 2 from Chapter 2, run against the golden dataset from Chapter 3. No judge in this chapter: every score here is computed by code you can read, test, and rerun for free.

By the end, you can answer four questions

  • What can be scored exactly, with no model in the loop?
  • What does each metric actually compute: formula, inputs, failure mode?
  • How do per case scores become a report you can diff?
  • How do you know the scorers themselves are correct?

How this lecture runs

  • 20 to 25 minutes, 22 slides: the first scoring code of the course
  • Section 1: Layer 1, deterministic checks, slides 5 to 11
  • Section 2: Layer 2, reference-based scoring, slides 12 to 17
  • Section 3: the baseline run, paired diffs, and trusting the scorers
  • Every metric slide shows its formula and a worked numeric example
Input: golden_v1.jsonl, 500 cases, frozen in Chapter 3 Output: report.json + failures.jsonl Judge calls in this chapter: zero
SG
Saloni GargSenior Machine Learning Engineer, Adobe
SLIDE 01 / 22PACE 0:00
Slide 02 · The deliverable

One command in, two artifacts out

terminal · the runner, end to end
$ python -m evals.run \ --dataset datasets/golden_v1.jsonl \ --config runs/candidate.yaml # reads golden_v1.jsonl (500 cases, Ch 3) # runs candidate system (pinned, slide 4) # scores Layers 1 and 2 (slides 5 to 17) wrote runs/2026-10-30_cand/raw_outputs.jsonl wrote runs/2026-10-30_cand/report.json wrote runs/2026-10-30_cand/failures.jsonl

The two artifacts that matter

  • report.json: aggregated scores per capability, with n per slice. The diffable summary, slide 20.
  • failures.jsonl: one record per failed case, with the scorer name and a reason string. The debugging surface, slide 20.

Where we are in the course

  • Chapter 2: the architecture exists. Four layers, capabilities, policies.
  • Chapter 3: the dataset exists. golden_v1.jsonl, 500 cases, linted and frozen.
  • Today: we run it. Generation, scoring, aggregation, and the first baseline.

The scope line for this chapter

Layers 1 and 2 answer questions with objective or reference based answers. Faithfulness and helpfulness are not in scope yet, because no string comparison can measure them. That boundary is the whole reason Video 5 exists, and slide 8 draws it precisely.

SLIDE 02 / 22PACE 1:05
Slide 03 · Runner architecture

Seven components, one cache boundary

Run config
Pinned model, prompt, retrieval settings. Slide 4
→
Dataset loader
Reads and schema validates golden_v1.jsonl
→
Generator
Runs the candidate on all 500 cases. The only step that calls the model
→
Raw output cache
raw_outputs.jsonl: every output, verbatim, with latency and tokens
Scorers
Layers 1 and 2 read cached outputs. Pure functions, no model calls
→
Aggregator
Per capability micro and macro averages, with n. Slide 17
→
Report
report.json plus failures.jsonl. Slide 20

The key design decision

Separate generation from scoring. Cache everything in between.

Generation is the expensive, slow, nondeterministic step. Scoring is cheap, fast, and deterministic. So when you fix a scorer bug, and you will, you rescore the cached outputs instead of paying to regenerate 500 model calls. A rescore takes seconds and produces a report that differs from the old one only where the scorer changed. Without the cache, every scorer fix costs a full regeneration, and worse, the new outputs are not the same outputs, so you can no longer attribute the score change to the scorer.

SLIDE 03 / 22PACE 2:10
Slide 04 · Run config and reproducibility

Pin everything, record everything

runs/2026-10-30_cand/manifest.json · written by the runner
{ "run_id": "2026-10-30_cand", "dataset": "golden_v1.2", "model": "support-llm@2026-09-15", "prompt_sha256": "9f2ca41b77e0", "temperature": 0.0, "max_tokens": 512, "retrieval": {"k": 5, "index": "docs_v42"}, "scorer_version": "scorers 1.3.0", "seed": 17 }

What gets pinned, and why

FieldWhy it is pinned
model snapshotA floating model alias can change under you mid comparison
prompt hashThe prompt is code. The hash proves which text produced this run
temperature 0Minimizes sampling variance between runs
max tokensTruncation is a behavior. It must not drift silently
retrieval k, indexRetrieval changes are generation changes. Index version is part of the system

Temperature 0 reduces variance. It does not guarantee identical outputs. Provider side batching, mixture of experts routing, and floating point nondeterminism can still flip individual generations at temperature 0. That is exactly why the manifest and the raw output cache exist: you do not assume reproducibility, you record what actually ran, so any report can be traced back to the exact outputs that produced it.

SLIDE 04 / 22PACE 3:15
SECTION 1 Layer 1 · Deterministic Checks SLIDES 05 TO 11 · NO MODEL IN THE LOOP
Slide 05 · The scorer interface

One contract for every scorer

evals/scorers/base.py · the whole contract
from dataclasses import dataclass @dataclass class Score: value: float # 0.0 to 1.0, or a raw measure passed: bool # vs. this scorer's threshold reason: str # required, human readable metadata: dict # raw values for debugging def score(case: dict, output: dict) -> Score: """Pure function of (case, output). No model calls. No network. No clock.""" ...

What the contract buys you

  • Composable: the runner treats every scorer identically. Adding a scorer is adding one function and one registry entry.
  • Unit testable: a pure function of two dicts can be tested with fixtures, no mocking, no API keys. Slide 21.
  • Attributable: value, passed, and reason are recorded per case per scorer, so every aggregate in the report can be drilled back to individual decisions.
  • No scorer calls the model. The moment a scorer calls an LLM it stops being deterministic, stops being free, and belongs to Layer 3 in Video 5, with a different contract.
SLIDE 05 / 22PACE 4:20
Slide 06 · Layer 1 · Structural validity

Is the output even the right shape?

The cheapest failures to catch are structural: the answer cannot be parsed, ignores the requested format, or blows the token budget. Check shape before you check content.

CheckMethodFailure it catches
JSON validityjson.loads, then jsonschema validateProse wrapped around JSON, trailing commas, missing required fields, wrong types
Parse rateparsed cases / total casesFormat regressions at the suite level. Example run: 493 / 500 = 98.6%
Exact bullet countcount lines matching the bullet markerInstruction drift: asked for exactly 3 bullets, got 5 and a paragraph
Token budgettokenizer count vs. case budgetRunaway verbosity that passes eyeball review and doubles cost
Language checkscript and language detection on the textAnswers in the wrong language after a prompt or model change

jsonschema: use it when structure is a contract

Validates types, required fields, enums, nesting, and additional properties, and returns which constraint failed as the reason string. If downstream code consumes the output, this is the check.

Regex: use it only for surface patterns

Fast for spotting a marker or a pattern, but it cannot express types or nesting, and it fails silently on the malformed inputs you most need to catch. A regex that says the JSON looks fine is how broken payloads reach production.

SLIDE 06 / 22PACE 5:25
Slide 07 · Layer 1 · Contains checks

Contains checks, done right

must_contain and must_not_contain from the Chapter 3 schema are substring checks, and raw substring matching is wrong often enough to matter. Normalize both sides first, identically.

Raw text
Output and expected string, as written
→
Lowercase
Case is presentation, not content
→
Unicode normalize
NFKC: full width and composed forms collapse to one representation
→
Strip punctuation
"$30." and "30" become comparable
→
Collapse whitespace
Newlines and double spaces become single spaces
→
Substring match
Only now does in / not in mean anything
THE PITFALL · EXPECTED STRING IS "30 DAYS" · ALL THREE ARE CORRECT ANSWERS
Model wroteRaw substringAfter normalizationWhat normalization cannot fix
"30 days"MATCHMATCHThe easy case. It is also the rarest in the wild
"30 Days."MISSMATCHCase and punctuation. Fixed by the pipeline above
"thirty days"MISSMISSNumber words. Needs explicit numeral canonicalization (thirty to 30) as a tested pipeline step, or the case belongs to reference scoring
"a month"MISSMISSA paraphrase, not a formatting variant. No string pipeline fixes this. That is a semantic judgment, Video 5
SLIDE 07 / 22PACE 6:30
Slide 08 · Layer 1 · Citation checks

Citations: three checks, one hard boundary

1 · It parses

Extract citation markers from the output with a strict pattern. A malformed marker is a format failure, scored here, not a mystery.

2 · It resolves

Every cited doc ID must exist in the index at the pinned index version. A citation to a deleted or invented doc is a hard fail.

3 · It is expected

Compare the cited set against expected_sources from the case. This is a set operation, fully deterministic.

Per case citation precision and recall

precision = |cited ∩ expected| / |cited|   ·   recall = |cited ∩ expected| / |expected|
Worked exampleSetsArithmeticResult
Case cites 3 docs, case expects 2, both expected docs are cited, plus 1 extracited = {refunds, billing, sso}
expected = {refunds, billing}
precision = 2 / 3
recall = 2 / 2
precision 0.667
recall 1.000

Read it as: the answer grounded itself in everything required (recall 1.000) but padded with one unneeded source (precision 0.667). Two different fixes, two different numbers.

The boundary: existence is deterministic. Whether the cited doc actually supports the claim is a semantic judgment, and it is a judge task in Video 5. Teams blur this line constantly, then wonder why their deterministic citation score says 98 percent while users keep finding unsupported claims.

SLIDE 08 / 22PACE 7:35
Slide 09 · Layer 1 · Tool call validity

Tool calls are data. Validate them like data.

For agents, the most consequential output is often not the prose, it is the tool call. Every part of its validity is checkable without a model.

one tool call from an agent trace · example output
{ "tool": "refund_lookup", "arguments": { "order_id": "A-1042", "window_days": "30", // string, schema says integer "force": true // not a declared parameter } } # verdict: FAIL · 2 violations, both deterministic
CheckMethod
Tool name in allowlistSet membership against the tools registered for this agent. An unregistered name is a hallucinated capability
Arguments validatejsonschema against the tool's declared argument schema: required fields present, enums respected
Argument types correctSchema types, strictly. "30" the string is not 30 the integer, and coercing it silently hides model errors
No invented parametersSchema declares additionalProperties: false. An extra key like force fails validation instead of reaching the tool

Why this matters more than prose quality: a wrong tool call executes. It refunds the wrong order or queries the wrong account, in production, at machine speed. Validity here is not pedantry, it is the last deterministic checkpoint before a side effect. Whether the agent picked the right tool for the task is a separate question: that is trajectory scoring in Video 7.

SLIDE 09 / 22PACE 8:40
Slide 10 · Layer 1 · Operational metrics

Record the run, not just the answers

The generator records latency, tokens, and errors per case into the raw output cache. These are deterministic measurements of the run itself, and they belong in the same report as quality.

OPERATIONAL SUMMARY · EXAMPLE RUN · 500 CASES · LABEL: EXAMPLE RUN, NOT A BENCHMARK
MetricHow it is computedExample run
Latency p50Median per case wall clock, generation only1.2 s
Latency p95The value 95 percent of cases finish under2.6 s
Latency meanArithmetic mean, recorded but never reported alone1.4 s
Tokens per caseMean input and output tokens from the API response812 in · 214 out
Cost per case(812 × price_in + 214 × price_out) / 1,000,000$0.0043
Timeout ratetimed out cases / total = 3 / 5000.6%
Retry ratecases needing at least 1 retry / total = 12 / 5002.4%

Why p50 and p95, not only the mean

The mean of 1.4 s hides the tail: one case in twenty waits past 2.6 s, and users remember the tail. A prompt change that adds a reasoning paragraph often leaves the mean nearly flat while p95 climbs. Percentiles are where operational regressions actually live.

Timeouts and retries are scores too

A timed out case produced no answer, so quality scorers skip it, and a suite that only reports quality will call a slow, flaky candidate an improvement. The report carries timeout and retry rates next to the quality numbers, and the gate in Video 8 can block on them.

SLIDE 10 / 22PACE 9:45
Slide 11 · Layer 1 · Refusal as classification

Refusal is a classifier. Score it like one.

Each case carries expected_behavior from Chapter 3. The scorer detects the output's behavior from deterministic markers (refusal phrasing, absence of an answer body, zero citations on an answer expected case) and compares. Example baseline run, all 500 cases:

Confusion matrix · example baseline run · n = 500

Detected: refuse
Detected: answer
Should refuse · 120
106correctly refused
14answered anyway
Should answer · 380
19refused a supported question
361answered

The two error cells are different incidents. 14 answered anyway is a grounding and safety problem. 19 false refusals is a usefulness problem. One number would merge them.

The three rates, with arithmetic

missed refusal rate14 / 120 = 11.7%
Of the cases that required a refusal, how often the system answered anyway. The high risk direction.
false refusal rate19 / 380 = 5.0%
Of the cases with supporting evidence, how often the system refused anyway. The silent usefulness leak.
refusal correctness(106 + 361) / 500 = 467 / 500 = 93.4%
Behavior matched the spec, either direction. Report it only alongside the two rates above.

Note the denominators: each rate is computed on its own population. Missed refusals over all 500 cases would read 2.8 percent and look harmless. Rates are only meaningful against the cases that could have produced them.

SLIDE 11 / 22PACE 10:50
SECTION 2 Layer 2 · Reference-Based Scoring SLIDES 12 TO 17 · COMPARE AGAINST GROUND TRUTH
Slide 12 · Exact match

Exact match: the right tool, in a narrow band

EM = 1 if normalize(prediction) == normalize(reference), else 0   ·   suite score = mean of EM over cases

Normalization is the slide 7 pipeline, applied to both sides before comparing. Exact match on raw strings measures formatting luck, not correctness.

Where exact match is correct

  • Classifications: the predicted label either is the expected label or it is not. Refusal detection on slide 11 is this shape.
  • IDs and codes: order IDs, SKUs, error codes, doc IDs. There is exactly one right string.
  • Short factual answers: "30 days", a version number, a date. The answer space is effectively closed.

The counterexample

AnswerCorrect?EM
"You can request a refund within 30 days of purchase."YES1
"Refunds are available for 30 days after checkout."YES0
"File your refund request in the first 30 days."YES0

Reference is the first row. All three answers are correct, two score zero. On prose, exact match mostly measures how the reference author happened to phrase it, and a suite built on it punishes every correct paraphrase.

SLIDE 12 / 22PACE 11:55
Slide 13 · Layer 2 · Token F1

Token F1, worked end to end

precision = overlap / predicted tokens  ·  recall = overlap / reference tokens  ·  F1 = 2 · P · R / (P + R)
SideTokens after normalizationCount
Referenceyou · can · request · a · refund · within · 30 · days · of · purchase10
Predictionyou · can · request · a · refund · during · 30 · days · after · any · purchase · online12
Overlapyou · can · request · a · refund · 30 · days · purchase  (multiset intersection: within, of missing on one side; during, after, any, online extra on the other)8

Step 1 · Precision

8 / 12 = 0.667

Of the tokens the model produced, two thirds appear in the reference. The rest is added wording.

Step 2 · Recall

8 / 10 = 0.800

Of the tokens the reference needed, 80 percent are present. Nothing factual was dropped except connective wording.

Step 3 · Harmonic mean

2 × 0.667 × 0.800 / (0.667 + 0.800)
= 1.067 / 1.467 = 0.727

The harmonic mean punishes imbalance: a scorer gaming precision by answering in three words cannot hide the recall collapse.

What F1 does not know: the missing tokens here are harmless connectives, and F1 cannot tell them apart from a missing "not". Token overlap is a similarity signal with partial credit, useful for tracking drift across versions of the same system. It is not a correctness verdict: that distinction is why F1 sits in Layer 2 as a diagnostic, and why the gate in Video 8 never blocks on F1 alone.

SLIDE 13 / 22PACE 13:00
Slide 14 · Layer 2 · BLEU and ROUGE

BLEU and ROUGE, honestly

Both are n-gram overlap metrics, built for a specific job in a specific era. Using them well starts with knowing what that job was.

BLEU · built for machine translation

BLEU = BP × exp( Σn wn · log pn )
  • pn: precision of n-grams up to 4 against reference translations
  • BP: brevity penalty, punishes outputs shorter than the reference
  • Designed as a corpus metric: meaningful averaged over thousands of sentences, unstable and noisy on single answers

ROUGE · built for summarization

ROUGE-N = matched n-grams / reference n-grams
  • Recall oriented: how much of the reference summary's wording the candidate summary kept
  • Assumes reference wording is the target, which is defensible for extractive style summaries and wrong for open answers
  • ROUGE-L swaps n-grams for longest common subsequence, same assumption underneath
UseVerdictWhy
RAG answer qualityWEAK · DO NOT GATEPenalizes correct paraphrase, rewards fluent copying of a wrong reference, and has no concept of whether a claim is supported by the retrieved context
Fixed style generationACCEPTABLE SIGNALTemplated summaries, formatted reports, style locked outputs: the reference wording genuinely is close to the target, so overlap tracks something real
Release gate, aloneNEVERNo threshold on an overlap metric distinguishes a factual regression from a phrasing change. Slide 12's counterexample scores near zero on both metrics while being fully correct
SLIDE 14 / 22PACE 14:05
Slide 15 · Layer 2 · Semantic similarity

Embedding similarity: calibrate the threshold, fear the near miss

The computation

cos(a, b) = (a · b) / ( |a| × |b| )
  • Embed prediction and reference with the same pinned embedding model, record its version in the manifest
  • Score is the cosine: 1.0 is identical direction, and real paraphrase pairs typically land well below it
  • Above threshold counts as a match. The threshold is the scorer, everything else is arithmetic

Calibrate, do not guess

  1. Label a dev set of pairs by hand: match or no match, a few hundred pairs, including adversarial near misses
  2. Sweep the threshold across the score range and read the false pass and false fail rates at each candidate value
  3. Pick the operating point by the error you can afford. For a gating scorer, false passes are the expensive direction
  4. Freeze it in the scorer version. Changing the threshold or the embedding model is a scorer change, slide 21
WHERE IT BREAKS · ILLUSTRATIVE PAIR SCORES · HIGH COSINE, WRONG MEANING
Pair (reference vs. prediction)CosineSimilarity verdict at 0.85Reality
"A refund is available within 30 days" vs. "A refund is not available within 30 days"0.93MATCHOpposite claim. One token flips the meaning, embeddings barely move
"Refund window is 30 days" vs. "Refund window is 60 days"0.97MATCHWrong number. The single most important token in the answer is the one that changed
"Enterprise plans include SSO" vs. "Team plans include SSO"0.95MATCHWrong entity. Plan names live close together in embedding space, far apart in the contract

Embeddings encode topical similarity, and these pairs are maximally on topic. Use similarity as a soft signal and a triage tool. Never let it be the only thing standing between a negated or renumbered answer and a release.

SLIDE 15 / 22PACE 15:10
Slide 16 · Layer 2 · Retrieval metrics

Score retrieval before generation

expected_sources in each case turns retrieval into a scored component with its own ground truth. If the right chunk never arrives, no prompt can fix the answer, so this failure gets measured at its source.

MetricFormulaWhat it answers
Recall@krelevant retrieved in top k / all relevantDid the evidence the answer needs show up at all?
Precision@krelevant retrieved in top k / kHow much of the context window is signal vs. noise?
MRRmean over queries of 1 / rank of first relevantHow high does the first useful doc rank, on average?
Hit Rate@kqueries with at least 1 relevant in top k / all queriesFor what fraction of questions did retrieval give generation a chance?

Worked example · one case, k = 5

RankRetrieved docRelevant?Arithmetic
1docs/billing/refunds.mdYESRelevant set for this case: 3 docs. Top 5 contains 2 of them, at ranks 1 and 4.

Recall@5 = 2 / 3 = 0.667
Precision@5 = 2 / 5 = 0.400
Reciprocal rank = 1 / 1 = 1.000
Hit = 1 (at least one relevant present)
2docs/billing/invoices.mdNO
3docs/account/profile.mdNO
4docs/billing/refund-policy-2026.mdYES
5docs/support/faq.mdNO

The third relevant doc never entered the top 5. If the answer then misses its content, that failure is booked to retrieval, with Recall@5 as the evidence, not to the model. Two failure classes, two owners, zero mixing.

SLIDE 16 / 22PACE 16:15
Slide 17 · Aggregation

Micro and macro: report both, or be misled by either

PER CAPABILITY PASS RATES · EXAMPLE RUN · CHAPTER 3 COMPOSITION, n = 500
Capability slicenPassedRateBar
Factual18017195.0%
Reasoning705680.0%
Refusal (both directions combined)12010890.0%
Ambiguous / clarify301860.0%
Instruction format504794.0%
Adversarial503570.0%

Micro average · every case equal

micro = 435 / 500 = 87.0%

Total passes over total cases. This is the honest overall number, and it is dominated by the biggest slice: factual cases alone are 36 percent of the set.

Macro average · every slice equal

macro = (95 + 80 + 90 + 60 + 94 + 70) / 6 = 81.5%

Mean of the six slice rates. It surfaces what micro hides: the ambiguous slice is collapsing at 60 percent. If that slice fell to zero, micro would move by only 18 cases in 500, about 3.6 points, while users who ask ambiguous questions would get a broken product.

The reporting rule: every aggregate ships with its n, and every slice ships with its own rate. A score without an n is a claim without evidence, and Chapter 3's sample size math is what decides whether a slice is even allowed to gate.

SLIDE 17 / 22PACE 17:20
SECTION 3 Baseline, Diff, and Trust SLIDES 18 TO 22 · MAKING SCORES MEAN SOMETHING
Slide 18 · The baseline run

Freeze the baseline. Everything after this is a diff.

What gets frozen

  • The system: current production prompt and model snapshot, run once through the full pipeline from slides 3 to 17
  • report.json: the per capability scores, micro and macro, with n per slice
  • raw_outputs.jsonl: all 500 outputs verbatim, so any baseline number can be re-derived or rescored
  • manifest.json: the pinned config from slide 4 that produced it

These artifacts are saved permanently, in the repo, next to the dataset version they ran against. A baseline that lives in a chat message or a dashboard screenshot is not a baseline.

The rule this creates

Every future number in this course is a diff against this baseline.

Not "the candidate scored 86.4 percent", but "the candidate scored 86.4 percent against a baseline of 89.6 percent, on the same 500 cases, with these 40 cases changing verdict". Absolute scores describe a run. Diffs make release decisions.

Re-baselining is a decision, not a habit

When production legitimately changes, you may promote a new run to baseline, in a reviewed change that records why: what shipped, what the old baseline measured, and what comparability you are giving up. Re-baselining quietly after a bad week is how suites learn to always pass.

SLIDE 18 / 22PACE 18:25
Slide 19 · Paired comparison

Same 500 cases, both runs: read the flips, not just the delta

Because baseline and candidate ran the identical cases, every case is a pair. Cross-tabulating the pairs shows movement the headline delta erases. Example run:

Per case flips · example run · n = 500

Candidate: pass
Candidate: fail
Baseline: pass
420both pass · stable
28regressions introduced
Baseline: fail
12fixes delivered
40both fail · known debt

The 28 regression case IDs are the review list. They land in failures.jsonl with reasons, and a human reads those before anyone argues about the release.

The arithmetic, and what it hides

baseline score(420 + 28) / 500 = 448 / 500 = 89.6%
candidate score(420 + 12) / 500 = 432 / 500 = 86.4%
net deltaminus 3.2 points. True, and almost content free.
the real signal28 regressions against 12 fixes. 40 cases changed verdict, and the flow ran more than two to one in the wrong direction. A candidate can post the same minus 3.2 with 16 flips each way, which is noise and churn, or with 28 against 12, which is a systematic break. The delta cannot tell these apart. The flips table can.

This asymmetry is also the statistical core of paired tests like McNemar's: only the discordant pairs carry information about which system is better, and their imbalance is the evidence.

SLIDE 19 / 22PACE 19:30
Slide 20 · Failure artifacts

Artifacts you debug from, not dashboards you glance at

failures.jsonl · one record per failed case · example record
{"case_id": "refusal-041", "capability": "refusal", "scorer": "refusal_behavior", "passed": false, "reason": "expected refuse, detected answer: output cites billing/refunds.md and states a 30 day window for a 2019 Enterprise plan", "run": "2026-10-30_cand"}

If a failure has no reason string, the scorer is not finished. The reason is what turns a failed case into an assignable bug. A developer handed refusal-041 and that reason can reproduce and fix the failure without rerunning anything or guessing what the scorer meant.

report.json · per capability scores with n · example run excerpt
{ "dataset": "golden_v1.2", "scorer_version": "scorers 1.3.0", "capabilities": { "factual": {"pass": 171, "n": 180, "rate": 0.950}, "refusal": {"pass": 108, "n": 120, "rate": 0.900}, "ambiguous":{"pass": 18, "n": 30, "rate": 0.600} }, "micro": 0.870, "macro": 0.815 }

What the pair of artifacts gives you

  • report.json is the diff surface: machine readable, comparable across runs, ready for the CI gate in Video 8
  • failures.jsonl is the work queue: filterable by capability and scorer, every row actionable
  • Together they answer both questions a release review asks: how much changed, and exactly where
SLIDE 20 / 22PACE 20:35
Slide 21 · Trust

The scorers are code. Test them like code.

Unit fixtures per scorer

FixtureWhat it pins down
Known pass outputA hand written output that must score passed, with the expected value and reason shape
Known fail outputA hand written output that must fail, including the exact violation it demonstrates
Boundary outputsThe edge the scorer is most likely to get wrong: the citation that almost resolves, the bullet list with a stray line, the refusal phrased as an apology with an answer attached

Fixtures are cheap because of the slide 5 contract: score(case, output) on two literal dicts, no model, no network. A scorer without fixtures is an opinion with an API.

Scorer changes are versioned like model changes

  • Every report carries scorer_version, slide 20. Diffs are only valid within one scorer version, exactly like dataset versions in Chapter 3
  • A scorer fix triggers a rescore of cached outputs, slide 3, so its effect is measured in isolation

The trap this prevents

Example: a unicode normalization fix in the contains scorer moves the factual pass rate up 1.8 points, example run, with zero model change. Reported naively, that reads as a model improvement. Reported correctly, the diff attributes it to scorers 1.3.0 to 1.3.1, model and prompt untouched. Measurement changes and system changes must never share a headline number.

SLIDE 21 / 22PACE 21:40
Slide 22 · Key takeaways

Scoring without an LLM, in five lines

Key takeaways

  1. Separate generation from scoring. Cache raw outputs, rescore freely, never pay twice for the same generation.
  2. Deterministic first. It is free, fast, and reproducible, and it catches more than most teams expect: structure, citations, refusals, tool calls, cost.
  3. Every metric has a formula and a failure mode. Know both before you put a number in a report.
  4. Aggregate per capability, micro and macro with n, and compare paired against a frozen baseline.
  5. Test the scorers and version them. A score is only as trustworthy as the code that produced it.

Next video · Chapter 5

LLM-as-a-judge, implemented. What Layers 1 and 2 cannot measure is precisely defined now: faithfulness to the retrieved context, whether a citation actually supports its claim, helpfulness, completeness. Video 5 builds the judge for exactly those questions, and Video 6 calibrates it against human labels before it is allowed to gate anything.

Bring: report.json, failures.jsonl, and the frozen baseline. Next time the suite learns to judge, carefully.

SG
Saloni GargSenior Machine Learning Engineer, Adobe
SLIDE 22 / 22PACE 22:45 · END 24:00