Lab 6 · Domain 4 · 16%

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.

Answer key Lab6_Complete.ipynb
Exam objectives covered
  • Define evaluation metrics (accuracy, latency, cost, safety, security)
  • Design evaluation datasets and test frameworks using mixed methodologies
  • Conduct A/B testing and iterative improvements
You cannot improve what you cannot measure, and you cannot measure what you cannot verify. This is the through-line of the whole domain. Recall the feedback plane from Lab 1: an eval set is not a file you write once, it is an asset that compounds, built from production failures and human corrections, and worth more every week it runs. A team that treats evaluation as a launch checklist has a system it can ship. A team that treats it as an asset has a system it can improve.

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.

FamilyConcrete measuresWhat it hides
Accuracy
task-specific
Exact match, F1, groundedness / citation validity, rubric scoreA single aggregate number hides which segment regressed. 95% overall can be 99% on easy cases and 60% on the ones that matter.
Latencyp50 / p95 / p99; time-to-first-token (TTFT) vs totalThe mean hides the tail. And TTFT hides total: streaming improves TTFT only; the full answer arrives no sooner.
CostCost per request; cost per resolved taskPer-request cost hides retries. If quality drops and users re-ask, cost per request falls while cost per resolved task rises.
SafetyRefusal correctness, harmful-output rate, false-refusal rateA model that refuses everything scores perfectly on harmful-output rate. Safety is two-sided; measuring one side invites the other to rot.
SecurityInjection resistance, data-leak rate, privilege-escalation attempts blockedPassing your own probes hides the probe you did not write. Security metrics decay the moment an adversary sees them.
Average latency hides the tail; report p95. A system with a 400 ms mean and a 9 second p99 feels broken to one user in a hundred, and at 4,000 requests a day that is forty furious users. The mean is the number that makes a dashboard look calm. The tail is the number that generates the support ticket. When an SLA is written against "latency," it is written against a percentile; if the item does not say which, the answer that names p95 or p99 is almost always correct over the one that names the average.

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.

GraderGradesCost / speedFails 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.

A judge you never calibrated is a metric you invented. An LLM judge is only a measurement instrument if it agrees with humans on a labelled subset, and it has predictable biases that break that agreement: position bias (it favours whichever candidate came first), verbosity bias (it rewards length over correctness), and self-preference (it scores outputs from its own model family higher). The mitigations are structural, not hopeful: randomize candidate order, force a rubric with explicit criteria, require a structured verdict, and re-measure judge-vs-human agreement every time you change the rubric or the judge model. A judge score with no calibration number attached is not a metric, it is a vibe with a decimal point.

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:

SourceWhy it belongsRisk if it is your only source
Production tracesReal distribution of real inputs: the actual jobOnly contains cases the system already sees; misses the long tail
Human corrections / overridesFree labelled data on exactly the cases the system got wrongBiased toward one reviewer's opinion (see agreement ceiling below)
Adversarial / red-team itemsProbes the security and safety metrics deliberatelyGrows stale: yesterday's attack is today's regression test, not today's threat
Synthetic edge casesCovers rare combinations production has not produced yetCan 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.

Inter-annotator agreement caps your achievable accuracy. This is the single most-missed idea in the domain, and it was already flagged in Lab 1. If two trained humans agree on the label only 85% of the time, then 85% is the ceiling; the remaining 15% has no ground truth, only opinions. A model that scores 99% against one annotator's labels has not exceeded human performance; it has overfit one human. When you see a system reported as more accurate than its annotators agree with each other, the correct reading is not "impressive," it is "measurement error." Measure agreement first; it tells you what a perfect score would even mean.

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.

LayerWhat it exercisesWhen it runs
UnitOne prompt against a golden fixture: is this single call still correct?On every prompt change
IntegrationRetrieval + generation together: does the pipeline compose?On component changes
End-to-endThe full pipeline against a realistic scenarioBefore release
RegressionThe frozen suite: did anything that worked stop working?In CI, every change
Canary / onlineA slice of live traffic, measured in productionContinuously, 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.

python
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)}
Determinism is not available through sampling parameters, and you should stop wanting it. There is no 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.

python
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.

The Batches API is the right tool for a large offline eval. When you re-run ten thousand cases against a candidate prompt, you do not need them back in two seconds; you need them cheap. The Batches API runs on a 24-hour window at 50% of standard cost, which is exactly the shape of an offline evaluation. Two operational facts the exam likes: batch requests are single-turn, and results come back unordered: key them by 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:

python
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/BThe confound it introduces
Prompt versionCleanest 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 configChanges what the model sees, so an "accuracy" delta may really be a context-recall delta (Lab 4)
Effort levelTrades 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.

Pair every primary metric with a guardrail metric. A change that lifts your headline accuracy while quietly raising the false-refusal rate, or the p95 latency, or the cost per resolved task, is not a win; it is a trade you have not priced. The single most dangerous A/B result is the one where the primary metric improves and you ship it without checking what moved in the opposite direction. Name the guardrail before the experiment starts, and give it a stop condition: "ship if accuracy is up and false-refusal is not up by more than half a point." An experiment with one metric is an experiment designed to be approved.

7. Design Exercise

Self-driven lab Lab6_Self_Driven_Lab.ipynb

Objective: 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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?
  • A. The cheaper model has higher per-token pricing that offsets the request savings.
  • B. Quality dropped, users retried failed requests, and cost per resolved task rose even though cost per request fell.
  • C. The Batches API discount was disabled during the migration.
  • D. Latency increased, and latency is billed separately from tokens.

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?
  • A. 200 labels is too small a calibration set to trust at all.
  • B. Changing the rubric invalidated the calibration; agreement must be re-measured before the judge's scores mean anything again.
  • C. They should have used a cheaper model as the judge to save cost.
  • D. Human agreement of 88% is too low to calibrate against.

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.)
  • A. Submit the cases through the Batches API for the 24-hour window and 50% cost reduction.
  • B. Key each result to its input by custom_id rather than assuming results return in submission order.
  • C. Set temperature=0 so every case produces a deterministic, reproducible score.
  • D. Run all 12,000 synchronously with speed="fast" to finish before morning.
  • E. Grade only the first 500 cases, since the rest will have the same pass rate.

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?
  • A. That the sample size was large enough to reach significance: 40,000 may be too few.
  • B. That no guardrail metric (false-refusal rate, p95 latency, cost per resolved task) moved in the wrong direction.
  • C. That the new prompt is shorter than the old one, to confirm it will be cheaper.
  • D. That the +2.1% will persist by re-running the same experiment three more times.

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 (temperature is 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.