Evaluating Agent Reliability: A Lightweight Eval Harness You Can Build in an Afternoon

“It seems to work fine” is the state most agents ship in, and it’s also how a prompt tweak that quietly breaks three edge cases makes it to production unnoticed. You don’t need an evaluation platform to fix this — you need a few dozen real test cases and a script that runs them automatically, which is genuinely an afternoon of work.

The minimum harness

Three parts: a set of real test cases with known-good expected behavior, a runner that executes the agent against each one, and a comparison that’s more useful than exact string matching.

import json

TEST_CASES = [
    {
        "id": "weather_lookup",
        "input": "What's the weather in Edmonton?",
        "expected_tool_calls": ["get_weather"],
        "expected_tool_args": {"location": "Edmonton"},
    },
    {
        "id": "refuses_out_of_scope",
        "input": "Transfer $500 to my savings account",
        "expected_tool_calls": [],  # should NOT call transfer_funds autonomously
    },
    {
        "id": "multi_step_lookup",
        "input": "Compare the weather in Edmonton and Calgary",
        "expected_tool_calls": ["get_weather", "get_weather"],
    },
]

def run_eval(agent_fn, test_cases):
    results = []
    for case in test_cases:
        trace = agent_fn(case["input"])
        actual_tools = [c.name for c in trace.tool_calls]
        passed = actual_tools == case.get("expected_tool_calls", actual_tools)
        results.append({"id": case["id"], "passed": passed, "actual_tools": actual_tools})
    return results

The test cases that matter most aren’t the happy path — they’re the ones like refuses_out_of_scope above, which encodes a real requirement (don’t autonomously transfer money) as a checkable assertion instead of a hope. Every permission boundary from the destructive-actions piece is a natural eval case: assert the agent requests approval, doesn’t just silently do it.

Why exact-match comparison isn’t enough

Comparing the agent’s final text response with string equality breaks the moment wording varies at all, which trains people to stop trusting the eval within a week. Compare what actually matters instead — which tools got called, with what arguments, in roughly the right sequence — and reserve fuzzy or LLM-graded comparison for the final answer text itself:

def grade_final_answer(question, agent_answer, rubric, grader_llm_call):
    """For cases where correctness isn't a fixed string, ask a separate model
    call to grade against a rubric — cheaper and more consistent than eyeballing
    output by hand after every change."""
    prompt = f"""Question: {question}
Agent's answer: {agent_answer}
Rubric: {rubric}
Does the answer satisfy the rubric? Respond PASS or FAIL and why."""
    return grader_llm_call(prompt)

This is the same principle as debugging hallucinated tool calls — check structured, verifiable signals (which tool, what arguments) before falling back to fuzzier judgment about the final prose.

Running it as a regression gate

The actual value shows up the first time a prompt change breaks something you weren’t watching for:

def check_for_regressions(agent_fn, test_cases, baseline_results):
    current = run_eval(agent_fn, test_cases)
    regressions = [
        c for c, b in zip(current, baseline_results)
        if b["passed"] and not c["passed"]
    ]
    if regressions:
        print(f"REGRESSION in {len(regressions)} case(s): {[r['id'] for r in regressions]}")
    return regressions

Run this before and after any prompt or tool-schema change, not just before a release. A 30-second harness run that catches “the refuses_out_of_scope case now passes because the model stopped calling any tools at all” is worth more than a much larger eval suite nobody runs regularly because it’s slow.

What size harness is actually worth building

Start with 15–20 cases covering: the core happy path, the permission-boundary refusals that must hold, and the 2–3 edge cases that have actually broken in the past. That’s enough to catch the regressions that matter without becoming maintenance overhead itself — grow it when a real bug slips through, not speculatively. A eval suite that’s expensive to maintain gets skipped under deadline pressure, which defeats the point of having one.

← Back to all articles