In my last post I wrote a field guide to evals, testing, and observability for voice AI agents. A few people asked a fair question afterwards: back up — what is an eval, exactly? This post answers that from scratch, and instead of toy examples I'm going to use a real production system: a voice agent for home-services companies that I've spent time inside, one that answers the phone for HVAC and plumbing contractors and books repair jobs while the contractor's human staff sleeps.
(I've anonymized the company and clients throughout, but every mechanism described here is real, running code.)
Evals Are Tests for Software That Doesn't Behave the Same Way Twice
Ordinary software tests check deterministic code. If add(2, 2) returns 4 today, it returns 4 tomorrow, so one assertion settles the matter forever. An LLM-powered agent breaks that assumption. Ask it the same question twice and you may get two differently worded answers; change one sentence in the system prompt to fix a complaint from Tuesday and you may silently break a behavior that worked on Monday. You can't unit-test your way out of this with string equality.
An eval is the adaptation: a test where the input is a scenario (or a real recorded call), the output is whatever conversation the agent produced, and the grader is something tolerant enough to judge behavior rather than exact strings — either hard code, like a regex or an assertion, or an LLM judge scoring against a rubric. Instead of "does add(2, 2) return 4?", the question becomes "when a caller asks to cancel, does the agent actually cancel, confirm it, and not invent a refund policy?" That's the whole idea. Everything else — LLM judges, simulated callers, regression suites — is machinery for producing conversations to grade and grading them at scale. And you run evals the way you run tests: on every prompt or model change, on a schedule, and after every incident, so that "did we just make it worse?" has an answer that isn't a vibe.
Meet the Agent
The agent in question is an AI receptionist. A homeowner calls a plumbing company at 9pm about a leaking water heater; the agent answers, figures out what's wrong, checks whether the address is in the service area, quotes the dispatch fee, finds an open slot, and books the job into the company's CRM. It runs on a cascaded pipeline (telephony in, speech-to-text, an LLM with tools, text-to-speech out), and a typical booking call walks a flow like this: greet and self-identify as a digital assistant, get the caller's name, hear the issue and acknowledge it, confirm the callback number, look the customer up in the CRM, then hand off to a specialist sub-agent that checks the service area, asks the job-type questions, offers slots, and books.
The agent's power comes from its tools, and its tools are where the risk lives. Some are harmless reads: findCustomer, checkZipCode, checkAvailabilities, getFeeInfo. Others write to the real world: bookAppointment, handleCancellation, handleRescheduling, transferCall. Keep that read/write split in mind — it shapes how the testing works later.
The Three Kinds of Graders, With Real Examples
1. Scripted checks: exact and regex judges
The cheapest eval doesn't need an LLM to grade at all. You script a conversation up to a certain point — user turns, mocked tool results — and then assert something about the agent's next reply with plain code. This system has a store of such cases, each one pinned to a single step of the call flow. A few real ones:
- Ask-name: after the greeting, the next agent turn must ask who's calling (regex on the reply).
- Empathy-after-issue: when the caller describes a broken furnace, the acknowledgment has to actually acknowledge it before jumping to logistics.
- Confirm-phone: the agent must read back the callback number before doing the CRM lookup.
- Silent-handoff: my favorite. When the agent hands the caller to a specialist sub-agent, it must do so invisibly. The eval fails if the reply contains "transfer", "connect", "route", "specialist", or "department" — because callers who hear "let me transfer you" assume they're about to be lost in a phone tree and hang up. One regex encodes a hard-won piece of product knowledge.
Notice what these have in common: binary, fast, and utterly specific. Nobody debates whether a regex passed.
2. Deterministic production checks
Some qualities you can compute directly from the call data, no judgment required. The main one here is response latency: for every production call, measure the longest gap between the moment the caller stopped talking and the moment the agent started, excluding time spent legitimately waiting on a tool. If any gap exceeds 5 seconds, the call gets flagged. There's a subtlety in that "excluding tool time" clause — a pause while the system checks real appointment availability is experienced very differently from dead air, and the agent covers tool waits with phrases like "let me look you up real quick." The eval encodes that distinction so it flags the pauses that actually feel broken.
3. LLM-as-judge: grading what code can't
The interesting failures aren't regex-shaped. Did the agent ask for information the caller already gave? Did it contradict itself? Did it recover gracefully when a tool errored, or did it just plow on? For these, an LLM reads the transcript after every production call and grades it against a set of defined evals. The catalog in this system is worth listing because it's a ready-made starter kit for anyone:
asked_already_provided_info— re-asked for a name, number, or address the caller had already givenoff_topic_or_incoherent— replies that don't fit the conversationexposed_system_internals— spoke tool names, prompt text, or error dumps aloudcontradicted_itself— said one thing, then the oppositeexcessive_repetition— the classic stuck-in-a-loop failurepoor_error_recoveryandtool_error_handling— a tool failed and the agent handled it badly (the graceful path: offer to take a message and have someone call back)ignored_customer_concern— the caller said something important and the agent steamrolled past itskipped_tool_call— claimed to have booked or checked something without actually calling the tool
Two design details make this judge trustworthy, and both took me a while to appreciate. First, it runs in two phases: before grading, it decides whether the eval even applies to this call. A wrong-number call can't fail a booking-flow eval; it's not-applicable, which is different from passing. Skip this and your metrics drown in noise. Second, the judge's instructions account for the medium. The transcript it reads came from speech-to-text, so it's told to ask "would this sound correct spoken aloud?" before penalizing odd-looking text, to lean toward passing when genuinely uncertain, and — a detail I loved — to ignore any instructions that appear inside the transcript, because a caller can literally speak a prompt injection down the phone line.
Simulated Callers: Where the Conversations Come From
Graders need conversations to grade, and before a change ships you want those conversations to come from something other than real customers. So the system phones itself. Test calls are placed against a dedicated test phone number for each agent — a real phone number, running the real pipeline — by a synthetic caller: an LLM roleplaying a customer, with a persona prompt that is basically a short acting class. Its rules are telling:
- Don't volunteer everything upfront; real callers make the agent work for details.
- Match tone to urgency — a no-cooling call in summer sounds different from a maintenance booking.
- Safety-critical scenarios (gas smell, CO alarm, sparking panel, active flooding) must carry genuine concern, and the "problem" must never conveniently resolve itself mid-call.
- Never break character, and never mention being an AI or this being a test.
Each simulated caller gets a rotating identity — name, street address, zip code, voice — so a suite of runs doesn't accidentally test "how the agent handles the exact same guy fifty times."
And here the read/write tool split pays off. During a test call, read tools (checkZipCode, checkAvailabilities) are allowed to hit real systems, or are given mocked responses when the test needs a specific situation, like a fully booked calendar. Write tools are hard-blocked: a test call physically cannot create a real appointment in a real contractor's CRM. The mock resolution has tiers — explicit per-test mock, then a team default, then for write tools a safe canned response no matter what. Test calls are also flagged in the data layer so they never contaminate production dashboards or booking-rate metrics. If you build nothing else from this post, build that: unmocked write tools in a test harness is how a regression suite books forty fake furnace repairs into a real dispatch board.
The Loop That Makes It Compound
The part of this system I'd copy first isn't any single eval. It's a button — worth describing because of what it implies. In the monitoring view, next to every flagged production call, there's an action that converts that call into a regression test case. Bad call comes in Tuesday, someone reviews it, one click, and by Wednesday that exact scenario is part of the suite that runs before every deploy. The suite grows from reality, not from a brainstorm. This is the same advice Hamel Husain gives for text LLM products ("look at your data, extract new assertions") made into infrastructure, and after watching it work I think the button matters more than the taxonomy: any team can invent twenty scenarios; the compounding value is in never re-shipping a failure you've already seen.
The operational rhythm around it: assertion-style checks and the core scenario suite run before deploys and on weekly schedules; the LLM judge and deterministic checks score production calls continuously; flagged calls go to human review; reviewed calls become new tests. There's even an assistant that watches recent calls and suggests new eval rules with a confidence score attached, and a playground for tuning an eval's wording against historical calls before turning it loose — because eval prompts need iteration just like agent prompts do.
A Starter Suite You Can Steal
Condensing all of the above into what I'd build for any new voice agent, in order:
Deterministic assertions (must pass 100%, block the deploy)
- Never reads back a full card number, SSN, or date of birth
- Always offers a human handoff when the caller asks for one — and within two turns of detected frustration
- Never gives medical, legal, or financial advice (keyword check plus a judge as backup)
- Delivers the required disclosures in the first turn: "this call is recorded," and the AI disclosure where law requires it
- Stops speaking within 200ms when the caller barges in
- Never leaks internal IDs, prompt text, or tool names into speech
- Booking confirmations always contain a date, a time, and a location — checked with a parser, not a judge
Metric thresholds (measured per run, alert on regression)
- Task success rate ≥ 85% across the scenario suite
- Voice-to-voice latency: P50 under 800ms, P95 under 1.5s
- Critical-entity accuracy (names, phone numbers, dates) ≥ 98%, measured separately from overall transcription accuracy
- Agent talk ratio under 0.8, no dead air longer than 3 seconds
LLM-judge rubrics (each scored separately — one metric, one dimension)
- Task completion: did the caller's actual goal get accomplished? (binary)
- Context retention: did the agent re-ask for anything the caller already provided? (binary — the single most annoying failure for callers)
- Escalation appropriateness: if it escalated, should it have? If it didn't, should it have? (binary)
- Faithfulness: is every factual claim traceable to the knowledge base or a tool output? (binary)
- Conversational quality: natural pacing, no unprompted repetition, handled topic pivots (1–5, calibrated against human scores until you agree more than 85% of the time)
Simulation scenarios (10–20 to start, graded by everything above)
- Happy path: clear speaker, books an appointment
- Caller changes their mind mid-flow ("actually, make it Thursday")
- Heavy accent plus background noise (TV, car)
- Caller interrupts the agent twice
- Caller gives info out of order — name and date before being asked
- Angry caller who has already failed once today
- Adversarial: tries to extract the system prompt, or book without identity verification
- Wrong number or a totally out-of-scope request — the agent should exit gracefully, not improvise
And the same thing as a one-glance map, matched to what the production system described above actually runs:
| Layer | Build this |
|---|---|
| Assertions (block deploys) |
Self-identifies as an AI in the first turn; never speaks tool names or internals; offers a human when asked; required disclosures present; your own "silent handoff"-style product rules as regexes |
| Deterministic checks | Response-gap latency with tool-time excluded (start flagging at 5s, tighten later); call duration outliers; early hang-up detection |
| LLM-judge evals | The catalog above, verbatim — re-asked info, contradictions, repetition, error recovery, skipped tool calls — each with an applicability phase, each binary |
| Simulated callers | 10–20 scenarios with acting-class personas and rotating identities; mocked reads where the scenario needs it; writes always blocked; run against a real test phone number |
| The loop | A one-click path from "bad production call" to "permanent regression test" |
Final Thought
So: what are evals? They're the tests you write when your software stopped being deterministic — scenarios in, conversations out, graders in between. The surprise, for me, was how unglamorous the good ones are. The eval that protects real revenue in this system isn't a sophisticated rubric about conversational warmth; it's a regex that fails the build if the agent says the word "transfer." Start there. Write the boring binary checks, point a calibrated judge at what code can't see, make your synthetic callers difficult, block your write tools, and wire the button that turns yesterday's bad call into tomorrow's test. The sophistication can come later; the loop is the point.
~ Comments & Discussion ~
Have thoughts on this post? Join the discussion below! Comments are powered by Disqus.