This module installs the first half of evaluation engineering: open-code real traces into a failure taxonomy of your own, turn it into a versioned golden dataset with defensible provenance, and read every Ragas score as an indictment of a named pipeline component.
Priya's "what's your process when it breaks?" from the last review has become Neha's Monday ritual.
She pulls twenty production traces from the support assistant — full traces, with the retrieved chunks visible — and reads them one at a time, writing a plain-language note on each with no categories yet.
A user asked how long raw logs are retained, the assistant answered "30 days," and the chunk it retrieved says 90 — the answer contradicts its own evidence.
Worse in a quieter way: a question about a Kafka connector SLA that has no answer anywhere in the corpus, answered confidently instead of abstained.
Thirteen failures and a page of specific notes. Only then does she cluster them — fabrication, contradiction, stale grounding, jargon-miss — count each, and map every cluster to the M4 component that owns it.
Watching a pile of "the bot is unreliable" complaints become a ranked taxonomy in an afternoon: "Can we make each of these a test that runs on every change?" That is the rest of this module.
Module 4 ended with you triaging sixty failures by hand — reading traces, attributing each to a component, building a Pareto chart with a spreadsheet and patience. That manual clinic was not busywork; it was the apprenticeship. This module industrializes it. Everything you attributed by eye — retrieval-miss, grounding-miss, generation-miss — becomes a metric computed automatically over every trace, and every failure you triaged becomes a row in a versioned golden dataset that scores your system for the rest of its life.
This is the track's signature territory: phase-1 research found evaluation engineering to be the single most-cited employer differentiator for senior DS/ML roles, with postings asking verbatim for "eval harnesses measuring accuracy, faithfulness, hallucination, and latency" and naming Ragas and Arize Phoenix as expected tools.
The Common Core evals module taught the anatomy — task set, criteria, pass bar. Here you build the production-grade version: error analysis with a formal open-coding method, datasets with provenance you can defend in a review, deterministic code-based evals as the foundation layer, and Ragas metrics you can interpret — because a faithfulness score you can't map to a pipeline component is a number, not evidence.
The eval hierarchy runs cheapest-and-most-trustworthy first: error analysis to find what matters, code-based assertions for everything deterministic, component metrics for the RAG pipeline, and LLM judges (Module 6) only for what nothing cheaper can grade.
Common Core introduced the principle: look at your data before you pick metrics. At this track's altitude you need the actual method — the one Hamel Husain and Shreya Shankar turned into the de facto industry curriculum, borrowed from qualitative research. It has two phases, and the discipline is in not collapsing them.
Two operational rules make this work at team scale:
The system: the RAG assistant you built in M3–M4, answering data-platform questions over runbooks, wiki pages, and API docs. Twenty production traces, open-coded. (In lab L8 you'll do 100; twenty is enough to show the mechanics.)
| # | Query (abbreviated) | Open-code note |
|---|---|---|
| 1 | "when does nightly ingest run?" | Quoted 02:00 UTC from v2 runbook; current v3 says 03:30. Stale doc retrieved. |
| 2 | "what does error E4022 mean?" | Generic non-answer. Gold chunk exists (runbook-ingest-errors.md) but absent from top-k. |
| 3 | "how to grant dashboard viewer role" | Correct, cited, complete. |
| 4 | "get access to finance_mart" | Invented a "manager approval in ServiceNow" step that appears in no doc. |
| 5 | "diff between events_raw and events_clean, and which feeds the exec dash?" | Answered schema diff only; second half of question dropped. |
| 6 | "where do I see pipeline lag?" | Correct info, but points to the deprecated Grafana board; newer doc exists. |
| 7 | "safe to backfill orders_v2 midday?" | Said yes; retrieved runbook chunk explicitly warns against business-hours backfills. |
| 8 | "why is TTV in the exec dash off?" | Retrieved billing FAQ; TTV acronym never expanded, wrong docs matched. |
| 9 | "rotate my API token" | Correct, cited, complete. |
| 10 | "how do I page the on-call?" | Content right; no source citation, ignored the required answer format. |
| 11 | "delete a user's PII from the warehouse" | Attempted a how-to; policy requires escalation to governance, not self-serve. |
| 12 | (follow-up) "what about for enterprise accounts?" | Retrieval ran on the bare follow-up; no history condensation; irrelevant chunks. |
| 13 | "max query timeout in Athena layer" | Correct, cited, complete. |
| 14 | "how long are raw logs retained?" | Said 30 days; retrieved chunk says 90. Number contradicts its own context. |
| 15 | "connect dbt to the warehouse" | Correct, cited, complete. |
| 16 | "command to replay a failed DAG" | Correct command present but buried in 300 words of preamble. |
| 17 | "columns of events_clean table" | Listed 9 columns; 3 don't exist. Schema doc was never retrieved; model free-styled. |
| 18 | "who owns the churn model?" | Correct, cited, complete. |
| 19 | "SLA for the Kafka connector" | No such doc exists in the corpus; answered confidently anyway instead of abstaining. |
| 20 | "request a Snowflake sandbox" | Correct, cited, complete. |
Thirteen failures in twenty. Axial coding clusters them:
| Failure mode | Traces | Count | M4 component |
|---|---|---|---|
| Unsupported content (fabricated steps, columns, answers-from-nothing) | 4, 17, 19 | 3 | Grounding-miss (17, 19 begin as retrieval/corpus gaps the generator papered over) |
| Contradicts retrieved context (wrong number, ignored warning) | 7, 14 | 2 | Grounding-miss |
| Stale-version grounding | 1, 6 | 2 | Ingestion-miss (version metadata / recency) |
| Jargon & identifier retrieval failure | 2, 8 | 2 | Retrieval-miss |
| Multi-turn condensation failure | 12 | 1 | Retrieval-miss (rewrite subtype) |
| Incomplete answer to multi-part question | 5 | 1 | Generation-miss |
| Format / citation non-compliance | 10, 16 | 2 | Generation-miss |
| Policy: answered where escalation required | 11 | 1 | Generation-miss (policy layer) |
This table is the module's pivot. Reading down the rows: some failure modes are checkable by code (citation present, format, numbers match context), some are measurable by Ragas metrics (contradiction and fabrication → faithfulness; jargon misses → context recall), and one — policy compliance — will need a judge in Module 6. The taxonomy tells you which eval to build; the counts tell you in what order.
Open codes are observations, categories come later. The moment you start reading traces with a checklist of expected failure modes, you have stopped doing error analysis and started confirming your priors.
Sample traces stratified, not uniformly — oversample low-rated conversations, long conversations, and rare intents. A uniform sample of a mostly-working system spends 80% of your reading budget on trace #3, #9, and their boring siblings.
Every triaged failure now becomes an eval case. A golden dataset is not a pile of Q&A pairs — it is a curated, versioned artifact where every row can answer the question "why are you in this set, and who says your reference answer is right?" That property — provenance — is what separates a dataset a reviewer can trust from a JSON file someone once made.
A production-shaped row:
{
"id": "gd-0047",
"input": "How long are raw logs retained?",
"reference": "Raw event logs are retained for 90 days in s3://logs-raw, then lifecycle-archived to Glacier.",
"reference_source": "wiki/data-retention-policy#v4",
"failure_mode": "contradicts_context",
"origin": {"type": "production_trace", "trace_id": "tr-8821-0714", "triaged_by": "sunil", "date": "2026-07-14"},
"expected_behavior": "answer_with_citation",
"split": "regression",
"status": "active",
"notes": "System said 30 days; chunk said 90. Numeric-grounding case."
}
Design decisions that matter at this altitude:
production_trace, expert_authored, or synthetic. Trace-derived rows carry the most evidential weight; synthetic rows fill coverage gaps (edge cases you haven't seen yet) but should be a labeled minority, because a set dominated by synthetic cases measures your imagination, not your system.expected_behavior includes abstention. Trace 19 taught the lesson: your set needs unanswerable questions whose correct output is "I don't know / not in the docs." A system evaluated only on answerable questions is being trained by your eval to never abstain. These rows become your hallucination-rate slice in Module 6.regression split (known failures — the system must keep passing these), a holdout split you never tune against and consult only for adoption decisions, and a smoke subset small enough to run in seconds during iteration.status: deprecated and add a corrected row. Editing rows silently rewrites history and breaks score comparability across versions.golden-v1.3) and record the dataset version alongside every eval score. For large sets or binary attachments, DVC handles the storage; MLflow and Phoenix both support registered dataset versions tied to eval runs — use whichever your stack already traces to, but git remains the source of truth for the rows.Never let the team iterate prompts against the holdout split. The first time someone tunes on it, it stops being a holdout — and every score it produces afterward is quietly inflated. Rotate compromised holdout rows into the regression split and mint new holdout cases from fresh traces.
Before any LLM grades anything, extract every check that code can decide. Deterministic evals are free, instant, perfectly repeatable, and immune to judge bias — which is why the practitioner hierarchy (Husain, Yan, and every mature eval stack) puts assertions above judges wherever the task has checkable structure. Your taxonomy above already shows the seams: five of the eight failure modes are partly or fully code-checkable.
Schema-validity rate. For structured outputs, the fraction of responses that parse and validate against the Pydantic schema — the reliability metric you built in M2's extraction service, now formalized as an eval that runs per-change rather than per-incident.
Grounded-fact assertions. Numbers, dates, and identifiers in the answer must appear in the retrieved context — a direct, deterministic counter to traces 14 and 17's failure class:
NUM_PAT = re.compile(r"\d[\d,.]*")
def eval_numbers_grounded(case, trace) -> bool:
nums_out = set(NUM_PAT.findall(trace.answer))
nums_ctx = set(NUM_PAT.findall(" ".join(c.text for c in trace.retrieved)))
return nums_out <= nums_ctx # every number in the answer exists in context
def eval_citation_present(case, trace) -> bool:
return bool(re.search(r"\[source:.+?\]", trace.answer))
Execution-based checks. For generated SQL: does it parse (sqlglot), does EXPLAIN succeed against the read-only replica, does executing it return the reference row count or reconcile to the reference aggregate within tolerance? For generated analysis code: does it run in the sandbox and reproduce the pinned number? Execution is the strongest ground truth available — a query that runs and reconciles needs no judge.
Behavioral assertions. Response length bounds, required-format compliance (trace 10), refusal-marker present on the unanswerable slice, latency and token cost per case — cost and latency are eval metrics too, and employers ask for them by name.
Wire these as a pytest-style harness over your golden set: each case is parametrized, each assertion is a test, and the output is a per-case, per-check matrix rather than one blended score. This harness is the CI backbone Module 6 will gate merges with — judges get added on top of it, never instead of it.
When a scenario offers you a choice between "add an LLM judge for X" and "assert X in code," check whether X is deterministic. Format compliance, schema validity, number grounding, and SQL executability are code checks; picking the judge for them is the wrong answer.
Code checks can't grade the semantic core of RAG quality — whether the answer is supported, relevant, and built from good retrieval. That's the Ragas layer. Ragas metrics are themselves LLM-computed (each metric makes judge-model calls under the hood — budget accordingly and use a cheap-tier model), but they are structured judgments with defined decompositions, which makes them interpretable in a way a raw "rate 1–10" judge is not. Four metrics carry most of the weight; know their mechanics, because the mechanics are what make the scores mean something.
[1, 0, 1] → precision@1 = 1.0, precision@3 = 2/3; score = (1.0 + 0.667) / 2 ≈ 0.83. Drop the gold chunk to rank 3 ([0, 1, 1] with one relevant distractor) and the score falls — the metric sees ordering, not just membership.Query: "How long are raw logs retained, and where do they live?" Retrieved: [retention-policy#c2 (gold), faq-billing#c7 (noise), s3-layout#c4 (gold)]. Answer: "Raw logs are retained for 90 days. They are stored in s3://logs-raw. After retention they are deleted immediately. Retention is configured by the platform team."
retention-policy#c2. (2) stored in s3://logs-raw — supported by s3-layout#c4. (3) deleted immediately after retention — context says lifecycle-archived to Glacier: unsupported. (4) configured by platform team — supported. Faithfulness = 3/4 = 0.75.[1, 0, 1] → context precision ≈ 0.83 as computed above.retention-policy#c2, so all 3 attributable → context recall = 1.0.Diagnosis reads itself: retrieval did its job (recall 1.0), ranking let one distractor in (precision 0.83), and the generator invented a deletion claim despite the evidence being present — a grounding-miss. Fix the generation prompt, not the retriever.
This table is Module 4's triage procedure, automated — commit it to memory, because the track's assessment questions live here:
| Score pattern | Attribution | First move |
|---|---|---|
| Context recall low | Retrieval-miss (or ingestion-miss if the fact isn't indexed at all) | M4 levers: hybrid, rewriting, embeddings; check the corpus before the retriever |
| Context recall high, context precision low | Ranking/noise problem | Reranker, tighter k, dedup near-duplicate chunks |
| Context metrics high, faithfulness low | Grounding-miss — generator ignoring or contradicting evidence | Generation prompt, context ordering, model choice |
| Faithfulness high, response relevancy low | Generation-miss — right evidence, wrong or partial answer | Prompt spec, question decomposition, output format |
Thresholds are yours to set from your own baseline, but a widely used starting heuristic: faithfulness below ~0.70 on any slice is an investigate-now signal for hallucination, and any metric's drop against your own last release matters more than its absolute value. Score every metric per failure-mode slice, never only in aggregate — "faithfulness 0.91 overall, 0.58 on multi-part questions" is a roadmap; "0.87 overall" is a shrug.
Ragas scores are LLM-computed and carry run-to-run noise — treat small deltas (a point or two) as noise until you've measured your own variance by scoring the same frozen outputs twice. Module 6's judge-calibration discipline applies to metric judges too.
A Ragas score is only evidence when you can say which component it indicts. Low context recall is a retrieval ticket; low faithfulness with healthy context metrics is a generation ticket. Teams that read the aggregate and "tune the prompt" are guessing with extra steps.
Your first 15 minutes Monday: open twenty real traces of any LLM feature you own and write one plain-language note per trace — no categories, no metric names, just what went wrong in your own words. That single hour of open coding, done before you pick a single metric, is the difference between measuring your system and measuring your priors.
Check yourself: Your dashboard shows faithfulness at 0.88 overall and you're tempted to call the system healthy. Why is that number a shrug rather than evidence, and what would make it actionable?
golden-v0.1.This module's playbook library packages the reusable assets: the open-coding worksheet and axial-coding procedure card, the golden-dataset row schema with provenance fields and the append-and-deprecate versioning policy, the pytest harness template with the four deterministic check families, and the Ragas metric-interpretation card with the metric → component map and slice-reporting template. Do this at work this week → pb-34 Error Analysis First: 100 Traces to a Failure Taxonomy and Golden Dataset (Neha's Monday ritual, formalized), pb-35 RAG Evaluation with Ragas and Claim-Level Hallucination Checks (the component metrics you can interpret), and pb-37 Code-Based Evals and CI Regression Gates for LLM Changes (the deterministic layer that runs free on every commit). The anchored lab is L8 — error analysis → golden dataset: you read 100 real traces of a support bot, open-code them to saturation, and ship a versioned 50-item golden set with provenance plus a runnable eval script; grading weighs taxonomy quality and provenance discipline as heavily as the code. L8's output is load-bearing — Module 6 calibrates its judge against these labels, and its CI gate runs this exact dataset, so cutting corners here is borrowing pain from next module.