Claude Code AskUserQuestion 交互式提问机制深度解析

背景

在 Claude Code 中,AI 模型并非只能被动等待用户输入。当 AI 在推理过程中发现信息不完整、需要用户做出选择或确认时,它能够主动向用户"提问"------在终端中渲染出带有选项的交互式 UI(单选、多选、文本输入),等待用户操作后继续执行。

这个能力背后的实现,本质上是一个 Tool Calling + Permission 中断 + React 组件映射 的三方协作系统。

整体架构

复制代码
┌─────────────────────────────────────────────────────────┐
│                     AI 模型推理层                         │
│  AI 判断需要用户输入 → 调用 AskUserQuestion 工具          │
│  输出 tool_use JSON(符合预定义 schema)                   │
└──────────────────────┬──────────────────────────────────┘
                       │ tool_use JSON
                       ▼
┌─────────────────────────────────────────────────────────┐
│                   Permission 拦截层                       │
│  checkPermissions() → behavior: 'ask'                    │
│  创建 ToolUseConfirm 对象 → 推入权限队列 → 暂停执行       │
└──────────────────────┬──────────────────────────────────┘
                       │ ToolUseConfirm 对象
                       ▼
┌─────────────────────────────────────────────────────────┐
│                   UI 渲染层(React/Ink)                  │
│  PermissionRequest 组件 → switch(tool) 映射              │
│  → AskUserQuestionPermissionRequest 渲染交互式 UI        │
│  (Select / SelectMulti / TextInput / Preview)           │
└──────────────────────┬──────────────────────────────────┘
                       │ 用户选择 → onAllow(answers)
                       ▼
┌─────────────────────────────────────────────────────────┐
│                   结果回传层                               │
│  answers → updatedInput → tool.call() 执行               │
│  → tool_result 返回给 AI → AI 继续推理                    │
└─────────────────────────────────────────────────────────┘

第一层:Tool 定义------AI 侧的"提问协议"

核心文件

src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx

工具注册

每个工具通过 buildTool() 注册,提供 nameinputSchemaoutputSchemacheckPermissions 等钩子。AI 模型在推理时看到的工具列表中,就包含了 AskUserQuestion

typescript 复制代码
export const AskUserQuestionTool: Tool<InputSchema, Output> = buildTool({
  name: ASK_USER_QUESTION_TOOL_NAME,  // "AskUserQuestion"
  searchHint: 'prompt the user with a multiple-choice question',
  maxResultSizeChars: 100_000,
  shouldDefer: true,

  async description() {
    return DESCRIPTION
  },

  async prompt() {
    const format = getQuestionPreviewFormat()
    if (format === undefined) {
      return ASK_USER_QUESTION_TOOL_PROMPT
    }
    return ASK_USER_QUESTION_TOOL_PROMPT + PREVIEW_FEATURE_PROMPT[format]
  },

  get inputSchema(): InputSchema {
    return inputSchema()
  },

  // ... 其他钩子
})

Input Schema------AI 必须遵守的结构化协议

工具的 inputSchema 定义了 AI 输出时必须遵守的 JSON 结构。这是整个机制的数据契约:

typescript 复制代码
const questionOptionSchema = lazySchema(() => z.object({
  label: z.string().describe(
    'The display text for this option that the user will see and select. ' +
    'Should be concise (1-5 words) and clearly describe the choice.'
  ),
  description: z.string().describe(
    'Explanation of what this option means or what will happen if chosen. ' +
    'Useful for providing context about trade-offs or implications.'
  ),
  preview: z.string().optional().describe(
    'Optional preview content rendered when this option is focused. ' +
    'Use for mockups, code snippets, or visual comparisons.'
  )
}))

const questionSchema = lazySchema(() => z.object({
  question: z.string().describe(
    'The complete question to ask the user. Should be clear, specific, ' +
    'and end with a question mark.'
  ),
  header: z.string().describe(
    'Very short label displayed as a chip/tag (max 12 chars). ' +
    'Examples: "Auth method", "Library", "Approach".'
  ),
  options: z.array(questionOptionSchema())
    .min(2).max(4)
    .describe(
      'The available choices for this question. Must have 2-4 options. ' +
      'There should be no "Other" option, that will be provided automatically.'
    ),
  multiSelect: z.boolean().default(false).describe(
    'Set to true to allow the user to select multiple options instead of just one.'
  )
}))

const inputSchema = lazySchema(() =>
  z.strictObject({
    questions: z.array(questionSchema()).min(1).max(4)
      .describe('Questions to ask the user (1-4 questions)'),
    answers: z.record(z.string(), z.string()).optional()
      .describe('User answers collected by the permission component'),
    annotations: annotationsSchema(),
    metadata: z.object({
      source: z.string().optional()
    }).optional(),
  }).refine(UNIQUENESS_REFINE.check, {
    message: UNIQUENESS_REFINE.message,
  })
)

设计要点

  • 限制 1-4 个问题,每个问题 2-4 个选项------防止 AI 滥用
  • 自动提供 "Other" 选项------用户始终可以自由输入
  • schema 中的 .describe() 文本就是给 AI 模型的使用指南
  • refine 校验确保问题文本和选项标签的唯一性

AI 实际输出的 JSON 示例

当 AI 决定需要提问时,它会输出这样的 tool_use:

json 复制代码
{
  "type": "tool_use",
  "name": "AskUserQuestion",
  "input": {
    "questions": [
      {
        "question": "Which library should we use for date formatting?",
        "header": "Library",
        "options": [
          { "label": "date-fns", "description": "Lightweight, tree-shakeable, functional API" },
          { "label": "dayjs", "description": "Moment.js compatible, small bundle size" },
          { "label": "Temporal", "description": "Modern native API, no dependencies needed" }
        ],
        "multiSelect": false
      }
    ]
  },
  "id": "toolu_01ABC123"
}

第二层:Permission 拦截------工具执行的"红绿灯"

核心文件

  • src/utils/permissions/permissions.ts --- 权限检查入口
  • src/hooks/toolPermission/handlers/interactiveHandler.ts --- 交互式权限处理
  • src/components/permissions/PermissionRequest.tsx --- UI 路由组件

checkPermissions------固定的"必须询问"策略

AskUserQuestionToolcheckPermissions 方法始终返回 behavior: 'ask',表示这个工具 必须 经过用户确认才能执行:

typescript 复制代码
async checkPermissions(input) {
  return {
    behavior: 'ask' as const,
    message: 'Answer questions?',
    updatedInput: input,
  }
}

还有一个关键标记------requiresUserInteraction

typescript 复制代码
requiresUserInteraction() {
  return true
}

权限处理流程

behavior === 'ask' 时,系统进入 interactiveHandler,创建一个 ToolUseConfirm 对象并推入权限队列:

复制代码
tool_use 到达
    ↓
checkPermissions() → { behavior: 'ask' }
    ↓
interactiveHandler 创建 ToolUseConfirm
    ↓
ctx.pushToQueue(toolUseConfirm)  ← 推入队列,暂停工具执行
    ↓
等待用户操作(onAllow / onReject)

ToolUseConfirm------连接 AI 和 UI 的桥梁

ToolUseConfirm 是一个携带完整上下文的对象(定义在 PermissionRequest.tsx):

typescript 复制代码
export type ToolUseConfirm<Input extends AnyObject = AnyObject> = {
  assistantMessage: AssistantMessage;   // AI 的原始消息
  tool: Tool<Input>;                     // 工具实例(用于 switch 映射)
  description: string;                   // 工具描述
  input: z.infer<Input>;                 // AI 输出的结构化数据(questions 等)
  toolUseContext: ToolUseContext;         // 工具执行上下文
  toolUseID: string;                     // 工具调用 ID
  permissionResult: PermissionDecision;  // 权限决策结果

  // 回调函数------用户操作后触发
  onAllow(updatedInput, permissionUpdates, feedback?, contentBlocks?): void;
  onReject(feedback?, contentBlocks?): void;
  recheckPermission(): Promise<void>;
}

这个对象是连接后端(工具执行)和前端(UI 渲染)的桥梁。AI 的结构化数据通过 input 字段传递给 UI,用户的操作通过 onAllow / onReject 回传。

组件路由------switch-case 映射

PermissionRequest 组件维护了一个工具到 UI 组件的映射表:

typescript 复制代码
function permissionComponentForTool(tool: Tool):
  React.ComponentType<PermissionRequestProps>
{
  switch (tool) {
    case FileEditTool:
      return FileEditPermissionRequest
    case FileWriteTool:
      return FileWritePermissionRequest
    case BashTool:
      return BashPermissionRequest
    case AskUserQuestionTool:
      return AskUserQuestionPermissionRequest  // ← 这里
    case ReviewArtifactTool:
      return ReviewArtifactPermissionRequest ?? FallbackPermissionRequest
    // ... 其他工具
    default:
      return FallbackPermissionRequest
  }
}

设计选择:没有使用通用的 schema→UI 渲染引擎,而是每个工具一个专用组件。这样每个 UI 可以针对自己的交互场景做深度优化(比如 Bash 工具显示命令预览,FileEdit 工具显示 diff)。


第三层:UI 渲染------React/Ink 交互式组件

核心文件

  • src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx
  • src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx
  • src/components/permissions/AskUserQuestionPermissionRequest/use-multiple-choice-state.ts
  • src/components/CustomSelect/select.tsx

组件层级

复制代码
AskUserQuestionPermissionRequest       ← 入口组件
  ├─ AskUserQuestionPermissionRequestBody  ← 解析 input,管理状态
  │   ├─ QuestionView                      ← 单个问题渲染
  │   │   ├─ Select / SelectMulti          ← 选项控件
  │   │   ├─ PreviewQuestionView           ← 预览面板(如果有 preview)
  │   │   └─ TextInput                     ← "Other" 自定义输入
  │   └─ SubmitQuestionsView               ← 多问题时的提交确认页
  └─ useMultipleChoiceState()             ← 状态管理 hook

状态管理------useReducer 管理复杂交互

use-multiple-choice-state.ts 使用 useReducer 管理多问题场景下的状态流转:

typescript 复制代码
type QuestionState = {
  selectedValue?: string | string[]  // 选中的选项(单选=string,多选=string[])
  textInputValue: string             // 自定义文本输入
}

type State = {
  currentQuestionIndex: number               // 当前显示第几个问题
  answers: Record<string, AnswerValue>       // 已收集的回答(question→answer)
  questionStates: Record<string, QuestionState>  // 每个问题的 UI 状态
  isInTextInput: boolean                     // 是否正在文本输入模式
}

type Action =
  | { type: 'next-question' }                // 下一题
  | { type: 'prev-question' }                // 上一题
  | { type: 'update-question-state';         // 更新问题 UI 状态
      questionText: string;
      updates: Partial<QuestionState>;
      isMultiSelect: boolean }
  | { type: 'set-answer';                    // 设置答案
      questionText: string;
      answer: string;
      shouldAdvance: boolean }
  | { type: 'set-text-input-mode';           // 切换文本输入模式
      isInInput: boolean }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'next-question':
      return { ...state,
        currentQuestionIndex: state.currentQuestionIndex + 1,
        isInTextInput: false }

    case 'prev-question':
      return { ...state,
        currentQuestionIndex: Math.max(0, state.currentQuestionIndex - 1),
        isInTextInput: false }

    case 'set-answer': {
      const newState = {
        ...state,
        answers: { ...state.answers,
          [action.questionText]: action.answer }
      }
      if (action.shouldAdvance) {
        return { ...newState,
          currentQuestionIndex: newState.currentQuestionIndex + 1,
          isInTextInput: false }
      }
      return newState
    }
    // ...
  }
}

UI 控件映射规则

QuestionView.tsx 根据 schema 中的字段决定渲染什么控件:

Schema 字段 渲染的 UI 控件 说明
multiSelect: false Select 组件 单选,方向键导航,Enter 确认
multiSelect: true SelectMulti 组件 多选,空格切换,Enter 提交
始终存在 "Other" TextInput 自动追加的自由输入选项
preview 有值 PreviewQuestionView 左右分栏,右侧显示预览内容
多个问题 QuestionNavigationBar 问题导航栏 + 提交确认页

入口组件------解析 input 并初始化渲染

AskUserQuestionPermissionRequestBody 是核心渲染组件,它:

  1. inputSchema.safeParse() 解析 AI 输出的 JSON
  2. 提取 questions 数组
  3. 调用 useMultipleChoiceState() 初始化状态
  4. 根据 currentQuestionIndex 决定渲染问题还是提交页
typescript 复制代码
function AskUserQuestionPermissionRequestBody(t0) {
  const { toolUseConfirm, onDone, onReject, highlight } = t0

  // 解析 AI 输出的结构化数据
  const result = AskUserQuestionTool.inputSchema.safeParse(toolUseConfirm.input)
  const questions = result.success ? result.data.questions || [] : []

  // 初始化多选状态
  const state = useMultipleChoiceState()
  const {
    currentQuestionIndex, answers, questionStates,
    isInTextInput, nextQuestion, prevQuestion,
    updateQuestionState, setAnswer, setTextInputMode
  } = state

  // 当前问题 or 提交页
  const currentQuestion = currentQuestionIndex < questions.length
    ? questions[currentQuestionIndex]
    : null
  const isInSubmitView = currentQuestionIndex === questions.length

  // 单问题单选时隐藏提交页
  const hideSubmitTab = questions.length === 1 && !questions[0]?.multiSelect

  // 是否所有问题都已回答
  const allQuestionsAnswered =
    questions.every(q => q?.question && !!answers[q.question])

  // ... 渲染逻辑
}

第四层:结果回传------用户选择如何回到 AI

核心文件

src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx

提交流程

用户点击确认后:

  1. submitAnswers() 收集所有回答,构建 updatedInput
  2. 调用 toolUseConfirm.onAllow(updatedInput, [], undefined, contentBlocks)
  3. Permission 系统将 updatedInput 传递给 tool.call()

tool.call------透传数据

call 方法不做处理,直接返回数据:

typescript 复制代码
async call({ questions, answers = {}, annotations }, _context) {
  return {
    data: { questions, answers,
      ...(annotations && { annotations }) }
  }
}

mapToolResultToToolResultBlockParam------格式化回传

这个方法将用户的回答格式化为 AI 可以理解的 tool_result

typescript 复制代码
mapToolResultToToolResultBlockParam({ answers, annotations }, toolUseID) {
  const answersText = Object.entries(answers)
    .map(([questionText, answer]) => {
      const annotation = annotations?.[questionText]
      const parts = [`"${questionText}"="${answer}"`]
      if (annotation?.preview) {
        parts.push(`selected preview:\n${annotation.preview}`)
      }
      if (annotation?.notes) {
        parts.push(`user notes: ${annotation.notes}`)
      }
      return parts.join(' ')
    })
    .join(', ')

  return {
    type: 'tool_result',
    content: `User has answered your questions: ${answersText}. ` +
             `You can now continue with the user's answers in mind.`,
    tool_use_id: toolUseID,
  }
}

AI 收到这个 tool_result 后,就能读取用户的回答,继续推理。


完整端到端流程

复制代码
用户提问:"帮我选一个日期库"
         ↓
    AI 开始推理
         ↓
AI:"我需要问用户选哪个库" → 生成 tool_use:
    {
      name: "AskUserQuestion",
      input: { questions: [{ question: "选哪个?", options: [...] }] }
    }
         ↓
    Tool Calling 框架接收 tool_use
         ↓
    checkPermissions() → { behavior: 'ask' }
         ↓
    interactiveHandler 创建 ToolUseConfirm
    推入权限队列 → 暂停工具执行
         ↓
    PermissionRequest 组件 switch 映射
    → AskUserQuestionPermissionRequest
         ↓
    解析 input.questions → 渲染:
    ┌─────────────────────────────────────┐
    │  Library                             │
    │  ○ date-fns  Lightweight...          │
    │  ○ dayjs     Moment compatible...    │
    │  ○ Temporal  Modern native...        │
    │  ○ Other [输入框]                     │
    └─────────────────────────────────────┘
         ↓
    用户选择 "dayjs" → onAllow(answers)
         ↓
    tool.call({ answers: { "选哪个?": "dayjs" } })
         ↓
    → tool_result:
    "User has answered your questions: "选哪个?"="dayjs".
     You can now continue with the user's answers in mind."
         ↓
    AI 读取回答 → "好的,使用 dayjs,开始编写代码..."

设计模式总结

1. 声明式交互协议

AI 不是随意输出结构化数据,而是调用预定义的工具。工具的 inputSchema(Zod)同时服务于两个目的:

  • 约束 AI 输出:模型必须生成符合 schema 的 JSON
  • 指导 UI 渲染:前端根据 schema 中的字段决定渲染什么控件

2. Permission 中断模式

工具执行不是"直通"的,而是经过权限检查的中断层。behavior: 'ask' 的工具会暂停执行,等待用户响应后才继续。这种模式统一处理了所有需要用户介入的场景------不只是提问,还包括 Bash 命令确认、文件编辑确认等。

3. 专用组件 vs 通用渲染

Claude Code 没有采用"通用 schema→UI 渲染引擎"的方案,而是每个工具一个专用 Permission 组件。好处是每个交互场景可以做深度优化(Bash 显示命令预览,FileEdit 显示 diff,AskUserQuestion 显示选项),代价是扩展新工具时需要同时写后端逻辑和前端组件。

4. 可复用的状态管理

useMultipleChoiceState 是一个独立的 reducer hook,封装了多问题导航、选项切换、文本输入等通用交互逻辑。它不耦合于任何特定组件,可以被其他需要类似交互的工具复用。


关键源码文件索引

文件路径 职责
src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx 工具定义、schema、权限检查、结果格式化
src/tools/AskUserQuestionTool/prompt.ts 给 AI 模型的工具使用指南
src/utils/permissions/permissions.ts 权限检查入口
src/hooks/toolPermission/handlers/interactiveHandler.ts 交互式权限处理
src/components/permissions/PermissionRequest.tsx 工具→UI 组件路由(switch-case)
src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx 提问 UI 入口组件
src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx 单个问题渲染(Select/SelectMulti/TextInput)
src/components/permissions/AskUserQuestionPermissionRequest/use-multiple-choice-state.ts 多问题状态管理 reducer
src/components/CustomSelect/select.tsx 底层选择控件
相关推荐
qcx234 小时前
【AI Daily】每日Arxiv论文研读Top5-2026-05-18
人工智能·ai·llm·论文·agent·arxiv
叶子Talk4 小时前
谷歌I/O明日开幕:Gemini 3.2对标GPT-5.5,AI眼镜十年后重启
人工智能·gpt·ai·谷歌·gemini·google i/o·gpt-5.5
overwizard4 小时前
AI工程双剑:gstack与Superpowers实战指南
人工智能·claude code·vibe-coding·skills·cc switch
踏着七彩祥云的小丑4 小时前
AI——多模态 / 复杂文档 RAG
人工智能·ai
Yuk丶4 小时前
LPM的AI 角色三大核心技术实现:长效记忆、人格锁定、低延迟口语化
人工智能·ai·ue4·虚幻·ue4客户端开发
@蔓蔓喜欢你4 小时前
低代码平台设计:我是如何构建可视化表单编辑器的
人工智能·ai
搬砖的小码农_Sky4 小时前
如何用AMD Radeon游戏卡打造AI工作站?
人工智能·ai·gpu算力·agi
二月夜4 小时前
Claude Code 接入 DeepSeek V4
deepseek·claude code
极客老王说Agent5 小时前
实在Agent物流对账全流程自动化方案与落地案例:2026智享财务新标杆
运维·人工智能·ai·chatgpt·自动化