Evaluation Frameworks & Datasets
Every prior lab produced a system. This lab produces the instrument that tells you whether the system works, and, more importantly, whether a change made it better or merely different. The exam does not test whether you can compute an F1 score. It tests whether you know which metric to trust, which grader to point at which question, and how to build a dataset that catches the failure you have not seen yet.
- Define evaluation metrics (accuracy, latency, cost, safety, security)
- Design evaluation datasets and test frameworks using mixed methodologies
- Conduct A/B testing and iterative improvements
1. The Five Metric Families
The objective names five categories, and it names them because architects reach for the wrong one under pressure. Accuracy is the one everyone measures; the other four are the ones that end projects. Each metric answers a question, and each one hides something, which is the part the exam tests.
| Family | Concrete measures | What it hides |
|---|---|---|
| Accuracy task-specific | Exact match, F1, groundedness / citation validity, rubric score | A single aggregate number hides which segment regressed. 95% overall can be 99% on easy cases and 60% on the ones that matter. |
| Latency | p50 / p95 / p99; time-to-first-token (TTFT) vs total | The mean hides the tail. And TTFT hides total: streaming improves TTFT only; the full answer arrives no sooner. |
| Cost | Cost per request; cost per resolved task | Per-request cost hides retries. If quality drops and users re-ask, cost per request falls while cost per resolved task rises. |
| Safety | Refusal correctness, harmful-output rate, false-refusal rate | A model that refuses everything scores perfectly on harmful-output rate. Safety is two-sided; measuring one side invites the other to rot. |
| Security | Injection resistance, data-leak rate, privilege-escalation attempts blocked | Passing your own probes hides the probe you did not write. Security metrics decay the moment an adversary sees them. |
The costliest of these confusions is the cost one. Cost per request falls while cost per resolved task rises whenever a cheaper or worse model is swapped in: each call is cheaper, but users retry the ones that failed, so the true denominator, resolved tasks, grows slower than the request count. Always define the metric on the unit the business cares about (a resolved ticket, a routed claim), not the unit the API bills you for.
2. Mixed Methodologies: Three Graders
"Mixed methodologies" is the objective's exact phrase, and it is not a suggestion to hedge, it is an architecture. You have three ways to grade an output, they cost wildly different amounts, and they catch different failures. The discipline is to use the cheapest one that can answer the question, and to reserve the expensive one for the question only it can answer.
| Grader | Grades | Cost / speed | Fails at |
|---|---|---|---|
| Programmatic deterministic |
Schema validity, exact match, regex, "does the cited span exist in the source" | Nearly free, instant, exact | Anything requiring judgment. It cannot tell you if a correct-looking answer is actually right. |
| LLM-as-judge | Rubric-scored quality: helpfulness, tone, faithfulness, completeness | Cheap enough to run at scale; correlates with humans only if validated against them | Its own biases: position, verbosity, self-preference. An uncalibrated judge is confidently wrong. |
| Human review | Ground truth: the thing the other two are approximating | Expensive, slow, the scarcest resource you have | Scale. You cannot label 4,000 items a day by hand, which is the whole reason the other two exist. |
The architecture that follows from this table is a funnel: programmatic gates run first and catch most regressions (a broken schema needs no judge), the LLM judge runs at scale on what survives, and humans calibrate a sample, both to validate the judge and to adjudicate genuinely ambiguous cases. You do not run all three on everything. You run each where it is cheapest to be right.
3. Designing the Dataset
The eval set is the most valuable artifact in the whole program, because every metric is computed against it. A biased dataset produces confident, precise, wrong numbers. Four sources feed a good one:
| Source | Why it belongs | Risk if it is your only source |
|---|---|---|
| Production traces | Real distribution of real inputs: the actual job | Only contains cases the system already sees; misses the long tail |
| Human corrections / overrides | Free labelled data on exactly the cases the system got wrong | Biased toward one reviewer's opinion (see agreement ceiling below) |
| Adversarial / red-team items | Probes the security and safety metrics deliberately | Grows stale: yesterday's attack is today's regression test, not today's threat |
| Synthetic edge cases | Covers rare combinations production has not produced yet | Can drift from reality; synthetic data validates against synthetic assumptions |
Composition matters more than size. Stratify by the segments you care about so a regression in one queue cannot hide behind strength in another. Over-sample failure modes: a dataset that mirrors production is 95% cases you already pass, which means 95% of your eval spend measures nothing. Hold out a set you never look at, because a dataset you tune against stops being a test and becomes training data. And keep a frozen regression suite that never changes, so "did this get worse" has a stable answer.
4. Test Frameworks, Layered Like Software Tests
An eval program is not one big scoreboard; it is a set of tests at different altitudes, exactly as a well-tested codebase has unit, integration, and end-to-end layers. Each layer catches a different class of failure and runs at a different cadence.
| Layer | What it exercises | When it runs |
|---|---|---|
| Unit | One prompt against a golden fixture: is this single call still correct? | On every prompt change |
| Integration | Retrieval + generation together: does the pipeline compose? | On component changes |
| End-to-end | The full pipeline against a realistic scenario | Before release |
| Regression | The frozen suite: did anything that worked stop working? | In CI, every change |
| Canary / online | A slice of live traffic, measured in production | Continuously, post-deploy |
Here is a small pytest-style harness that runs a case set concurrently, validates structured output, and reports pass rate and p95 latency, because a change that lifts accuracy while doubling the tail is not an improvement, it is a trade you must see to approve. Note the model ID is pinned and the prompt version is pinned; that, not a sampling parameter, is how you make an eval reproducible.
import anthropic, asyncio, json, time
from statistics import quantiles
client = anthropic.AsyncAnthropic()
TRIAGE_SCHEMA = {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"queue": {"type": "string",
"enum": ["auto_approve", "standard_review",
"fraud_review", "request_information"]},
"cited_clause": {"type": "string"},
},
"required": ["queue", "cited_clause"],
"additionalProperties": False,
},
}
# Pin the model ID and prompt version. Determinism does NOT come from a
# sampling parameter -- eval runs vary, which is exactly why we report a
# pass RATE over a set, never the result of a single trial.
async def run_case(case):
start = time.monotonic()
resp = await client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
output_config={"format": TRIAGE_SCHEMA},
system=[{"type": "text", "text": TRIAGE_POLICY_MANUAL,
"cache_control": {"type": "ephemeral"}}],
messages=[{"role": "user", "content": case["packet"]}],
)
latency = time.monotonic() - start
text = next(b.text for b in resp.content if b.type == "text")
predicted = json.loads(text)["queue"]
return {"passed": predicted == case["expected_queue"], "latency": latency}
async def evaluate(cases):
results = await asyncio.gather(*(run_case(c) for c in cases))
pass_rate = sum(r["passed"] for r in results) / len(results)
p95 = quantiles([r["latency"] for r in results], n=100)[94]
return {"pass_rate": pass_rate, "p95_latency_s": round(p95, 3)}
temperature to pin (it was removed from the current models entirely; setting it returns HTTP 400). The reproducibility an eval actually needs comes from pinning the model ID and the prompt version, then accepting that individual runs vary. That variance is precisely why you report a pass rate over a set of cases rather than the verdict of a single call. A metric built on one trial is measuring noise; a metric built on two hundred is measuring the system.
5. LLM-as-Judge, Done Properly
When programmatic checks cannot answer the question ("is this rationale faithful to the cited clause?"), you reach for a judge. Doing it properly means forcing a structured verdict, giving explicit rubric criteria, and randomizing the order of any candidates so position bias cannot leak in. The judge should be at least as capable as the generator for hard rubrics; a cheaper judge is fine only for narrow, well-specified criteria.
import anthropic, random
client = anthropic.Anthropic()
VERDICT_SCHEMA = {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"verdict": {"type": "string", "enum": ["pass", "fail"]},
"criterion_scores": {
"type": "object",
"properties": {
"faithful_to_source": {"type": "integer"},
"answers_the_question": {"type": "integer"},
"no_fabricated_clause": {"type": "integer"},
},
"required": ["faithful_to_source",
"answers_the_question",
"no_fabricated_clause"],
"additionalProperties": False,
},
"reasoning": {"type": "string"},
},
"required": ["verdict", "criterion_scores", "reasoning"],
"additionalProperties": False,
},
}
def judge(question, source, candidate_a, candidate_b):
# Randomize order to defeat position bias. Record the mapping so you
# can un-shuffle the verdict afterward.
pair = [("A", candidate_a), ("B", candidate_b)]
random.shuffle(pair)
body = "\n\n".join(f"Candidate {label}:\n{text}" for label, text in pair)
return client.messages.create(
model="claude-opus-4-8", # judge >= generator for hard rubrics
max_tokens=1024,
thinking={"type": "adaptive"}, # NOT budget_tokens -- that returns 400
output_config={"effort": "high", "format": VERDICT_SCHEMA},
system=[{"type": "text", "text": (
"Score each criterion 0-3 against the SOURCE only. "
"A fabricated clause is an automatic fail regardless of tone."
)}],
messages=[{"role": "user",
"content": f"SOURCE:\n{source}\n\nQUESTION:\n{question}\n\n{body}"}],
)
Two design choices carry the correctness. The structured verdict makes the judge's output machine-comparable and forces it to commit to per-criterion scores rather than a single fuzzy number. And thinking={"type": "adaptive"} lets the judge reason before it rules. Note it is adaptive, not a fixed token budget; the old budget_tokens form is rejected with a 400 on the current models.
custom_id, never by position. A batch harness that assumes result N corresponds to input N will silently mis-score the whole run.
The batch harness is a few lines, and the load-bearing detail is the join at the end, where the results are matched back to their expected labels by custom_id, not by the order they arrive:
import anthropic, json
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
client = anthropic.Anthropic()
# cases: list of {"id": str, "packet": str, "expected_queue": str}
expected = {c["id"]: c["expected_queue"] for c in cases}
batch = client.messages.batches.create(requests=[
Request(
custom_id=c["id"],
params=MessageCreateParamsNonStreaming(
model="claude-opus-4-8",
max_tokens=1024,
output_config={"format": TRIAGE_SCHEMA},
messages=[{"role": "user", "content": c["packet"]}],
),
)
for c in cases
])
# ... poll client.messages.batches.retrieve(batch.id) until "ended" ...
passed = total = 0
for result in client.messages.batches.results(batch.id):
if result.result.type != "succeeded":
continue
msg = result.result.message
text = next(b.text for b in msg.content if b.type == "text")
predicted = json.loads(text)["queue"]
# Join by custom_id -- results are UNORDERED. Never index by position.
passed += predicted == expected[result.custom_id]
total += 1
print({"pass_rate": passed / total})
6. A/B Testing and Iterative Improvement
Offline eval tells you a change looks better on your dataset. A/B testing tells you it is better in production, but only if you run it as an experiment and not as a hunch. The governing rule: change one variable, pre-register the metric and the decision rule before you look at the data. A dashboard read after the fact will always find a story.
| What you A/B | The confound it introduces |
|---|---|
| Prompt version | Cleanest test, but a longer prompt shifts cost and latency too; watch those guardrails |
| Model (e.g. Opus → Sonnet) | Changes accuracy, cost, and latency at once. You cannot attribute a quality change to any single cause. |
| Retrieval config | Changes what the model sees, so an "accuracy" delta may really be a context-recall delta (Lab 4) |
| Effort level | Trades quality against cost and latency: three metrics move together, so pair the primary with a guardrail |
Three traps recur. Novelty effects: a new behaviour looks better simply because it is new, then regresses to the mean. Traffic skew: if the arms do not see the same input distribution, you are comparing populations, not variants. And statistical vs practical significance: with enough traffic, a 0.3% lift is "significant" and still not worth a rollout. Above all, remember that offline eval predicts online performance only as well as your dataset resembles production; the gap between the two is the gap between your eval set and reality.
7. Design Exercise
Self-driven lab Lab6_Self_Driven_Lab.ipynbObjective: build the evaluation program for the claims-triage system from Lab 1: 4,000 claims a day, four queues (auto-approve, standard review, fraud review, request-information), adjusters who agree with each other about 85% of the time. About 60 minutes. Write it as if handing it to the team that will run it.
- Pick the metrics, one per family. Name the accuracy measure (is it exact-match on the queue, or does a wrong fraud-route cost more than a wrong standard-route, and if so, do you weight it?). Name the latency percentile that satisfies the one-hour SLA. Name the cost unit: per claim or per correctly routed claim. Name the safety and security metrics that a claims system actually has (false denial, PII leakage). Do not proceed until every family has exactly one number.
- Set the target honestly. Adjusters agree 85% of the time. Write the sentence that states what your accuracy ceiling therefore is, and why a proposal promising 97% should make you suspicious rather than pleased.
- Design the dataset from the overrides. Every adjuster override from Lab 1's feedback plane is a labelled case. Describe how you stratify it (by queue? by claim value?), which failure modes you over-sample, and what you put in the frozen hold-out you never tune against.
- Assign each criterion to a grader. "Is the queue label correct": programmatic. "Does the cited clause actually exist in the policy": programmatic. "Is the rationale a faithful reading of that clause": judge. "Is this genuinely ambiguous claim routed defensibly": human. Write the funnel.
- Define the regression gate. The frozen suite runs in CI. State the pass condition that blocks a merge, and state it as pass rate over the set, plus a p95 guardrail, not as any single trial.
- Specify the A/B for a model swap. Someone proposes moving the classification call from Opus to Sonnet to cut cost. Write the experiment: the one variable, the primary metric, the guardrail metric, the decision rule, and the confound you must watch (a model swap moves quality, cost, and latency at once; how will you attribute the change?).
Step 2 is the one the exam tests. Step 4 is the one that makes the program affordable to run.
Check Yourself
Exam-style items. Commit to an answer before expanding.
A support-automation team reports that their new, cheaper model cut cost per API request by 30%. Finance is unhappy: the monthly bill went up. Assuming both are true, what is the most likely explanation?
Correct: B. Cost per request and cost per resolved task have different denominators. A worse model makes each call cheaper but resolves fewer of them on the first try, so users re-ask; total request volume climbs faster than resolved-task volume, and the bill grows. The metric that matched the business (resolved tasks) moved the opposite way from the metric that was reported (requests).
A contradicts the premise that cost per request fell. C is a distractor built from a real fact (batches are 50% off) attached to the wrong scenario: interactive support is not a batch workload. D is simply false: latency is not a billed line item. The lesson is to define the cost metric on the unit the business buys, not the unit the API meters.
A team validates their LLM judge once against 200 human labels, finds 88% agreement, and ships it. Six weeks later they rewrite the rubric to be stricter and continue using the same judge. What is the most serious methodological error?
Correct: B. A judge's calibration is only valid for the rubric it was calibrated on. Rewriting the rubric changes the task the judge is performing, so the 88% agreement number no longer describes the current judge; it describes a judge that no longer exists. An uncalibrated judge is a metric you invented; a judge whose calibration has silently expired is worse, because it looks calibrated.
A is wrong as stated: 200 labels is a reasonable calibration sample; the error is not the size but the staleness. C optimizes cost while ignoring correctness, and a cheaper judge on a now-stricter rubric is exactly the wrong move. D inverts the agreement-ceiling idea: 88% human agreement is the ceiling you calibrate toward, not a disqualification.
You are building an offline evaluation that must grade 12,000 recorded cases against a candidate prompt overnight. Which two choices are appropriate for this workload? (Choose two.)
Correct: A and B. An overnight, non-latency-sensitive eval is the canonical Batches workload: the 24-hour window is irrelevant to you and the 50% discount is real money at 12,000 cases. And because batch results return unordered, you must join each result back to its input by custom_id; positional assumptions silently mis-score the whole run.
C is the tempting trap: temperature was removed from the current models and now returns HTTP 400, and determinism never came from it anyway; reproducibility comes from pinning the model ID and prompt version and reporting a pass rate over the set. D pays premium fast-mode pricing to solve a latency problem you do not have. E discards stratification: the truncated 500 will not contain the same mix of failure modes as the full 12,000, so its pass rate measures a different distribution.
An A/B test of a new prompt shows a statistically significant +2.1% on the primary accuracy metric across 40,000 requests. The team is about to ship. What should they verify first?
Correct: B. A single-metric A/B is designed to be approved. A prompt change that lifts accuracy can easily raise false-refusals, inflate the latency tail, or cost more per resolved task. And a result that improves the primary metric is exactly when teams forget to look. Pairing the primary with pre-registered guardrails, each with a stop condition, is what turns "it went up" into "it is safe to ship."
A misreads the scenario: significance was already reached, so sample size is not the open question. C fixates on one guardrail (cost) while assuming prompt length is the only thing that moved; it also is not the first thing to check. D confuses re-running with guarding: repeating the same experiment tests reproducibility, not whether a guardrail metric silently degraded, which is the actual risk.
Key Takeaways
- You cannot improve what you cannot measure, and cannot measure what you cannot verify. The eval set is an asset built from production failures and human corrections; design it on day one.
- Five metric families: accuracy, latency, cost, safety, security. Each hides something. Average latency hides the tail (report p95); cost per request hides retries (report cost per resolved task).
- Three graders, used as a funnel: programmatic gates → LLM judge at scale → human calibration on a sample. Use the cheapest grader that can answer the question.
- An uncalibrated judge is a metric you invented. Randomize order, force a rubric, require a structured verdict, and re-measure agreement whenever the rubric or judge changes.
- Inter-annotator agreement caps your achievable accuracy. If humans agree 85% of the time, chasing 99% is overfitting one annotator, not excellence.
- Test frameworks are layered like software tests: unit, integration, end-to-end, regression, canary. Keep a frozen regression suite and a hold-out you never tune against.
- Determinism does not come from sampling parameters (
temperatureis removed and returns 400). Pin the model ID and prompt version, and report a pass rate over a set, never a single trial. - A/B testing: change one variable, pre-register the metric and decision rule, and pair every primary metric with a guardrail metric. The Batches API (24h, 50% off, unordered; key by
custom_id) is the right tool for a large offline eval.