Disambiguation Patterns: Helping Users Choose When Multiple Options Match
Master disambiguation design patterns for conversational AI agents that present options clearly, narrow choices with follow-up criteria, and guide users to the right selection.
Why Disambiguation Matters
When a user asks "Show me the Python course," your agent might find three matching courses. Guessing which one the user wants creates frustration. Effective disambiguation presents options in a ranked, digestible format and uses follow-up questions to narrow the choice set naturally.
Disambiguation is the process of resolving ambiguity when a user query matches multiple entities, intents, or actions. Good disambiguation feels like a helpful assistant narrowing options, not a database dumping search results.
Modeling Disambiguation Candidates
from dataclasses import dataclass
from typing import Optional
@dataclass
class Candidate:
id: str
title: str
description: str
score: float # relevance score 0.0-1.0
differentiator: str # what makes this option unique
def summary(self) -> str:
return f"{self.title} - {self.differentiator}"
@dataclass
class DisambiguationResult:
resolved: bool
selected: Optional[Candidate] = None
candidates: list[Candidate] = None
follow_up_question: Optional[str] = None
The differentiator field is critical. It tells users why each option is distinct, preventing them from staring at a list of near-identical items.
The Disambiguation Engine
The engine decides whether to auto-resolve (one clear winner), present ranked options, or ask narrowing questions based on the score distribution.
class DisambiguationEngine:
def __init__(
self,
auto_resolve_threshold: float = 0.85,
max_options: int = 5,
score_gap_threshold: float = 0.3,
):
self.auto_resolve_threshold = auto_resolve_threshold
self.max_options = max_options
self.score_gap_threshold = score_gap_threshold
def resolve(
self, candidates: list[Candidate]
) -> DisambiguationResult:
if not candidates:
return DisambiguationResult(
resolved=False,
follow_up_question="I could not find any matches. "
"Could you rephrase your request?",
)
ranked = sorted(candidates, key=lambda c: c.score, reverse=True)
# Auto-resolve when the top candidate is dominant
if ranked[0].score >= self.auto_resolve_threshold:
if len(ranked) == 1 or (
ranked[0].score - ranked[1].score
>= self.score_gap_threshold
):
return DisambiguationResult(
resolved=True, selected=ranked[0]
)
# Present top options
top = ranked[: self.max_options]
return DisambiguationResult(
resolved=False,
candidates=top,
follow_up_question=self._build_question(top),
)
def _build_question(self, options: list[Candidate]) -> str:
lines = ["I found several matches. Which one did you mean?"]
for i, opt in enumerate(options, 1):
lines.append(f" {i}. {opt.summary()}")
lines.append("You can pick a number or describe what you need.")
return "\n".join(lines)
Narrowing with Follow-Up Criteria
When options are too similar, ask a targeted narrowing question instead of listing everything.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
class NarrowingStrategy:
def __init__(self, attribute_extractors: dict):
self.extractors = attribute_extractors
def find_distinguishing_attribute(
self, candidates: list[Candidate]
) -> Optional[str]:
"""Find an attribute that splits candidates into groups."""
for attr_name, extractor in self.extractors.items():
values = set()
for c in candidates:
val = extractor(c)
if val:
values.add(val)
if 1 < len(values) <= 4:
return attr_name
return None
def generate_narrowing_question(
self, attr_name: str, candidates: list[Candidate]
) -> str:
values = set()
for c in candidates:
val = self.extractors[attr_name](c)
if val:
values.add(val)
options = ", ".join(sorted(values))
return (
f"To narrow it down, are you looking for a specific "
f"{attr_name}? Options: {options}"
)
Putting It Together
engine = DisambiguationEngine()
candidates = [
Candidate("py101", "Python Basics", "Intro course",
0.72, "For beginners, 4 weeks"),
Candidate("py201", "Python for Data Science", "Intermediate",
0.70, "Pandas and NumPy focus, 6 weeks"),
Candidate("py301", "Advanced Python Patterns", "Expert-level",
0.68, "Design patterns, 8 weeks"),
]
result = engine.resolve(candidates)
if not result.resolved:
print(result.follow_up_question)
# Presents numbered list with differentiators
The engine automatically adapts: a single strong match is auto-resolved, close scores trigger a list, and when candidates are nearly identical, narrowing questions provide a better experience than an overwhelming option dump.
FAQ
How many disambiguation options should you present?
Keep it between two and five. Research on choice overload shows that more than five options increases cognitive load and reduces satisfaction. If you have more matches, show the top five and offer a "show more" option, or ask a narrowing question to reduce the set first.
When should the agent auto-resolve instead of asking?
Auto-resolve when the top candidate's relevance score is significantly higher than the runner-up and exceeds a confidence threshold. A good rule of thumb is auto-resolve when the gap is at least 0.3 and the top score is above 0.85. Always log auto-resolutions so you can tune thresholds from real usage data.
How do you handle users who pick none of the presented options?
Treat "none of these" as a signal to broaden the search or ask an open-ended clarification question. Never loop back with the same options. Instead, ask what specifically they are looking for, or offer to start a fresh search with different keywords.
#Disambiguation #ConversationalAI #DialogDesign #UXPatterns #Python #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.