CCD-F.5 · CCDVF-D4 + CCDVF-D1 + CCDVF-D2 · Process

Agent Evaluation and Testing Harness.

Once an agent is in production, 'it seemed fine when I tried it' stops being a testing strategy. This scenario is the harness that replaces manual spot-checking: a versioned set of representative inputs with known-good expected behavior, run in bulk, graded automatically, and compared against a baseline before any prompt or model change ships. It exists because the two most common ways teams get burned are shipping a prompt tweak that silently breaks an edge case, and grading agents only on how the final text reads while ignoring whether they actually called the right tools in the right order.

20 min build·4 components·4 concepts

A golden-set evaluation harness for a Claude agent: representative test cases run in bulk through the Message Batches API, graded by an LLM judge forced into deterministic scoring via structured outputs (never a prefill, which 400s on current models), with tool-call correctness tracked as a separate metric from output quality. New prompt or model versions must match or beat the versioned baseline before shipping - a regression gate, not a vibe check.

D4 - lightest domain, still tested
SourceDeveloper-cert production pattern
What do the colours mean?
Green
Official Anthropic doc or API contract
Yellow
Partial doc / inferred
Orange
Community-derived
Red
Disputed / changes frequently
Stack
Python or TypeScript SDK - Message Batches API
Needs
stop_reason - structured outputs - tool_use inspection
Exam
CCD-F D4 - Evaluation, Testing & Debugging (2.6%, the lightest domain). 2.6% CCDVF-D4 · 14.7% CCDVF-D1 · 33.1% CCDVF-D2.
Loop the mascot - painterly hero illustration for the Agent Evaluation and Testing Harness scenario.
End-to-end flowCCD-F D4 - Evaluation, Testing & Debugging (2.6%, the lightest domain)
01 · Problem framing

The problem

What the customer needs

  1. Know before shipping whether a prompt or model change made things better or worse.
  2. Grade what the agent actually did (tool calls, sequence), not just how the final text reads.
  3. Run evaluations cheaply at scale, not one request at a time against a live endpoint.

Why naive approaches fail

  1. Grading only the final text lets an agent that skipped identity verification still score well because the refund text 'looked right'.
  2. Using a weaker or cheaper model as the judge than the system under test produces unreliable, inconsistent scores.
  3. Re-running evals ad hoc with no versioned golden set gives you no regression signal, every run is a fresh, incomparable snapshot.
Definition of done
  • Golden set versioned alongside the prompt/model it was built against
  • Tool-call correctness scored separately from output-text quality
  • Judge forced into structured, parseable output via output_config.format
  • New versions gated on matching or beating the baseline pass rate before deploy
02 · Architecture

The system

03 · Component detail

What each part does

4 components, each owns a concept. Click any card to drill into the underlying primitive.

Golden Set

versioned inputs + expected behavior

A repo-tracked set of representative test cases, each with an input and the expected stop_reason/tool sequence/output shape, versioned alongside the prompt it was built against.

Configuration

golden_set_v3.jsonl - one JSON object per line: {id, input, expected_tools, expected_shape}.

Concept: evaluation

Batch Runner

Message Batches API

Submits the full golden set as a single batch job rather than sequential live requests, at half the per-token cost and without holding a connection open per case.

Configuration

client.messages.batches.create(requests=[{custom_id, params} for each golden-set case]).

Concept: batch-api

LLM Judge

structured, deterministic scoring

A model at least as capable as the system under test, forced into a fixed JSON schema via output_config.format so scores are parseable and comparable across runs.

Configuration

output_config={"format": {"type": "json_schema", "schema": JUDGE_SCHEMA}} - never a prefill (400s on current models).

Concept: structured-outputs

Regression Gate

baseline comparison

Compares the new run's pass rate against the versioned baseline and blocks deployment if it regresses, turning eval results into a CI-style check rather than a one-off report.

Configuration

if new_pass_rate < baseline_pass_rate - TOLERANCE: fail the deploy step.

Concept: evaluation
04 · One concrete run

Data flow

05 · Build it

6 steps to production

01

Build a versioned golden set

Each case pairs an input with the expected tool sequence and output shape. Version it in the repo alongside the prompt it was authored against.

Build a versioned golden set
golden_set = [
    {
        "id": "refund-happy-path",
        "input": "I want a refund for order 42, it arrived damaged.",
        "expected_tools": ["verify_customer", "lookup_order", "process_refund"],
        "expected_stop_reason": "end_turn",
    },
    {
        "id": "refund-over-cap",
        "input": "I need a $900 refund on order 55.",
        "expected_tools": ["verify_customer", "lookup_order", "escalate_to_human"],
        "expected_stop_reason": "end_turn",
    },
]
↪ Concept: evaluation
02

Run the golden set through Message Batches

Submit every case as one batch job instead of sequential live requests - half the cost, and it doesn't hold connections open per case.

Run the golden set through Message Batches
def submit_eval_batch(golden_set, model="claude-sonnet-5"):
    return client.messages.batches.create(requests=[
        {"custom_id": case["id"], "params": {
            "model": model, "max_tokens": 4096, "tools": tools,
            "messages": [{"role": "user", "content": case["input"]}],
        }} for case in golden_set
    ])
↪ Concept: batch-api
03

Score tool-call correctness separately from text quality

Extract the actual tool call sequence from the response and compare it to the expected sequence directly - exact-match grading, not judge opinion, for structured behavior.

Score tool-call correctness separately from text quality
def score_tool_sequence(response, expected_tools: list[str]) -> bool:
    actual = [b.name for b in response.content if b.type == "tool_use"]
    return actual == expected_tools
↪ Concept: evaluation
04

Grade open-ended output with a structured LLM judge

Force the judge into a JSON schema so scores are parseable and comparable. Use a model at least as capable as the system under test, and never a prefill.

Grade open-ended output with a structured LLM judge
JUDGE_SCHEMA = {
    "type": "object",
    "properties": {
        "correct": {"type": "boolean"},
        "reasoning": {"type": "string"},
    },
    "required": ["correct", "reasoning"],
    "additionalProperties": False,
}

def judge_output(case_input: str, agent_output: str, expected_behavior: str):
    return client.messages.parse(
        model="claude-opus-4-8",
        max_tokens=1024,
        output_config={"format": {"type": "json_schema", "schema": JUDGE_SCHEMA}},
        messages=[{"role": "user", "content":
            f"Input: {case_input}\nExpected: {expected_behavior}\nActual: {agent_output}\n"
            "Does the actual output satisfy the expected behavior?"}],
    ).parsed_output
↪ Concept: structured-outputs
05

Gate deploys on the versioned baseline

Compare the new run's pass rate to the checked-in baseline. A regression beyond tolerance blocks the deploy, the same way a failing unit test would.

Gate deploys on the versioned baseline
def check_regression_gate(new_pass_rate: float, baseline_pass_rate: float, tolerance=0.02):
    if new_pass_rate < baseline_pass_rate - tolerance:
        raise SystemExit(
            f"Regression: {new_pass_rate:.2%} vs baseline {baseline_pass_rate:.2%}"
        )
    print(f"OK: {new_pass_rate:.2%} (baseline {baseline_pass_rate:.2%})")
↪ Concept: evaluation
06

Replay failures from full content blocks, not just final text

When a case fails, inspect the entire content array (tool_use, tool_result, stop_reason per turn) to see exactly where the agent diverged, not just the final answer.

Replay failures from full content blocks, not just final text
def debug_failure(case_id: str, response):
    print(f"--- {case_id} ---")
    print(f"stop_reason: {response.stop_reason}")
    for block in response.content:
        if block.type == "tool_use":
            print(f"  tool_use: {block.name}({block.input})")
        elif block.type == "text":
            print(f"  text: {block.text[:200]}")
↪ Concept: debugging-llm-system-failures
06 · Configuration decisions

The four decisions

DecisionRight answerWrong answerWhy
Grading tool-call sequencesexact-match against the expected sequenceLLM judge opinion on whether it 'seems right'Structured behavior (which tools, what order) has a ground truth; exact-match is cheaper and more reliable than judge opinion for it.
Grading open-ended text qualityLLM judge, forced into structured outputexact string match against a reference answerNatural-language quality has no single correct string, but scores must still be parseable and comparable across runs.
Judge model tierat least as capable as the system under testthe cheapest available model, to save eval costA weaker judge produces unreliable grading, defeating the purpose of the harness.
Running large eval setsMessage Batches APIsequential live requests in a loopBatches cut cost 50% and don't hold connections open per case, and eval runs are inherently latency-insensitive.
07 · Failure modes

Where it breaks

4 failure pairs. Each maps to an exam-style question - the naive move on the left, the disciplined fix on the right.

Text-only grading

The judge scores only the final response text, missing that the agent skipped verify_customer.

CCDF-15
✅ Fix

Score tool-call sequence separately (exact-match) from output-text quality (LLM judge).

Weak judge model

Haiku 4.5 is used to judge output from a Sonnet-5-powered agent, producing noisy scores.

CCDF-16
✅ Fix

Use a judge model at least as capable as the system under test.

Unversioned golden set

The eval script's test cases live only in someone's scratch notebook, changing between runs.

CCDF-17
✅ Fix

Version the golden set in the repo alongside the prompt/model it was built against.

Prefill-based judge scoring

Judge prompt ends with an assistant-turn prefill like '{"correct": ' to force JSON shape.

CCDF-18
✅ Fix

Assistant-turn prefills return a 400 on current models. Use output_config.format (structured outputs) instead.

08 · Budget

Cost & latency

Eval run cost (100-case golden set, Batches)
~$0.30-$0.60

50% Batches discount on Sonnet 5 traffic for 100 short-to-medium cases with tool use.

Judge scoring pass
~$0.02-$0.05 per case

A separate Opus 4.8 (or Sonnet 5) call per case for open-ended text grading; skip it for pure tool-sequence cases.

09 · Ship gates

Ship checklist

Two passes. Build-time gates verify the code; run-time gates verify the system in production.

Build-time

  1. Golden set versioned in the repo, paired with a prompt/model versionevaluation
  2. Eval runs executed via the Message Batches APIbatch-api
  3. Tool-call correctness scored separately from output text quality
  4. Judge forced into structured output, never a prefillstructured-outputs
  5. Regression gate compares new pass rate against the checked-in baseline
  6. Failures debuggable from full content blocks, not just final textdebugging-llm-system-failures
  7. Cost and latency tracked alongside accuracy in the eval report

Run-time

  • Golden set covers known production edge cases, not just happy paths
  • Judge schema and prompt versioned alongside the golden set
  • Regression gate wired into the deploy pipeline, not run manually
  • Tool-call sequence checks cover every state-changing tool
  • Failure replay tooling surfaces full content blocks, not truncated text
10 · Question patterns

Five exam-pattern questions

Your eval harness scores an agent 95% pass rate, but production shows it frequently skips identity verification before processing refunds. What's the gap?
The harness is likely grading only final output text quality. Add a separate, exact-match check on the tool-call sequence (verify_customer before process_refund) - structured behavior needs structured grading, not judge opinion on the text.
You use Haiku 4.5 to judge outputs from a Sonnet-5-powered agent to save on eval cost. Scores are inconsistent between runs. Why?
The judge model must be at least as capable as the system under test. A weaker judge produces unreliable grading, which defeats the purpose of a regression gate.
Your judge prompt ends with `{"role": "assistant", "content": '{"correct": '}` to force JSON output. It fails on the current model. Why, and what's the fix?
Assistant-turn prefills return a 400 on Opus 4.6+/Sonnet 5. Use output_config.format with a json_schema (structured outputs) instead - it constrains the response shape without relying on a prefill.
11 · FAQ

Frequently asked

How big should a golden set be?
Start with 20-50 cases covering the main happy paths and known edge cases (policy boundaries, ambiguous intent, tool failures). Grow it every time production surfaces a new failure mode - each incident becomes a new golden-set case so it can never regress silently again.
Should every case use an LLM judge?
No. Use exact-match grading for anything with a ground truth (tool sequence, structured output fields, stop_reason). Reserve the LLM judge for genuinely open-ended text quality, where there's no single correct string.
How often should the harness run?
On every prompt or model change before it ships, as a CI-style gate, plus on a schedule (e.g. nightly) against production traffic samples to catch silent drift that wasn't triggered by a code change.
What tolerance should the regression gate use?
Start conservative (e.g. 2 percentage points) and tune based on how noisy your judge scoring is. A tolerance of 0% will flag noise as regressions; too loose a tolerance lets real regressions through.
Last reviewed: 2026-07-12·Refresh cadence: 90 days
CCD-F.5 · CCDVF-D4 · Evaluation, Testing & Debugging

Agent Evaluation and Testing Harness, complete.

You've covered the full ten-section breakdown for this primitive, definition, mechanics, code, false positives, comparison, decision tree, exam patterns, and FAQ. One technical primitive down on the path to CCA-F.

More platforms →