Blog · · 10 min read
AI agent intent recognition: resolve ambiguity before acting

AI agent intent recognition can fail even when a tool call is valid. A user types "approve it," the model picks an approval function, and the tool accepts the arguments. That sequence does not prove which object the user meant or whether approval was the intended action. The product can complete the wrong side effect without reporting a technical error.
Put a typed intent boundary in front of tool execution. The model may propose an action, target, and parameters. Product code then resolves live entities, identifies missing or conflicting values, asks a bounded clarification question, and rechecks authority. An unresolved candidate cannot call a tool, so ambiguity becomes a workflow state rather than a guess.
A valid tool call can still represent the wrong request
Tool validation answers one narrow question: does this payload match the function schema? It does not tell you whether the user intended that function, whether "it" refers to the selected record, or whether a value copied from earlier context is still current.
This distinction already exists in natural-language systems. Google Cloud Dialogflow CX defines an intent as the end user's intention for one turn, with parameters extracted from the utterance. It also supports a no-match outcome when no intent reaches the configured threshold. Rasa distinguishes intents from entities: the intent represents the overall goal, while entities add structured context such as a destination.
An action-taking product needs another boundary after classification. The classifier can be probabilistic, but execution permission cannot be.
A current Stack Overflow practitioner report shows the failure in practice. Short inputs such as approve select the wrong function or call an approval function without the required issue identifier. Follow-up language such as "make it high priority" can also lose the referenced record. This is one developer's report, not a prevalence estimate. The case is useful because each step can be syntactically valid while the final action is wrong.
Treat the model output as an intent candidate
The model should emit a proposal, not an executable command. Product code resolves that proposal. A useful candidate carries the proposed intent, typed slots, evidence from the user's words, possible alternatives, and unresolved references.
intent_candidate:
schema_version: 4
conversation_turn: "turn-981"
proposed_intent: "approve_completion"
alternatives:
- "approve_deployment"
slots:
target:
state: "ambiguous"
mention: "it"
candidates:
- type: "issue"
id: "ISSUE-204"
- type: "deployment"
id: "DEPLOY-7"
approval_scope:
state: "missing"
source_spans:
- text: "approve it"
start: 0
end: 10
parser_outcome: "candidate"
Use a constrained schema so missing values remain missing instead of turning into plausible defaults. OpenAI Structured Outputs documents JSON Schema adherence, required keys, enum constraints, and programmatically detectable refusals. Those guarantees produce a parseable candidate. They do not prove that the model chose the right intent or entity, so the resolver still owns that decision.
Call this record intent_candidate, not command. The name reminds code reviewers and downstream services that the object is untrusted. The executor should accept it only after the product has converted it into a separate execution_candidate.
Model unresolved information as typed states
A nullable target_id does not explain why execution is blocked. Use explicit resolution states:
| State | Meaning | Required product behavior |
|---|---|---|
resolved | One live object matches and the reference is valid | Continue to policy and authorization checks |
missing | The user did not supply a required value | Ask for that value or offer valid choices |
ambiguous | More than one intent or object fits | Present a bounded disambiguation question |
conflicting | The utterance and conversation state disagree | Show the conflict and ask which value wins |
stale | The referenced object or version changed | Refresh the choice before proceeding |
unauthorized | The object exists but the user cannot act on it | Decline without exposing hidden object details |
invalid | The value cannot satisfy product rules | Explain the valid range or format |
refused | The parser could not provide a candidate safely | Stop or route to a supported manual flow |
These states keep a missing value from inheriting an unrelated recent object. They also stop an ambiguous value from masquerading as a low-confidence resolved value. Authorization failures stay separate from clarification prompts, which avoids revealing records the user should not know about.
Microsoft's Conversational Language Understanding documentation separates language processing from action: the component predicts an utterance's intention and extracts information, but it does not perform actions. The same documentation describes multi-turn entity slot filling for progressive information gathering. Keep that separation even if one LLM can classify, extract, and call tools in a single response.
Build the resolution pipeline in six steps
1. Define an intent registry from product actions
Start with product behavior, not with phrases that sound different. Each intent needs required slots, allowed target types, a risk class, a clarification template, an authorization rule, and a downstream operation.
Phrases belong to the same intent when they produce the same product transition under the same policy. Split actions into separate intents when they have different authorization, confirmation, audit, or failure semantics, even if one backend endpoint currently handles both.
The intent registry should be smaller than the tool catalog. Tool discovery finds a technical capability for a known action. Intent recognition works out which product outcome the user requested. If one model choice makes both decisions, diagnosis becomes much harder.
2. Parse the utterance without granting authority
Ask the model for a structured candidate with:
- one proposed intent and any plausible alternatives;
- slots whose states remain explicit rather than receiving guessed defaults;
- source spans showing which words support each value;
- stable product identifiers for objects already established in the conversation;
- a refusal or no-candidate outcome when parsing fails.
OpenAI's function-calling guide documents JSON Schema-defined tools, strict argument generation, and controls over tool choice. Use those mechanics to generate the candidate, but keep side-effecting tools out of the parsing call. The parser cannot invoke a tool it never receives.
3. Resolve entities against live product state
Model output may mention an issue title, customer name, date, or pronoun. Product code maps that language to current records inside the authenticated user's tenant and permission scope.
Use stable IDs from conversation state when a prior UI event established them. Requery mutable records before execution. If two visible records have the same label, return ambiguous with a short list of safe display choices. If a record exists outside the user's scope, return unauthorized without confirming its details.
A long-standing Rasa issue about intent and entity clarification gives a compact example: 10-12 can mean day-month or month-day, while "end of the month" was normalized to the wrong date without a clarification step. The implementation is old, but the product problem is still relevant. A date can be valid and still have two possible meanings.
4. Apply a deterministic decision policy
Do not ask the model whether it feels confident enough to act. Compute the next state from the resolved candidate and product policy.
function decide(candidate, user, product_state):
if candidate.parser_outcome != "candidate":
return stop("unsupported_or_refused")
resolved = resolve_slots(candidate, user, product_state)
if resolved.has("unauthorized"):
return stop("not_allowed")
if resolved.has_any("missing", "ambiguous", "conflicting", "stale"):
return clarify(build_bounded_question(resolved))
if resolved.has("invalid"):
return stop("invalid_input")
execution = freeze_execution_candidate(resolved, product_state)
authorization = authorize_now(user, execution)
if not authorization.allowed:
return stop("not_allowed")
return execute(execution, authorization.receipt)
Resolve only records the user may discover. Clarification happens before the product freezes an execution candidate. Authorization runs again afterward because the target, action, user role, or product state may have changed while the workflow waited.
5. Ask one bounded clarification question
A good clarification question reduces a specific ambiguity. "Can you clarify?" wastes a turn when the product already knows the relevant choices.
For example:
Which item do you want to approve: issue ISSUE-204 or deployment DEPLOY-7?
Store the question, allowed answer shape, candidate version, and expiry. Parse the answer only against that pending choice. Restarting the whole conversation with an unconstrained model call can introduce a new interpretation.
HumanFirst's intent-disambiguation guide distinguishes fallback from cases where several intents match, and it describes presenting top candidates for the user to choose. Product agents need tighter limits. Show only discoverable actions and objects, cap the rounds, and expire the pending candidate when state changes.
After one or two failed rounds, stop and offer a direct form or manual workflow. Otherwise the agent can loop, widen the question, or silently pick an answer to keep moving.
6. Freeze and execute an immutable candidate
When every required slot is resolved, create an immutable execution record. Include the intent-schema version, user and tenant, conversation turn, resolved object IDs and versions, tool-schema version, authorization decision, any required confirmation, and the idempotency key.
The executor accepts only this record type. It rejects raw model arguments and unresolved states. Immediately before the side effect, it checks current authorization and mutable target versions again. After execution, it records a product-owned receipt. The user-visible result comes from that receipt.
Walk through an ambiguous approval
Suppose the conversation recently mentioned issue ISSUE-204 and deployment DEPLOY-7. The user says, "approve it."
The parser proposes approve_completion, lists approve_deployment as an alternative, and marks the target as ambiguous. It does not call either approval tool. The resolver confirms that the user can see both objects, so the policy asks which one they mean.
The user chooses the deployment. Product code binds the answer to DEPLOY-7, reloads its current version, and checks whether the user's role can approve that environment. If the deployment changed or the role was revoked while the question was pending, the candidate becomes stale or unauthorized. The earlier permission check no longer applies.
After resolution, the system creates approve_deployment for DEPLOY-7 at version 19, attaches the current authorization receipt and idempotency key, and invokes the downstream tool. The visible result comes from the execution receipt rather than the model's plan.
The approval tool now receives a resolved and authorized request. The model interprets language, while the product decides whether that interpretation is specific enough to execute.
Test AI agent intent recognition with no-side-effect oracles
Parser unit tests are not enough. Run end-to-end cases against synthetic product records and instrument the execution gateway. Every unresolved case must produce zero attempted side effects, not just a polite clarification message.
Include these cases:
- a one-word action such as
approvewith no target; - a pronoun with two plausible visible records;
- a target that changed after the candidate was created;
- a hidden record with the same label as an allowed record;
- a follow-up answer after the clarification expired;
- conflicting values in the current utterance and prior context;
- a model refusal or malformed candidate;
- a valid intent with a required slot missing;
- a permission change between clarification and execution;
- an adversarial instruction that asks the parser to invent an identifier.
For each case, assert the resolution state, the question or stop reason shown to the user, the number of clarification rounds, the authorization check, tool attempts, durable writes, and the final outcome. Add a positive control for every intent. A fail-closed implementation should not pass by blocking all work.
Intent accuracy and slot accuracy help with diagnosis, but side-effect gates need product outcomes: unresolved execution attempts, wrong-target attempts, unauthorized discovery, clarification completion, safe abandonment, and verified successful actions. These measures match the boundary the product controls.
Avoid five implementation mistakes
- A model confidence number does not grant execution authority. It may rank alternatives, while product state determines ambiguity and permission.
- Keep side-effecting tools out of the first parsing call. Otherwise intent classification can become immediate execution.
- Resolve names only within the current tenant and user's discoverable scope, not across the whole database.
- Persist a typed pending choice with a version and expiry instead of an open-ended clarification conversation.
- Recheck authorization after the user answers, using the final action, target, and current product state.
Start with one high-impact action this week. Define its intent schema, required slots, unresolved states, and clarification question. Route its natural-language entry point through the candidate resolver. Then add a test proving that approve it cannot reach the executor until exactly one current, authorized target is bound. That small AI agent intent recognition boundary gives the rest of the intent registry a concrete pattern.
References
- Google Cloud Dialogflow CX: Intents supports intent, parameter, confidence-threshold, and no-match concepts.
- Microsoft Foundry: Conversational Language Understanding supports separating language understanding from actions and collecting entity slots over multiple turns.
- OpenAI: Structured model outputs supports JSON Schema adherence, required fields, enum constraints, and programmatically detectable refusals.
- OpenAI: Function calling supports schema-defined arguments, strict generation, and explicit tool-choice controls.
- Rasa: Intents and Entities supports the distinction between an overall intent and structured entities.
- HumanFirst: Intent Disambiguation supports presenting candidate intents when several meanings match.
- Stack Overflow API: incorrect intent detection from free-text input provides an author-reported example of short inputs selecting wrong functions or omitting required identifiers.
- Rasa issue #2176 provides an author-reported example of ambiguous date language being normalized without clarification.