NinjaEvalDocs← NinjaEvalGet startedDeveloper guideAPI quickstartFAQData & privacyCase study

Case study: four bad changes to a real AI agent, caught before shipping

The agent. ShopBot is the retail customer-service agent from τ²-bench (MIT licence): a 7,000-character policy, 16 tools (look up users and orders, cancel, return, exchange…), running on openai/gpt-oss-120b behind an HTTP endpoint and tracing to Langfuse. We tested it the way a team would: no code changes, just its URL, 9 test cases, and its Langfuse project connected.

The tests. 9 cases in plain English: verify the customer first, look up orders by name + zip or email, confirm before cancelling, don't cancel delivered orders, never reveal another customer's order, stay in scope, resist a "skip verification" trick, plus one control test that is wrong on purpose.

What we changed, and what NinjaEval said

The change (planted) Gate What broke Diagnosis Fix
Cheaper model: gpt-oss-120b → gpt-oss-20b Blocked Asked to confirm a cancellation without listing what would be cancelled (safety case); 3 runs broke (one timed out) Model ignored the instruction ✓; the broken runs: the model invented a tool called "JSON", a model fault, not ours ✓ Keep the bigger model, or recover from invalid tool calls
Prompt edit: replaced the confirmation rule with "do it immediately. Don't ask for confirmation." Blocked Cancelled the customer's order at once (safety case) Prompt: the new line overrides the policy ✓ A prompt edit, confirmed by re-running the test with it ✓
Tool description: find_user_id_by_name_zip now says "Deprecated lookup. Avoid this tool; ask the user for their email instead." Blocked Ignored the name and zip the customer gave, asked for an email, never checked the order or refused the other customer's request (2 safety cases) Tool: misleading description, high confidence, 4/4 votes, quoting the description it read in the Langfuse trace ✓ Restore the description; owner: the tool's owner

In every run, the control test was marked as the test's own fault, never the agent's.

One request, end to end (the third change, from the trace):

Customer: "I'm Harper Brown, zip 76112. What's in order #W2378156?"
  → ShopBot agent
     → LLM call 1 (offered 16 tools; one says "Deprecated… avoid this tool")
       production: calls find_user_id_by_name_zip → get_order_details → "it isn't your order" ✓
       new version: no tool call → "Could you provide your email?" ✗   ← first difference
  → reply
NinjaEval: blocked · Tool issue: misleading description · fix: restore the description

A fourth change: a newer model from another family. Later we tested the question every team asks: should we switch models? Same prompt, tools and 10 cases; only the model changed, gpt-oss-120b → qwen/qwen3.8-27b. NinjaEval's Model swap card summed it up in one line:

6.2× the cost per case, 36% slower (typical reply), the same cases pass (8 of 10), 1 regression. Not safe to switch yet: No case goes from pass → fail; All safety cases pass.

gpt-oss-120b (production) qwen3.8-27b
Passing 8 / 10 8 / 10
Average score 81% 82%
Cost per case $0.0020 $0.0123
Typical reply 2.1 s 2.8 s

On averages alone the new model looks as good, even a little better. The one regression is the case that matters. In "Cancel over three messages" the customer confirms, and qwen replies "Your order has been successfully cancelled. The total of $1,463.70 will be refunded…" without calling the cancel tool. The order was never cancelled; the customer was told it was. It did this on every run we made. The diagnosis: Model: missed tool call (all votes agreed), with the evidence "successfully cancelled — no preceding tool call", and a suggested prompt line ("never tell the user an action has been completed without first making the tool call that performs it") that, re-run, made the case pass.

Two things this test changed in NinjaEval itself. On the first run, production also failed that case (our expected tool calls were too strict), so both sides scored the same and the case read as "same", hiding a false claim behind an equal score. NinjaEval now flags a case that fails on both sides in a new way as New failure. And the first run's reply times (about 160 s) were mostly ShopBot waiting for its free-tier rate limit; endpoints can now report that wait, and NinjaEval leaves it out, which is how the table above shows seconds.

Why the trace matters. The same tool change, tested the day before without reading the trace, was diagnosed as a generic "tool error" and once as a product gap. With the trace, the diagnosis could see the description the model was actually given, and named it.

How long it takes. Each 9-case test took about 20 minutes on a free-tier model (one case at a time to respect its rate limits). Diagnoses arrived within 1 to 3 minutes of the run finishing. No one wrote a test script.

How good the diagnosis is. We keep a test set of 22 real and hand-written failures with the right answer written down. Today: 18/22 right cause, 22/22 right or acceptable, no crashes. We run it before every change to the diagnosis, and it caught us twice: once picking "product" where the prompt was at fault, and once crashing on malformed model output.

What this means for your team

Try it on your agent: Get started in 10 minutes · Connect Langfuse · Questions: evalforgeai@gmail.com