By Sagar Shankaran, Founder of CallSphere
Learn how to build a comprehensive testing and QA pipeline for voice agents, covering audio simulation, accuracy measurement, regression testing, and production monitoring.
Key takeaways
Testing a voice agent is fundamentally harder than testing a text-based chatbot. Text pipelines have a single input modality — strings. Voice pipelines have three stages that can each fail independently: speech-to-text transcription, language model reasoning, and text-to-speech synthesis. A bug in any stage produces a bad user experience, but the failure modes are completely different.
Traditional unit tests verify deterministic behavior. Voice agents are probabilistic at every layer. The same spoken phrase can transcribe differently depending on accent, background noise, microphone quality, and network latency. The LLM can produce different responses to identical transcriptions. The TTS layer can mispronounce domain-specific terms.
This guide walks through a production-tested approach to voice agent QA that covers audio simulation, transcription accuracy measurement, end-to-end conversation testing, and continuous monitoring.
A robust voice agent test pipeline has four layers:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
PR(["PR opened"])
UNIT["Unit tests"]
EVAL["Eval harness<br/>PromptFoo or Braintrust"]
GOLD[("Golden set<br/>200 tagged cases")]
JUDGE["LLM as judge<br/>plus regex graders"]
SCORE["Aggregate score<br/>and per slice"]
GATE{"Score regress<br/>more than 2 percent?"}
BLOCK(["Block merge"])
MERGE(["Merge to main"])
PR --> UNIT --> EVAL --> GOLD --> JUDGE --> SCORE --> GATE
GATE -->|Yes| BLOCK
GATE -->|No| MERGE
style EVAL fill:#4f46e5,stroke:#4338ca,color:#fff
style GATE fill:#f59e0b,stroke:#d97706,color:#1f2937
style BLOCK fill:#dc2626,stroke:#b91c1c,color:#fff
style MERGE fill:#059669,stroke:#047857,color:#fff
┌──────────────────────────────────────────────────┐
│ Test Pipeline │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Audio │──►│ Transcription │ │
│ │ Simulation │ │ Accuracy │ │
│ └─────────────┘ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Conversation │ │
│ │ Flow Tests │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Production │ │
│ │ Monitoring │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────┘
The first challenge is generating realistic audio inputs without requiring human speakers for every test run. We use text-to-speech to create test audio from scripted scenarios, then feed that audio into the voice agent as if it came from a real caller.
# test_audio_generator.py
import openai
import json
from pathlib import Path
client = openai.OpenAI()
# Define test scenarios with expected outcomes
TEST_SCENARIOS = [
{
"id": "billing_inquiry_01",
"utterances": [
"Hi, I need to check my account balance",
"My account number is 4 5 7 8 9 2",
"Yes that is correct",
"Can you also tell me when my next payment is due",
],
"expected_intent": "billing_inquiry",
"expected_tools": ["check_billing", "get_payment_schedule"],
},
{
"id": "refund_request_01",
"utterances": [
"I want to return a product I bought last week",
"The order number is A B C 1 2 3 4",
"The item arrived damaged",
],
"expected_intent": "refund_request",
"expected_tools": ["lookup_order", "initiate_refund"],
},
]
def generate_test_audio(scenarios: list, output_dir: str = "./test_audio"):
"""Generate synthetic audio files for each test scenario."""
Path(output_dir).mkdir(parents=True, exist_ok=True)
manifest = []
for scenario in scenarios:
scenario_files = []
for i, utterance in enumerate(scenario["utterances"]):
response = client.audio.speech.create(
model="tts-1",
voice="alloy",
input=utterance,
)
filename = f"{scenario['id']}_turn_{i:02d}.mp3"
filepath = Path(output_dir) / filename
response.stream_to_file(str(filepath))
scenario_files.append({
"file": filename,
"original_text": utterance,
"turn": i,
})
manifest.append({
"scenario_id": scenario["id"],
"files": scenario_files,
"expected_intent": scenario["expected_intent"],
"expected_tools": scenario["expected_tools"],
})
with open(Path(output_dir) / "manifest.json", "w") as f:
json.dump(manifest, f, indent=2)
return manifest
This generates a directory of audio files with a manifest that maps each file to its expected transcription and downstream behavior. The manifest is critical — it is the ground truth for every subsequent test layer.
Transcription accuracy is measured using Word Error Rate (WER), the standard metric in speech recognition. WER counts the minimum number of insertions, deletions, and substitutions needed to transform the transcribed text into the reference text.
# transcription_accuracy.py
import numpy as np
def word_error_rate(reference: str, hypothesis: str) -> float:
"""Calculate Word Error Rate between reference and hypothesis."""
ref_words = reference.lower().split()
hyp_words = hypothesis.lower().split()
# Build the edit distance matrix
d = np.zeros((len(ref_words) + 1, len(hyp_words) + 1), dtype=int)
for i in range(len(ref_words) + 1):
d[i][0] = i
for j in range(len(hyp_words) + 1):
d[0][j] = j
for i in range(1, len(ref_words) + 1):
for j in range(1, len(hyp_words) + 1):
if ref_words[i - 1] == hyp_words[j - 1]:
d[i][j] = d[i - 1][j - 1]
else:
substitution = d[i - 1][j - 1] + 1
insertion = d[i][j - 1] + 1
deletion = d[i - 1][j] + 1
d[i][j] = min(substitution, insertion, deletion)
return d[len(ref_words)][len(hyp_words)] / len(ref_words)
async def evaluate_transcription_accuracy(
audio_dir: str, manifest_path: str
) -> dict:
"""Run all test audio through transcription and measure accuracy."""
import json
from pathlib import Path
with open(manifest_path) as f:
manifest = json.load(f)
results = []
for scenario in manifest:
for file_info in scenario["files"]:
audio_path = Path(audio_dir) / file_info["file"]
with open(audio_path, "rb") as audio_file:
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
)
wer = word_error_rate(
file_info["original_text"],
transcript.text,
)
results.append({
"scenario": scenario["scenario_id"],
"turn": file_info["turn"],
"reference": file_info["original_text"],
"hypothesis": transcript.text,
"wer": wer,
})
total_wer = sum(r["wer"] for r in results) / len(results)
return {"average_wer": total_wer, "details": results}
A healthy voice pipeline should maintain an average WER below 0.10 (10%). Anything above 0.15 indicates a problem — either the audio quality is poor, the domain vocabulary is not being recognized, or the transcription model needs prompt tuning.
Transcription accuracy alone does not guarantee a good user experience. The agent must also route to the correct department, call the right tools, and produce appropriate responses. End-to-end conversation tests validate the full pipeline.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
# test_conversation_flows.py
import pytest
from agents import Runner
from your_app.agents import triage_agent
@pytest.fixture
def runner():
return Runner()
CONVERSATION_TEST_CASES = [
{
"name": "billing_happy_path",
"turns": [
{"user": "I need to check my balance", "expect_handoff": "billing_agent"},
{"user": "Account number 457892", "expect_tool": "check_billing"},
],
"expect_final_contains": ["balance", "$"],
},
{
"name": "refund_with_damaged_item",
"turns": [
{"user": "I want a refund", "expect_handoff": "refund_agent"},
{"user": "Order ABC1234", "expect_tool": "lookup_order"},
{"user": "It arrived damaged", "expect_tool": "initiate_refund"},
],
"expect_final_contains": ["refund", "processed"],
},
]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"test_case", CONVERSATION_TEST_CASES, ids=lambda tc: tc["name"]
)
async def test_conversation_flow(runner, test_case):
"""Validate that a multi-turn conversation produces expected behavior."""
result = None
for turn in test_case["turns"]:
result = await Runner.run(
triage_agent,
input=turn["user"],
context=result,
)
if "expect_handoff" in turn:
assert result.last_agent.name == turn["expect_handoff"], (
f"Expected handoff to {turn['expect_handoff']}, "
f"got {result.last_agent.name}"
)
if "expect_tool" in turn:
tool_names = [
item.name for item in result.new_items
if hasattr(item, "name")
]
assert turn["expect_tool"] in tool_names, (
f"Expected tool {turn['expect_tool']} not found in {tool_names}"
)
final_output = result.final_output
for expected_text in test_case["expect_final_contains"]:
assert expected_text.lower() in final_output.lower(), (
f"Expected '{expected_text}' in final output: {final_output[:200]}"
)
Testing before deployment is necessary but not sufficient. Voice agents face real-world conditions that synthetic tests cannot fully replicate — different accents, background noise, network jitter, and unexpected user behavior. Production monitoring closes the loop.
# monitoring.py
import time
from dataclasses import dataclass, field
from collections import defaultdict
@dataclass
class CallMetrics:
call_id: str
transcription_confidence: float
response_latency_ms: float
tool_calls_made: list = field(default_factory=list)
user_sentiment: str = "neutral"
escalated: bool = False
completed: bool = False
class QualityMonitor:
def __init__(self, alert_threshold_wer: float = 0.15):
self.metrics: list[CallMetrics] = []
self.alert_threshold = alert_threshold_wer
self.hourly_stats = defaultdict(list)
def record_call(self, metrics: CallMetrics):
self.metrics.append(metrics)
hour_key = time.strftime("%Y-%m-%d-%H")
self.hourly_stats[hour_key].append(metrics)
self._check_alerts(hour_key)
def _check_alerts(self, hour_key: str):
recent = self.hourly_stats[hour_key]
if len(recent) < 10:
return
avg_confidence = sum(
m.transcription_confidence for m in recent
) / len(recent)
escalation_rate = sum(
1 for m in recent if m.escalated
) / len(recent)
avg_latency = sum(
m.response_latency_ms for m in recent
) / len(recent)
if avg_confidence < (1 - self.alert_threshold):
self._send_alert(
f"Transcription confidence dropped to {avg_confidence:.2f}"
)
if escalation_rate > 0.3:
self._send_alert(
f"Escalation rate at {escalation_rate:.0%} in last hour"
)
if avg_latency > 3000:
self._send_alert(
f"Average response latency at {avg_latency:.0f}ms"
)
def _send_alert(self, message: str):
print(f"ALERT: {message}")
# In production: send to PagerDuty, Slack, etc.
For production voice agents, monitor these metrics continuously:
When any metric degrades beyond its threshold, the monitoring system should alert the team immediately. The most common root causes of voice agent quality regressions are upstream API changes, domain vocabulary drift, and increased traffic from new user demographics with different speech patterns.
Combine all layers into a CI-runnable regression suite that executes on every deployment:
# .github/workflows/voice-agent-qa.yml
name: Voice Agent QA
on:
push:
branches: [main]
pull_request:
jobs:
voice-qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate test audio
run: python test_audio_generator.py
- name: Transcription accuracy check
run: |
python -m pytest tests/test_transcription.py \
--tb=short -q
- name: Conversation flow tests
run: |
python -m pytest tests/test_conversation_flows.py \
--tb=short -q
- name: Upload QA report
uses: actions/upload-artifact@v4
with:
name: qa-report
path: reports/
Voice agent quality is not a one-time achievement — it is a continuous practice. By layering audio simulation, transcription accuracy measurement, conversation flow testing, and production monitoring, you build a safety net that catches regressions before users experience them. The investment in test infrastructure pays for itself the first time it prevents a broken deployment from reaching production.

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
OpenAI's Frontier platform makes model-native orchestration the default. What that means for agent builders, voice/chat buyers, and the build-vs-buy decision.
The 2026 desktop AI agent landscape — ServiceNow Project Arc, Anthropic Claude offerings, OpenAI agents, and Google Mariner. A buyer's map.
May 2026's biggest agent-architecture shift: planning, tool selection, and self-correction move inside the model. Framework code shrinks. Here is what changes.
A three-way comparison of Gemini Enterprise, Anthropic managed agents and OpenAI Frontier Platform after Cloud Next 2026 — strengths, gaps, buyer fit.
Anthropic's May 2026 push positions Claude as a vertical platform for financial services. The strategic positioning versus OpenAI and Google.
Anthropic's Mythos is not alone. Compare Mythos against OpenAI's cybersec offerings, Google's Big Sleep lineage, and open-source alternatives in 2026.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco