Recovering from ASR Errors: Fallback Strategies for Voice Agents
Learn how to detect and recover from ASR errors in voice agents using confidence scoring, confirmation strategies, and graceful fallbacks.
Published on • September 13, 2026
AI Assistant

Recovering from ASR Errors: Fallback Strategies for Voice Agents
Automatic Speech Recognition (ASR) errors are inevitable in real-world voice agents. Background noise, accents, domain-specific vocabulary, and ambiguous utterances all contribute to misheard transcripts. The key is not preventing all errors—impossible—but detecting them early and recovering gracefully.
The ASR Error Landscape
Common Failure Modes
class ASRFailureModes:
"""Categories of ASR errors in voice agents."""
FAILURE_MODES = {
'noise_omissions': {
'description': 'Background noise causes entities to drop',
'examples': ['dates, amounts, names missing from transcript'],
'detection': 'Entity presence checks'
},
'substituted_intents': {
'description': 'Misheard words change user intent',
'examples': ['Cancel becomes schedule, buy becomes sell'],
'detection': 'Intent confidence scoring'
},
'formatting_drift': {
'description': 'Format changes without meaning change',
'examples': ['120 becomes one two zero, 555-1234 becomes 5551234'],
'detection': 'Format validation'
},
'truncation': {
'description': 'Agent responds to incomplete utterance',
'examples': ['User interrupted but agent already responded'],
'detection': 'Endpointing analysis'
},
'hallucinations': {
'description': 'ASR generates text during silence',
'examples': ['Fluent text appears when no one spoke'],
'detection': 'Audio presence verification'
}
}
Detection Strategies
Confidence-Based Detection
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class ASRResult:
transcript: str
confidence: float
alternatives: List[dict]
word_confidences: List[dict]
class ConfidenceDetector:
def __init__(self, confidence_threshold: float = 0.7):
self.threshold = confidence_threshold
def detect_uncertainty(self, result: ASRResult) -> dict:
"""Detect if ASR result is uncertain."""
signals = {
'low_overall_confidence': result.confidence < self.threshold,
'close_alternatives': self._has_close_alternatives(result),
'low_word_confidence': self._has_low_word_confidence(result),
'ambiguous_intent': self._detect_ambiguous_intent(result)
}
return {
'is_uncertain': any(signals.values()),
'signals': signals,
'recommended_action': self._recommend_action(signals)
}
def _has_close_alternatives(self, result: ASRResult) -> bool:
"""Check if alternatives are close in confidence."""
if len(result.alternatives) < 2:
return False
top_two = sorted(
[a['confidence'] for a in result.alternatives[:2]],
reverse=True
)
return (top_two[0] - top_two[1]) < 0.1 # Within 10%
def _has_low_word_confidence(self, result: ASRResult) -> bool:
"""Check for low-confidence words."""
return any(
word['confidence'] < 0.6
for word in result.word_confidences
)
def _detect_ambiguous_intent(self, result: ASRResult) -> bool:
"""Detect ambiguous intent from alternatives."""
intents = [a.get('intent') for a in result.alternatives]
return len(set(intents)) > 1 # Multiple different intents
def _recommend_action(self, signals: dict) -> str:
"""Recommend recovery action based on signals."""
if signals['ambiguous_intent']:
return 'explicit_confirmation'
elif signals['low_overall_confidence']:
return 'repetition_request'
elif signals['close_alternatives']:
return 'disambiguation'
else:
return 'implicit_confirmation'
Entity Presence Checks
class EntityPresenceChecker:
def __init__(self, required_entities: List[str]):
self.required_entities = required_entities
def check_entities(self, transcript: str,
extracted_entities: dict) -> dict:
"""Verify all required entities are present."""
missing = []
weak = []
for entity in self.required_entities:
if entity not in extracted_entities:
missing.append(entity)
elif extracted_entities[entity].get('confidence', 1.0) < 0.7:
weak.append(entity)
return {
'all_present': len(missing) == 0,
'missing_entities': missing,
'weak_entities': weak,
'can_proceed': len(missing) == 0 and len(weak) == 0
}
Recovery Strategies
1. Implicit Confirmation
Confirm without explicitly asking:
class ImplicitConfirmation:
def __init__(self, agent):
self.agent = agent
async def confirm_entity(self, entity: str, value: str) -> str:
"""Implicitly confirm by incorporating into next question."""
# Instead of: "Did you say March 14th?"
# Say: "And for March 14th, what time works for you?"
prompt = f"""
The user mentioned {entity}: {value}
Create a follow-up question that implicitly confirms this value.
The user can object if it's wrong.
"""
return await self.agent.generate_response(prompt)
2. Explicit Confirmation
Directly verify uncertain values:
class ExplicitConfirmation:
def __init__(self, agent):
self.agent = agent
async def confirm_value(self, entity: str, value: str,
alternatives: List[str]) -> dict:
"""Explicitly confirm a critical value."""
if len(alternatives) > 1:
# Present alternatives
options = " or ".join([value] + alternatives[:2])
prompt = f"I heard {options}. Which is correct?"
else:
# Confirm single value
prompt = f"I have {value} for {entity}. Is that right?"
response = await self.agent.get_user_response(prompt)
return {
'confirmed': response.get('affirmative', False),
'final_value': response.get('correction', value)
}
3. Targeted Reprompting
Ask for specific uncertain information:
class TargetedReprompt:
def __init__(self, agent):
self.agent = agent
async def reprompt_for_entity(self, entity: str,
context: dict) -> str:
"""Reprompt for a specific entity."""
# Instead of: "Sorry, I didn't catch that. Please repeat everything."
# Say: "I got the date, but could you tell me the time again?"
known_info = ", ".join([
f"{k}: {v}" for k, v in context.items()
if k != entity
])
prompt = f"""
I understood: {known_info}
But I'm unsure about the {entity}.
Ask specifically for the {entity} while showing what you do know.
"""
return await self.agent.generate_response(prompt)
4. Progressive Prompting
Vary the recovery strategy:
class ProgressivePrompting:
def __init__(self, agent):
self.agent = agent
self.attempt_strategies = [
'simple_repetition',
'rephrase_question',
'offer_alternatives',
'spell_or_slow_down',
'escalate_to_human'
]
async def recover(self, entity: str, attempt: int) -> str:
"""Progressive recovery with different strategies."""
if attempt >= len(self.attempt_strategies):
return await self.escalate_to_human(entity)
strategy = self.attempt_strategies[attempt]
if strategy == 'simple_repetition':
return f"Could you repeat the {entity}?"
elif strategy == 'rephrase_question':
return await self.rephrase_question(entity)
elif strategy == 'offer_alternatives':
return await self.offer_alternatives(entity)
elif strategy == 'spell_or_slow_down':
return f"You can spell the {entity} or say it slowly."
elif strategy == 'escalate_to_human':
return await self.escalate_to_human(entity)
async def escalate_to_human(self, entity: str) -> str:
"""Transfer to human agent."""
return f"I'm having trouble understanding the {entity}. Let me connect you with someone who can help."
Fallback Patterns
Multi-Vendor ASR Fallback
class MultiVendorASR:
def __init__(self):
self.primary = WhisperASR()
self.fallback = GoogleASR()
self.emergency = DeepgramASR()
async def transcribe(self, audio: bytes) -> ASRResult:
"""Try multiple ASR vendors."""
# Try primary
try:
result = await self.primary.transcribe(audio)
if result.confidence > 0.7:
return result
except Exception as e:
print(f"Primary ASR failed: {e}")
# Try fallback
try:
result = await self.fallback.transcribe(audio)
if result.confidence > 0.6:
return result
except Exception as e:
print(f"Fallback ASR failed: {e}")
# Try emergency
try:
return await self.emergency.transcribe(audio)
except Exception as e:
raise ASRFallbackExhausted("All ASR vendors failed")
Confidence-Based Routing
class ConfidenceBasedRouter:
def __init__(self):
self.routes = {
'high_confidence': self.proceed_with_action,
'medium_confidence': self.implicit_confirmation,
'low_confidence': self.explicit_confirmation,
'very_low_confidence': self.reprompt_or_escalate
}
def route_by_confidence(self, result: ASRResult,
action_risk: str) -> str:
"""Route based on confidence and action risk."""
if action_risk == 'high':
# Critical actions need higher confidence
if result.confidence > 0.9:
return 'high_confidence'
elif result.confidence > 0.7:
return 'medium_confidence'
else:
return 'explicit_confirmation'
else:
# Lower risk actions can proceed with lower confidence
if result.confidence > 0.7:
return 'high_confidence'
elif result.confidence > 0.5:
return 'medium_confidence'
else:
return 'low_confidence'
Production Implementation
Complete Error Recovery Pipeline
class VoiceAgentWithRecovery:
def __init__(self):
self.confidence_detector = ConfidenceDetector()
self.entity_checker = EntityPresenceChecker([
'date', 'time', 'location', 'amount'
])
self.implicit_confirm = ImplicitConfirmation(self)
self.explicit_confirm = ExplicitConfirmation(self)
self.progressive_prompt = ProgressivePrompting(self)
async def process_utterance(self, audio: bytes) -> dict:
"""Process utterance with error recovery."""
# Step 1: Transcribe
asr_result = await self.transcribe(audio)
# Step 2: Detect uncertainty
uncertainty = self.confidence_detector.detect_uncertainty(asr_result)
# Step 3: Extract entities
entities = await self.extract_entities(asr_result.transcript)
# Step 4: Check entity presence
entity_check = self.entity_checker.check_entities(
asr_result.transcript, entities
)
# Step 5: Determine action
if not uncertainty['is_uncertain'] and entity_check['can_proceed']:
# High confidence, all entities present
return await self.proceed_with_action(entities)
elif uncertainty['recommended_action'] == 'explicit_confirmation':
# Need explicit confirmation
return await self.handle_explicit_confirmation(
entities, uncertainty['signals']
)
elif not entity_check['all_present']:
# Missing entities
return await self.handle_missing_entities(
entity_check['missing_entities'], entities
)
else:
# General uncertainty
return await self.handle_general_uncertainty(
asr_result, uncertainty
)
Monitoring and Alerting
class ASRMonitoring:
def __init__(self):
self.metrics = {
'total_utterances': 0,
'uncertain_utterances': 0,
'recovery_attempts': 0,
'successful_recoveries': 0,
'escalations_to_human': 0
}
def record_utterance(self, uncertainty: dict,
recovery_action: str, success: bool):
"""Record ASR performance metrics."""
self.metrics['total_utterances'] += 1
if uncertainty['is_uncertain']:
self.metrics['uncertain_utterances'] += 1
if recovery_action != 'none':
self.metrics['recovery_attempts'] += 1
if success:
self.metrics['successful_recoveries'] += 1
if recovery_action == 'escalate_to_human':
self.metrics['escalations_to_human'] += 1
# Calculate rates
total = self.metrics['total_utterances']
self.metrics['uncertainty_rate'] = (
self.metrics['uncertain_utterances'] / total
)
self.metrics['recovery_success_rate'] = (
self.metrics['successful_recoveries'] /
max(self.metrics['recovery_attempts'], 1)
)
Best Practices
1. Selective Confirmation
Don’t confirm everything—balance accuracy with user experience:
class SelectiveConfirmationStrategy:
def should_confirm(self, entity: str, value: str,
confidence: float, risk_level: str) -> str:
"""Decide whether and how to confirm."""
# Always confirm high-risk, low-confidence values
if risk_level == 'high' and confidence < 0.8:
return 'explicit'
# Implicitly confirm medium-risk values
elif risk_level == 'medium' and confidence < 0.7:
return 'implicit'
# Skip confirmation for low-risk, high-confidence values
elif risk_level == 'low' and confidence > 0.6:
return 'none'
# Default to implicit for ambiguous cases
else:
return 'implicit'
2. Limit Retry Attempts
class RetryLimiter:
def __init__(self, max_retries: int = 3):
self.max_retries = max_retries
def can_retry(self, entity: str, attempt: int) -> bool:
"""Check if we should retry or escalate."""
if attempt >= self.max_retries:
return False
# Don't retry certain critical entities
critical_entities = ['payment_amount', 'social_security']
if entity in critical_entities and attempt >= 2:
return False
return True
3. Test with Noise
class NoiseTesting:
def __init__(self):
self.noise_levels = ['quiet', 'moderate', 'noisy', 'very_noisy']
async def test_recovery_strategies(self, agent):
"""Test recovery with various noise levels."""
results = {}
for noise_level in self.noise_levels:
test_audio = self.generate_noisy_audio(noise_level)
result = await agent.process_utterance(test_audio)
results[noise_level] = {
'transcription_accuracy': result.get('accuracy'),
'recovery_success': result.get('recovered'),
'user_effort': result.get('user_retries', 0)
}
return results
Conclusion
ASR errors are inevitable, but operational failures are preventable:
- Detect early: Use confidence scoring and entity presence checks
- Recover gracefully: Choose confirmation strategy based on risk and confidence
- Progressive recovery: Vary strategies across retry attempts
- Escalate when needed: Don’t trap users in endless retry loops
- Monitor continuously: Track uncertainty rates and recovery success
The key insight: a voice agent that acknowledges uncertainty and recovers gracefully builds more trust than one that pretends to never make mistakes. Selective confirmation—confirming critical values while letting low-risk turns pass—balances accuracy with conversational flow.
Remember: the goal isn’t zero errors, it’s zero silent failures that slip through without checks.