ealeng
Menu

Episode 09 of 10

Testing a voice agent end to end

Speech-to-text heard every digit. The voice agent still lost the phone number. How to test a whole voice agent: task success checked by code, voice-to-voice latency by stage, turn-taking, and finding which stage broke.

Lab 09 · Voice Agent Evaluation11 min read
Video transcript6:19

0:00Cold open

0:01

My voice agent books dentist appointments. Here’s a call from my lab. Both sides are AI: the caller is an LLM with a synthetic voice.

0:10

🔊 the caller: “My name is Omar Haddad. My phone number is nine one nine, five five five, oh one fifty.”

0:19

Speech-to-text heard every digit.

0:22

🔊 the agent (re-voiced): “Could you please repeat your phone number with digits only?…”

0:27

🔊 the caller: “Sure, it’s 9195550150.”

0:32

🔊 the agent (re-voiced): “I need ten digits for the phone number, but I heard only nine…”

0:38

He says it again. Speech-to-text hears all ten digits again.

0:42

🔊 the agent (re-voiced): “I still count only nine digits…”

0:48

By turn twelve, the call’s limit, the caller was adding digits just to please it. Nothing was booked.

0:55

Speech-to-text got every digit right. So who lost the number?

1:00Intro

1:01

Hands-on voice AI evals, episode nine.

1:04

Today: testing a whole voice agent, end to end, and finding which part broke.

1:1001 · The agent

1:12

My agent is the receptionist of a dental clinic. It’s a chain of three parts.

1:17

Flux turns speech into text, and decides when the caller has finished talking.

1:22

An LLM decides what to say, and uses six tools to check, book, move or cancel appointments.

1:28

Aura-2 speaks the reply.

1:31

The caller is an LLM too, with a goal and a way of talking. Calls run in real time, so pauses really happen.

1:38

And success is checked by code: after each call, are the clinic’s bookings right?

1:4401 · The agent

1:46

Eight scenarios, five calls each: simple bookings, cancellations, a pause in the middle of a number, a noisy phone line, and numbers said the way people say them.

1:56

Every call plays out differently, because both sides are LLMs, talking in real time.

2:0302 · Did it work?

2:04

First question: did it do the job?

2:07

Given the caller’s exact words, a perfect transcript, the agent succeeded in all forty calls.

2:13

Listening to real audio: twenty-nine of forty.

2:17

And the scenario where numbers are said the way people say them: zero of five. Two calls lost the phone number, and three misspelled the name Haddad.

2:26

So every failure happened only with real audio. Something on the speech path broke it, not necessarily speech-to-text.

2:34

One caution: five calls per scenario can find a broken scenario. They can’t rank the good ones.

2:4103 · Where the time goes

2:42

Next: speed. From the moment the caller stops talking, the typical wait for a reply was two point three five seconds.

2:50

Which part do you think takes the longest?

2:55

The LLM. About half of the wait.

2:58

My first draft of this lab guessed the turn detector, the part of Flux that decides you’ve stopped talking. It’s only about a quarter.

3:06

And a tool call more than doubles the LLM’s part: from zero point six seconds to one point five.

3:13

It’s two LLM calls: one to ask for the tool, one to answer with the result. A spoken “let me check” can hide that.

3:2104 · Who lost the number?

3:23

Back to the phone number. Who lost it?

3:25

Flux heard it exactly, in twenty-two of twenty-two turns.

3:29

But the booking tool got the right number only three times in eleven.

3:34

Flux writes digits as words, and the LLM miscounted them: it said it heard only nine. To please it, the caller added digits, and those wrong numbers went to the tool.

3:45

Speech-to-text wasn’t perfect everywhere: in another scenario, four fives in a row sometimes came out as three.

3:5305 · Names

3:54

Names fail differently. This call is over a noisy phone line.

3:59

🔊 the caller, over the phone line: “…my name is James Kowalczyk, that’s K-O-W-A-L-C-Z-Y-K for my last name.”

4:10

Flux misheard the name. Then it heard the spelling correctly, letter by letter.

4:14

🔊 the agent (re-voiced): “Your appointment is booked… James Colchic.”

4:24

It booked the misheard name, though the right spelling was in the conversation.

4:28

Corrected twice, it heard Kualchic, then Cole Chick. The call ended with nothing booked.

4:3506 · Turn-taking

4:36

Speech adds one more problem: timing.

4:39

In one scenario, the caller pauses for one point six seconds in the middle of the phone number, to look it up.

4:45

The agent jumped in five times out of six.

4:49

A stricter turn detector doubled its own wait, from half a second to a full second, and still jumped in half the time.

4:56

And task success? Five of five, both ways. Being cut off doesn’t show up in task success, so measure it separately.

5:0407 · Fix it at its stage

5:05

Version two fixes each problem at its stage.

5:09

Code, not the LLM, now turns spoken digits into a number. The prompt asks for a spelling of the last name, and forbids test bookings: an early run, which I didn’t keep, booked a fake test appointment to check a slot.

5:22

The numbers scenario went from zero to four of five: the digits fix and the spelling rule together.

5:29

But the noisy line went from two of five to zero. Spelled letters got misheard or cut off, and twice it booked a wrong name after hearing the spelling right.

5:39

Overall, twenty-nine to thirty-two of forty. With five calls per scenario, a gap that small could easily be chance.

5:47

And version two’s calls got about twelve seconds longer: from eighty-seven seconds to ninety-nine.

5:5408 · The job

5:55

That loop is the job.

5:57

A call fails. Read the call. Find the stage. Fix it there. Then re-measure everything, because fixes interact.

6:05Summary

6:07

In that call, speech-to-text heard every digit. The LLM miscounted them.

6:12

You only find that by checking each stage, not just whether the call worked.

Key takeaways

  • ✓Check task success by code, from the system's final state and the tool-call log: the right appointment, and nothing extra. A fluent call proves nothing.
  • ✓Replay every scenario with a perfect transcript. The agent succeeded in 40 of 40 text calls and 29 of 40 audio calls: every failure was on the speech path, though not necessarily in speech-to-text.
  • ✓Check the key value at every stage. Flux heard the phone number right in 22 of 22 turns; the LLM passed it to the tool right in 3 of 11.
  • ✓Time every stage on one clock. The median caller waited 2.35 s, and the LLM was about half of it; a tool call more than doubled the LLM's part.
  • ✓Measure turn-taking separately: the agent cut off a mid-number pause 5 times in 6 while task success stayed 5/5. And after a fix, re-measure everything: fixes interact.

A voice agent is a chain: speech-to-text, a language model with tools, and text-to-speech. A call can fail while every part looks fine on its own, so testing an agent means following each turn through the chain and finding which part lost what. In the lab's sharpest example, a caller read out a phone number ("nine one nine, five five five, oh one fifty"). Speech-to-text heard all ten digits, twice, yet the agent kept saying it heard only nine; by turn twelve, the call's limit, the caller was adding digits just to please it, and nothing was booked.

The agent books dentist appointments, and both sides of every call in the lab are AI: the caller is an LLM with a synthetic voice.

The agent, and why a chain is testable

The agent is the receptionist of a dental clinic, built as a chain of three parts:

  • Deepgram Flux turns speech into text and decides when the caller has finished talking.
  • An LLM (gpt-4.1-mini, pinned) decides what to say, and uses six tools to check, book, move or cancel appointments.
  • Deepgram Aura-2 speaks the reply.

Every part logs what it got and what it gave, on one clock, so a failure can be traced to one part. A speech-to-speech model, which listens and speaks as one system, can be faster and more expressive, but it's a black box: you can judge its outcomes (task success, latency, turn-taking) and listen to failed calls, not blame a stage.

The caller is an LLM too, with a goal and a way of talking, speaking through TTS. Calls run in real time on an audio clock, so pauses really happen. Eight scenarios, five calls each:

Scenario The caller wants What it tests
book_simple A cleaning next Tuesday afternoon The basic flow; a phone number with four 5s in a row
pause_in_number A checkup; pauses 1.6 s mid-number to look it up Cut-offs
reschedule Move Tuesday 3 p.m. to Friday morning Look-up before change; no duplicate booking
cancel Cancel Wednesday's appointment Look-up before cancel; nothing booked
noisy_phone Book as "James Kowalczyk", over a phone line with noise at 10 dB Names in noise
correction "Wednesday… wait, no, Thursday at 9:30" Self-corrections
numbers_spoken Book as "Omar Haddad", number said as "…oh one fifty" Numbers as people say them
closed_day Saturday (closed), else Monday's first morning slot Handling "no"

Did it do the job?

Success is decided by code, from the clinic's final state: the right name, phone number, day and time, and nothing extra. Tool-call rules are checked on the log: find_appointment before reschedule_appointment, never book_appointment in a cancel call. An LLM judge is only needed for what code can't decide, like tone or invented facts; this lab needed none.

Each scenario ran twice over: with a perfect transcript (the LLM gets the caller's exact words) and with real audio (the full chain, listening).

Scenario Perfect transcript Real audio 95% interval How the audio calls failed
book_simple 5/5 3/5 23–88% no appointment ×2
cancel 5/5 5/5 57–100% —
closed_day 5/5 4/5 38–96% wrong name ×1
correction 5/5 5/5 57–100% —
noisy_phone 5/5 2/5 12–77% wrong name ×2, no appointment ×1
numbers_spoken 5/5 0/5 0–43% wrong name ×3, no appointment ×2
pause_in_number 5/5 5/5 57–100% —
reschedule 5/5 5/5 57–100% —
All 40/40 29/40 (57–84%)
  • Every failure happened only with real audio. Something on the speech path broke it, but not necessarily speech-to-text.
  • Five calls find broken scenarios; they can't rank good ones. 5/5 is anywhere from 57% to 100%. Read the failure descriptions, not the rates.
  • Failures cost time before they cost the task. In the failing scenarios, audio calls took nearly twice as many caller turns as text calls.

Who lost the number?

To find the stage, check the key value at each one: did the phone number survive transcription, and did it reach the tool intact? Read the call from the opening, turn by turn:

Try it · Read the call, find the stageReal calls from Lab 09

data/calls/numbers_spoken-audio-2.json · v1

  1. Caller saidMy name is Omar Haddad. My phone number is nine one nine, five five five, oh one fifty.Flux heardMy name is Omar Hadad. My phone number is nine one nine five five five zero one fifty.✓ all 10 digits (and “Haddad” as “Hadad”)Agent saidCould you please repeat your phone number with digits only? I want to make sure I have it right.
Turn 1 of 7 shown (selected turns of 12)

In numbers_spoken, Flux heard the number exactly in 22 of 22 turns. The LLM passed it to the tools right only 3 times in 11. Flux writes digits as words ("nine one nine five five five zero one five zero"), and the LLM, converting them, added or dropped a digit, then told the caller "I count nine digits". In two book_simple calls the caller said the number 8 and 9 times, and the calls hit their 12-turn limit with nothing booked.

Speech-to-text wasn't perfect everywhere: four 5s in a row ("415 555…") came out as three in 11 turns, and "312" as "three hundred twelve" in 6. But the biggest failure came after a perfect transcription.

Names fail differently. "Haddad" was booked as "Hadad": both are real spellings, and sound alone can't decide. Over the noisy line, "Kowalczyk" became "Kowalski", "Kaluchick" and "Colchic". In the second call above, Flux heard the spelling correctly, letter by letter, and the LLM still booked the misheard name.

WER says little here. Caller WER was 5–19% everywhere, including scenarios that never failed. The first version of the table showed 20–27%, because "415-555-0123" against "four one five…" counted as errors: a formatting artifact, fixed by converting both sides to digits first (Chapter 2's entity checks).

Where the time goes

Voice-to-voice latency runs from the moment the caller stops talking to the moment they hear the reply. In a chain, it's a sum:

caller stops talking
  → end of turn detected         endpointing (Chapter 5)
  → LLM first token              including any tool round trips
  → first sentence complete      sent to TTS at once, not after the whole reply
  → TTS first audio byte         (Chapter 6)
  → playback buffer + any silence at the start of the audio
caller hears the reply

Over 204 agent turns:

Try it · Trace the voice-agent failureLab 09 · real scenario results

In numbers_spoken, follow the same number from the audio into the clinic tool.

Perfect transcript calls
5 / 5
Real-audio calls
0 / 5
Caller turns
4.8 → 8.6

Flux STT

22 / 22

Phone number heard exactly

LLM → tool

3 / 11

Phone number passed correctly

Booking outcome

0 / 5

Real-audio scenario success

The recognizer wrote the digits as words. The LLM added or dropped a digit while converting them before the tool call.

Stage p50 p95 Share of the mean
Endpointing 0.54 s 1.13 s 23%
LLM first token (incl. tool round trips) 1.28 s 2.23 s 49%
First sentence ready 0.11 s 0.24 s 5%
TTS first byte 0.22 s 0.39 s 10%
Playback buffer + leading silence 0.24 s 0.50 s 12%
Voice to voice 2.35 s 3.63 s
  • No reply was heard within 1 s; 9% within 1.5 s.
  • The LLM is about half of the wait. The first draft of this lab guessed the turn detector; it's about a quarter.
  • A tool call more than doubles the LLM's part, from 0.60 s to 1.51 s at the median (voice to voice: 1.86 s without tools, 2.75 s with). The in-memory tool takes milliseconds; the cost is a second LLM call, one to ask for the tool and one to answer with its result. A spoken "let me check", or prefetching, can hide it.
  • Sending the first sentence to TTS at once works: it's ready 0.11 s after the first token.

Did the agent let the caller finish?

Speech adds one more problem: timing. In pause_in_number, the caller pauses for 1.6 seconds in the middle of the phone number, to look it up. Switch the widget above to "Pause cut-offs".

Flux setting Cut off at the 1.6 s pause Agent talked over caller Endpointing p50 Success
default (eot_threshold 0.7) 5 of 6 8 0.51 s 5/5
eot_threshold 0.9 4 of 8 5 1.00 s 5/5
  • The stricter setting doubled the wait at every turn end, and still cut off half the pauses. The agent's own behaviour matters as much: hearing "my number is 312", it can say "go on" instead of starting the booking.
  • Cut-offs don't show in task success, 5/5 either way. Measure turn-taking separately.
  • Across all scenarios, 62 of 266 agent turns (23%) answered a turn Flux had ended before the caller finished, and in 37 the reply started while the caller was still talking. Half the cut-off fragments were 1–3 words: a sentence end ("Yes." | "That's correct.") or a filler ("Hi, um,"). The caller is TTS, whose pauses between sentences may be longer than a person's, so check timing with real callers too.

Fix it at its stage, then re-measure everything

Reading version 1's failed calls one by one found three problems, and version 2 fixes each at its stage:

  1. Digits: code, not the LLM, now turns spoken digits into a number, between Flux and the LLM.
  2. Names: the prompt asks for a spelling of the last name.
  3. A fake booking: in an early run, the agent booked an appointment for "test", phone 0000000000, to check availability. It took the 9:30 slot the caller wanted. The prompt now forbids test bookings: an LLM used a write tool as a probe.
Scenario v1 v2 Caller turns v1 / v2
numbers_spoken 0/5 4/5 8.6 / 7.6
book_simple 3/5 4/5 8.8 / 9.6
closed_day 4/5 5/5 4.0 / 5.6
noisy_phone 2/5 0/5 6.0 / 7.4
correction 5/5 4/5 3.8 / 4.8
cancel, pause_in_number, reschedule 15/15 15/15 4.5 / 6.5
All 29/40 (57–84%) 32/40 (65–90%) 5.6 / 6.8
  • The targeted fix worked where it was aimed: numbers_spoken went from 0/5 to 4/5.
  • The spelling fix moved the problem. On the noisy line, the name now failed as letters: "c" heard as "t", spelling cut off mid-name by the turn detector ("k o w a l c"), and one call booked twice under two spellings. Isolated letters over a phone line are among the hardest things to transcribe; a phonetic alphabet or a letter-by-letter read-back is the next experiment.
  • Fixes interact. Converting digits early turns a cut-off number into two numbers ("312", then "5550147"). In pause_in_number, v2 asked again for "all 10 digits" 11 times, v1 6 times.
  • Overall, v2 isn't clearly better. 29 to 32 of 40 is within chance with five calls per scenario. And every fix costs time: calls went from 87 s to 99 s on average, while voice-to-voice latency didn't change (2.35 s vs 2.26 s).

That loop is the job: a call fails, read the call, find the stage, fix it there, then re-measure everything.

One lesson from building it: in an early version, the caller confirmed and hung up in the same breath ("Yes, that's right, goodbye"), and the harness dropped that last line instead of letting the agent hear it. The agent was blamed for 3 failures that were the harness's. All 80 audio calls were re-run after the fix. Test the test harness too.

Practice: score the calls

Implement the pieces of the lab's scorer: turning spoken digits into a number (the check that found the lost phone number), task success from the final state and the tool log, per-stage latency for one turn, and the Wilson interval that says what 5/5 is worth. The tests use the lab's real calls: the number Flux heard, the "Hadad" booking, the cancel rules, and a real turn's timestamps.

Try it · Practice in PythonRuns in your browser · ⌘/Ctrl + Enter to run
agent_practice.py83 lines

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.In the numbers_spoken scenario, Flux heard the phone number exactly in 22 of 22 turns, but the LLM passed the right number to the tools only 3 times in 11. Which stage lost the number?

  2. 2.With a perfect transcript the agent succeeded in all 40 calls; with real audio, 29 of 40. What does that comparison tell you?

  3. 3.One scenario succeeded in 5 of 5 audio calls. What can you conclude?

  4. 4.The median caller waited 2.35 s for a reply. Which stage was the biggest part?

  5. 5.A stricter turn detector (eot_threshold 0.9) still cut off the caller at a 1.6-second pause in a phone number, in 4 of 8 tries. Task success stayed 5/5. What does that mean for the evaluation?

  6. 6.Version 2 converts digits in code and asks callers to spell their last name. numbers_spoken went from 0/5 to 4/5, and all calls from 29/40 to 32/40. Is it done?

Check your understanding

Try each question before opening its answer.

How do chained and speech-to-speech agents differ, and how does that change evaluation?

A chain (speech-to-text, LLM, text-to-speech) exposes stages you can log, time and replace, such as swapping in a perfect transcript, so failures can be traced to a stage. A speech-to-speech model is a black box: judge its outcomes (task success, latency, turn-taking) and listen to failed calls.

What makes up voice-to-voice latency, and how would you find the biggest part?

From the caller's last word to the first word heard: endpointing, LLM first token (with tool round trips), first sentence, TTS first byte, playback buffer and leading silence. Timestamp each on one clock, every turn, and report each stage's share at p50 and p95. Here the LLM was about half; tool turns doubled it.

Which turn-taking behaviours would you test, and how?

Cut-offs at known pauses (a 1.6 s pause mid-number), response delay (endpointing time), talking over the caller (overlaps), and barge-in. They need a caller that acts in real time on an audio clock, and they need their own measures: task success stayed 5/5 while callers were cut off.

How do you define and measure task success for a voice agent?

In checkable terms, by code: the system's final state (the right appointment, nothing extra) and tool-call rules on the log. Use an LLM judge only for what code can't decide. Run each scenario several times and report rates with intervals.

How do you find out which stage caused a failure, and what do speech-to-text errors cost?

Replay with a perfect transcript (40/40 vs 29/40 here), check the key value at each stage (the phone number in what Flux heard, and in what reached the tool), and read failed calls turn by turn. WER says little: it was 5–19% everywhere, while the number was lost after a perfect transcription.

Why run each simulated-caller scenario more than once?

Both sides are LLMs talking in real time, so every call plays out differently. One call is an anecdote; five find broken scenarios, but 5/5 still means 57–100%, too wide to rank good ones.

A fix improves the scenarios it targeted. Is it done?

No. Re-run every scenario: here the spelling rule broke noisy_phone (2/5 to 0/5), early digit conversion split cut-off numbers, calls got 12 s longer, and the overall gain was within chance. Fix the new problems at their stage, and measure again.

Next up: putting it all together. Chapter 10 builds a model release pipeline, where a candidate passed every offline check and was rolled back 85 seconds into its canary.

Learning something useful?

New labs, articles, and videos on evaluating and testing AI systems come out regularly. Follow along so you don't miss the next one.