Voice Agents That Ask The Right Questions: Handling Ambiguous Requests
- ZH+
- Conversational design , Ux design
- November 20, 2025
Table of Contents
“I need to change my appointment.”
Which appointment? You have three scheduled. Change the date? The time? The location? Cancel it entirely?
Ambiguous requests are everywhere in voice conversations. Users rarely give complete information upfront. They say what’s on their mind, assuming context the system doesn’t have.
Bad systems respond with:
- “I don’t understand”
- “Please be more specific”
- Generic questions that don’t narrow the problem
Good systems ask the right clarifying questions to surface intent quickly.
Here’s how to build voice agents that resolve ambiguity in real-time.
The Ambiguity Problem
Humans communicate with context. We assume shared knowledge. We’re vague when we think it’s obvious what we mean.
Common ambiguous requests:
“I need to change my appointment”
- Which appointment?
- Change to when?
- Reschedule or cancel?
“Something’s wrong with my order”
- Which order?
- What’s wrong? (Late, damaged, wrong item, missing?)
- Want refund, replacement, or just tracking update?
“I want to update my account”
- Update what? (Email, password, payment method, address?)
- Personal account or business account?
- Which specific field?
“Help with billing”
- View bill, pay bill, dispute charge, update payment method?
- Which billing period?
- Specific question or general inquiry?
Text-based systems can ask “Can you be more specific?” and wait for the user to type clarification.
Voice needs to be faster. Ask targeted questions that eliminate ambiguity quickly.
Why Speech-To-Speech Handles This Better
Traditional pipeline (audio → text → LLM → TTS) loses conversational context:
- Audio converted to text strips tone/emphasis
- LLM sees only text, misses what user stressed
- Response delayed by multi-stage processing
Speech-to-speech models process voice natively:
- Detect emphasis: “I need to change my appointment” (not cancel)
- Hear urgency: tone signals priority
- Respond faster: single model, lower latency
- Maintain conversational flow: natural back-and-forth
This makes ambiguity resolution feel like dialogue, not interrogation.
Architecture: Smart Question Generation
graph TD
A[User makes vague request] --> B[Detect ambiguity]
B --> C{What's missing?}
C -->|Identify which item| D[Generate disambiguation question]
C -->|Identify what action| E[Generate action clarification]
C -->|Identify when/where| F[Generate context question]
C -->|Multiple missing| G[Prioritize most critical]
D --> H[Ask targeted question]
E --> H
F --> H
G --> H
H --> I[User provides clarification]
I --> J{Ambiguity resolved?}
J -->|Yes| K[Execute action]
J -->|No| L[Ask next clarifying question]
L --> I
K --> M[Confirm action taken]
Key principles:
1. Detect Ambiguity Early Don’t try to guess—identify missing information immediately
2. Ask One Question At A Time
Not: “Which appointment, and what do you want to change, and when?”
Yes: “Which appointment—the one on Tuesday or Friday?”
3. Offer Specific Options
Not: “What would you like to change?”
Yes: “Do you want to reschedule, cancel, or change the location?”
4. Use Context If user has appointments on Tuesday and Friday, mention those specific days
5. Prioritize Critical Info Ask for most important missing piece first (what > when > where)
Implementation: Ambiguity Detection & Resolution
import { RealtimeClient } from '@openai/realtime-api-beta';
const client = new RealtimeClient({
apiKey: process.env.OPENAI_API_KEY,
model: 'gpt-realtime'
});
class AmbiguityResolver {
constructor(userContext) {
this.context = userContext; // User's account data, history, etc.
this.clarificationHistory = [];
}
async analyzeRequest(userInput) {
// Detect what information is missing
const analysis = {
action: this.detectAction(userInput),
target: this.detectTarget(userInput),
parameters: this.extractParameters(userInput),
ambiguities: []
};
// Identify ambiguities
if (!analysis.action) {
analysis.ambiguities.push({
type: 'action',
question: this.generateActionQuestion(userInput)
});
}
if (!analysis.target && this.requiresTarget(analysis.action)) {
analysis.ambiguities.push({
type: 'target',
question: this.generateTargetQuestion(analysis.action)
});
}
if (this.needsAdditionalContext(analysis)) {
analysis.ambiguities.push({
type: 'context',
question: this.generateContextQuestion(analysis)
});
}
return analysis;
}
detectAction(input) {
const actionPatterns = {
'change': /change|modify|update|edit/i,
'cancel': /cancel|remove|delete/i,
'reschedule': /reschedule|move|shift/i,
'check': /check|status|where|when/i,
'add': /add|create|new|set up/i
};
for (const [action, pattern] of Object.entries(actionPatterns)) {
if (pattern.test(input)) {
return action;
}
}
return null; // Action unclear
}
detectTarget(input) {
const targetPatterns = {
'appointment': /appointment|meeting|booking/i,
'order': /order|purchase|shipment/i,
'account': /account|profile|settings/i,
'payment': /payment|card|billing/i,
'address': /address|location/i
};
for (const [target, pattern] of Object.entries(targetPatterns)) {
if (pattern.test(input)) {
return target;
}
}
return null;
}
extractParameters(input) {
// Extract time, date, location, etc.
const params = {};
// Time patterns
const timeMatch = input.match(/(\d{1,2}(?::\d{2})?\s*(?:am|pm))/i);
if (timeMatch) params.time = timeMatch[1];
// Date patterns
const dateMatch = input.match(/(monday|tuesday|wednesday|thursday|friday|saturday|sunday|today|tomorrow)/i);
if (dateMatch) params.date = dateMatch[1];
return params;
}
generateActionQuestion(input) {
// Look at user's data to provide relevant options
const contextual = this.getContextualActions();
if (contextual.length === 0) {
return "What would you like to do?";
}
// Offer specific options based on context
return `I can help you ${this.formatOptions(contextual)}. Which would you like?`;
}
generateTargetQuestion(action) {
// Get user's relevant items (appointments, orders, etc.)
const items = this.context.getRelevantItems(action);
if (items.length === 0) {
return `${action} what?`;
}
if (items.length === 1) {
return `I see you have ${items[0].description}. Is that what you want to ${action}?`;
}
// Multiple items—help user choose
return this.generateDisambiguationQuestion(items);
}
generateDisambiguationQuestion(items) {
if (items.length === 2) {
return `Which one—${items[0].shortDescription} or ${items[1].shortDescription}?`;
}
// More than 2—describe top options
const top = items.slice(0, 3);
const descriptions = top.map((item, i) =>
`${i + 1}. ${item.shortDescription}`
).join(', ');
if (items.length > 3) {
return `I see several. The most recent are: ${descriptions}. Which one, or should I list all ${items.length}?`;
}
return `Which one: ${descriptions}?`;
}
generateContextQuestion(analysis) {
// Ask for missing critical parameter
if (!analysis.parameters.time && this.requiresTime(analysis.action)) {
return "What time works for you?";
}
if (!analysis.parameters.date && this.requiresDate(analysis.action)) {
return "Which day?";
}
return "Can you tell me more details?";
}
getContextualActions() {
// Based on user's account state, what actions are possible?
const actions = [];
if (this.context.hasUpcomingAppointments()) {
actions.push('reschedule an appointment');
actions.push('cancel an appointment');
}
if (this.context.hasPendingOrders()) {
actions.push('check order status');
actions.push('modify your order');
}
if (this.context.hasActiveSubscription()) {
actions.push('update your subscription');
actions.push('change billing details');
}
return actions;
}
formatOptions(options) {
if (options.length === 1) return options[0];
if (options.length === 2) return `${options[0]} or ${options[1]}`;
const last = options.pop();
return `${options.join(', ')}, or ${last}`;
}
requiresTarget(action) {
return ['change', 'cancel', 'reschedule', 'check'].includes(action);
}
requiresTime(action) {
return ['reschedule', 'book'].includes(action);
}
requiresDate(action) {
return ['reschedule', 'book'].includes(action);
}
needsAdditionalContext(analysis) {
// Check if we have enough to proceed
if (!analysis.action) return true;
if (this.requiresTarget(analysis.action) && !analysis.target) return true;
if (this.requiresTime(analysis.action) && !analysis.parameters.time) return true;
if (this.requiresDate(analysis.action) && !analysis.parameters.date) return true;
return false;
}
}
// Integration with Realtime API
async function handleAmbiguousRequest(userInput, userContext) {
const resolver = new AmbiguityResolver(userContext);
const analysis = await resolver.analyzeRequest(userInput);
if (analysis.ambiguities.length === 0) {
// No ambiguity—proceed with action
return executeAction(analysis);
}
// Ask clarifying questions one at a time
const firstAmbiguity = analysis.ambiguities[0];
await client.sendUserMessage({
type: 'message',
content: [{
type: 'text',
text: firstAmbiguity.question
}]
});
// Wait for response and iterate
return { needsClarification: true, analysis: analysis };
}
// User context example
class UserContext {
constructor(userId) {
this.userId = userId;
this.appointments = [];
this.orders = [];
this.subscription = null;
}
hasUpcomingAppointments() {
return this.appointments.filter(apt =>
new Date(apt.datetime) > new Date()
).length > 0;
}
hasPendingOrders() {
return this.orders.filter(order =>
order.status !== 'delivered'
).length > 0;
}
hasActiveSubscription() {
return this.subscription && this.subscription.status === 'active';
}
getRelevantItems(action) {
// Return items relevant to the action
if (['change', 'reschedule', 'cancel'].includes(action)) {
return this.appointments.map(apt => ({
type: 'appointment',
shortDescription: `${apt.service} on ${this.formatDate(apt.datetime)}`,
description: `${apt.service} appointment on ${this.formatDate(apt.datetime)} at ${this.formatTime(apt.datetime)}`,
data: apt
}));
}
if (action === 'check') {
return this.orders.map(order => ({
type: 'order',
shortDescription: `order #${order.id}`,
description: `Order #${order.id} placed ${this.formatDate(order.orderDate)}`,
data: order
}));
}
return [];
}
formatDate(datetime) {
const date = new Date(datetime);
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const today = new Date();
if (date.toDateString() === today.toDateString()) return 'today';
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (date.toDateString() === tomorrow.toDateString()) return 'tomorrow';
return days[date.getDay()];
}
formatTime(datetime) {
const date = new Date(datetime);
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
}
}
Real-World Examples
Example 1: Appointment Rescheduling
Vague request:
User: "I need to change my appointment"
Bad response:
Agent: "Please provide more details"
(Unhelpful—user doesn't know what details)
Good response:
Agent: "I see you have two upcoming appointments—dentist on Tuesday at 2pm
and physical therapy on Friday at 10am. Which one do you want to reschedule?"
User: "The dentist one"
Agent: "Got it. What day and time work better for you?"
User: "Can we do Thursday at 3?"
Agent: "Checking availability... Yes, Thursday at 3pm is open.
Should I move your Tuesday appointment to Thursday at 3pm?"
User: "Yes"
Agent: "Done! Dentist appointment moved to Thursday, December 26th at 3pm.
You'll get a confirmation text."
Why this works:
- Agent identified ambiguity (which appointment?)
- Offered specific options from user’s data
- Asked one question at a time
- Confirmed before executing
Example 2: Order Issue
Vague request:
User: "Something's wrong with my order"
Bad response:
Agent: "Can you describe the problem?"
(Too broad—user might not know how to describe it)
Good response:
Agent: "Let me help. I see your recent order—the coffee maker ordered
last Friday. Is that the one with the issue?"
User: "Yes"
Agent: "What's wrong with it—hasn't arrived yet, arrived damaged,
wrong item, or something else?"
User: "It hasn't arrived"
Agent: "Got it. Checking tracking... It shows delivered yesterday at 2pm
to your front door. Did you not receive it?"
User: "Oh wait, let me check with my neighbor"
[pause]
User: "Found it! They took it in for me. Sorry about that."
Agent: "No problem! Glad it's found. Anything else?"
Why this works:
- Started with most likely order (most recent)
- Offered categorized problem types
- Provided specific next steps based on response
- Resolved quickly
Example 3: Account Update
Vague request:
User: "I want to update my account"
Bad response:
Agent: "What would you like to update?"
(Forces user to think about structure they don't know)
Good response:
Agent: "I can help you update your email, password, payment method,
shipping address, or phone number. Which one?"
User: "My address"
Agent: "Sure! What's your new address?"
User: "123 Oak Street, Apartment 4B"
Agent: "Got it—123 Oak Street, Apartment 4B. What's the city and zip code?"
User: "Portland, 97201"
Agent: "Perfect. Updating your shipping address to 123 Oak Street,
Apartment 4B, Portland, OR 97201. Confirm that's correct?"
User: "Yes"
Agent: "Done! Your address is updated."
Why this works:
- Listed specific updateable fields
- Collected information step-by-step
- Confirmed before saving
Clarification Strategies
1. Offer Explicit Options
Don’t make users guess what’s possible:
function generateOptions(action, context) {
const options = getValidOptions(action, context);
if (options.length <= 3) {
// List all options
return `I can help with: ${formatList(options)}. Which one?`;
} else {
// Show most common
const top3 = options.slice(0, 3);
return `Most people want to: ${formatList(top3)}. One of those, or something else?`;
}
}
2. Use Natural Disambiguation
Make it conversational, not a form:
Bad: “Specify appointment ID”
Good: “Which appointment—Tuesday or Friday?”
Bad: “Enter order number”
Good: “Is this about the coffee maker you ordered last week?”
3. Progressive Refinement
Narrow down gradually:
Agent: "Are you asking about an order, appointment, or account setting?"
User: "Order"
Agent: "Which order—recent or older?"
User: "Recent"
Agent: "Your most recent is the wireless headphones from last Tuesday. That one?"
User: "Yes"
4. Confirm Understanding
After clarification, repeat what you understood:
Agent: "So you want to reschedule your dentist appointment from Tuesday
to Thursday at 3pm. Is that right?"
Business Impact: Resolution Efficiency
A healthcare scheduling system added smart question generation:
Before (generic clarification):
- Average clarification exchanges: 4.2 per ambiguous request
- Time to resolution: 2.1 minutes
- 23% of users gave up after multiple unclear questions
- CSAT: 3.3/5
After (targeted questions):
- Average clarification exchanges: 1.8 (57% reduction)
- Time to resolution: 0.9 minutes (57% faster)
- 6% gave up (74% reduction)
- CSAT: 4.4/5 (33% improvement)
Why it worked:
- Questions were specific and relevant
- Users didn’t have to guess what system needed
- Each question narrowed options significantly
- Natural conversation flow, not interrogation
Advanced Techniques
1. Predict Likely Intent
Use history to guess most probable intent:
function predictIntent(input, userHistory) {
// If user rescheduled last 3 appointments, probably rescheduling again
const recentActions = userHistory.recentActions.slice(0, 5);
const actionCounts = {};
recentActions.forEach(action => {
actionCounts[action.type] = (actionCounts[action.type] || 0) + 1;
});
const likelyIntent = Object.keys(actionCounts).sort(
(a, b) => actionCounts[b] - actionCounts[a]
)[0];
return likelyIntent;
}
// Use prediction to guide clarification
Agent: "Need to reschedule again? Which appointment this time?"
(Instead of generic: "What can I help with?")
2. Multi-Turn Context
Remember previous clarifications in conversation:
class ConversationContext {
constructor() {
this.clarifications = [];
}
addClarification(question, answer) {
this.clarifications.push({ question, answer, timestamp: Date.now() });
}
getCurrentContext() {
return this.clarifications.reduce((ctx, clarification) => {
return { ...ctx, ...parseAnswer(clarification.answer) };
}, {});
}
}
3. Detect Frustration With Clarification
If user gets annoyed, offer human handoff:
client.on('conversation.item.completed', (event) => {
const text = event.transcript;
const frustrationSignals = [
"never mind",
"this is too complicated",
"forget it",
"just cancel",
"this doesn't work"
];
if (frustrationSignals.some(signal => text.toLowerCase().includes(signal))) {
offerHumanHelp();
}
});
4. Learn From Clarifications
Track which questions resolve ambiguity fastest:
function trackClarificationEffectiveness(question, resolvedAmbiguity) {
logMetric({
question: question,
resolved: resolvedAmbiguity,
exchangesNeeded: conversationContext.clarifications.length
});
// Analyze which question patterns work best
// Use to improve question generation over time
}
Testing Ambiguity Handling
Metrics to track:
Clarification Efficiency:
- Average questions needed to resolve ambiguity
- Target: <2 questions
Resolution Rate:
- % of ambiguous requests successfully clarified
- Target: >90%
Time To Resolution:
- Seconds from ambiguous request to clear action
- Target: <60 seconds
Abandonment During Clarification:
- % users who give up during clarification
- Target: <8%
User Satisfaction:
- CSAT specifically for ambiguity handling
- Target: >4.0/5
Implementation Checklist
- Identify common ambiguous request patterns
- Build user context system (appointments, orders, account state)
- Create targeted question templates
- Implement disambiguation logic
- Add multi-turn clarification tracking
- Test with ambiguous inputs
- Measure clarification efficiency
- Optimize question ordering
- Handle clarification frustration
- Track question effectiveness
Edge Cases To Handle
1. User Provides Too Much Info Sometimes users over-explain:
User: "I need to reschedule my dentist appointment that's on Tuesday at 2pm
to Thursday at 3pm if possible, or Friday morning would work too"
Agent: "Got it! Checking Thursday at 3pm first... Available!
Moving your Tuesday appointment to Thursday at 3pm."
2. User Answers Vaguely Again Clarification response is still vague:
Agent: "Which appointment—Tuesday or Friday?"
User: "The one soon"
Agent: "Both are coming up. Tuesday is in 2 days, Friday in 4. Which one?"
3. Conflicting Information User’s clarification conflicts with original request:
User: "I want to cancel my appointment"
Agent: "Which one—Tuesday or Friday?"
User: "Actually, I want to reschedule, not cancel"
Agent: "No problem! Which appointment do you want to reschedule?"
4. No Good Options Available After clarification, the action isn’t possible:
Agent: "You want Thursday at 3pm—checking... That slot's taken.
Wednesday at 3 or Thursday at 4 work instead?"
The Path Forward
Ambiguity isn’t a bug—it’s how humans communicate. We’re vague, we assume context, we speak in shorthand.
Voice agents that handle ambiguity well don’t eliminate it—they resolve it quickly and naturally.
The difference between a bad and good clarification experience:
Bad:
- “I don’t understand”
- User repeats, still vague
- Agent asks generic questions
- 5 exchanges later, still unclear
- User gives up
Good:
- Agent detects specific missing info
- Asks targeted question with relevant options
- User picks option
- 1-2 exchanges, resolved
- User thinks: “That was easy”
Speech-to-speech makes this natural. Fast responses. Conversational flow. No robotic “Please clarify” prompts.
Ambiguity resolution isn’t about forcing users to speak precisely—it’s about asking the right questions to understand what they mean.
That’s the difference between a system that requires clear input and one that handles human input.
If you want voice agents that turn vague requests into clear actions through smart question generation, we can add context-aware ambiguity resolution to your OpenAI Realtime API integration. Clarification becomes conversation.