All case studies

01Deep dive · evals

Testing non-deterministic systems: a 26-conversation eval suite

You can't check a language model's exact words, but you can check what it must never do. How I turned 'is the agent lying about prices?' into a suite that runs in five minutes and stops a deploy cold.

In productionDavid RochaPart of the ZapBeta system
01

The problem

You can't test a language model with a string assert. Ask the same thing twice and the wording changes, the suite goes red over nothing, and sooner or later someone switches it off. But wording that varies isn't the same as anything goes. The agent can say a price a hundred different ways, as long as the price exists in the catalog. It can confirm a slot in its own words, as long as the slot was actually booked. That line is what I turned into a test.

02

Constraints

  • 01Deterministic enough to block a deploy. A suite that fails one run in five is worse than no suite: the team learns to ignore the red.
  • 02Fast. It runs on every change that touches the agent, so half an hour of runtime means nobody runs it.
  • 03Assert on behaviour, not on the sentence. "Did it quote R$ 54.90?" breaks for nothing; "did it quote a price that exists in the catalog?" is what I need to hold.
  • 04Real agent, real tools. Mocking the model tests the mock. The suite drives the same runner that answers WhatsApp.
03

Architecture

  1. cases.ts

    26 scripted conversations + the company facts the agent may know

  2. harness.ts

    drives the real agent runner, records reply + full tool trace

  3. checks.ts

    invariant checks run turn by turn against reply + trace

  4. npm run eval

    deterministic, ~5 min; a failure is a red build

The checks are the part that matters. provaSupervisor kills any price, total or product that isn't in the catalog. provaNomeInterno fails if the agent admits to being an "AI" instead of staying the specialist it is supposed to be. provaDiaDaSemana checks that the weekday it wrote actually matches the date. And provaPedido decodes the Pix code it sent — Pix being Brazil's instant-payment system — parsing the EMV payload, recomputing the CRC16 and checking the amount. If that code comes out wrong, the customer finds out at the bank, not me.

04

The hard part

The checks were easy to write, and I trusted them too much. I built a supervisor that flagged as a lie any number on screen the agent hadn't just fetched from a tool. First full run: 126 accusations. It looked thorough. It was almost all wrong — the agent had fetched those numbers, just not in the shape the guard expected.

Before letting that guard block a deploy, I measured it against the traces, looking at what the agent actually did that turn, and tuned it until the accusations matched reality. A guard is a hypothesis until someone measures it. And one that cries wolf 126 times gets switched off in the first week.

05

Trade-offs

  • Scripted, not generative. I write the customer turns myself, so coverage only goes as far as my imagination. I chose depth on the failure modes that cost money.
  • Behavioural asserts miss tone. The suite proves the agent didn't lie about a price; it doesn't prove it was warm. That one I read in the transcript.
  • Five minutes is a deploy-gate budget, not a pre-commit one. It runs in CI, not on every keystroke.
06

What I'd do differently

There's one thing I'd invert. Instead of writing the conversation and asserting invariants on top of it, I'd let the model generate adversarial customers against a fixed set of invariants: a customer whose only goal is to get the agent to quote a price that doesn't exist. The script proves the bugs I imagined are gone; a generated adversary would find the ones I didn't. I know the shape of that harness now. I didn't when I started, and shipping the scripted version first was the right call.

Let's talk

I'm open to remote work building with LLMs: agents, evals, tooling, the whole product around them. I'd be glad to show you a lot more on a call. Just reach out.

Available for remote roles, working from Brazil.Time zone: UTC−3 (Brazil)