当前位置: 首页 > news >正文

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

exportconstAskUserQuestionTool: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,asyncdescription(){returnDESCRIPTION},asyncprompt(){constformat=getQuestionPreviewFormat()if(format===undefined){returnASK_USER_QUESTION_TOOL_PROMPT}returnASK_USER_QUESTION_TOOL_PROMPT+PREVIEW_FEATURE_PROMPT[format]},getinputSchema():InputSchema{returninputSchema()},// ... 其他钩子})

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

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

constquestionOptionSchema=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.')}))constquestionSchema=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.')}))constinputSchema=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:

{"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',表示这个工具必须经过用户确认才能执行:

asynccheckPermissions(input){return{behavior:'ask'asconst,message:'Answer questions?',updatedInput:input,}}

还有一个关键标记——requiresUserInteraction

requiresUserInteraction(){returntrue}

权限处理流程

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

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

ToolUseConfirm——连接 AI 和 UI 的桥梁

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

exporttypeToolUseConfirm<InputextendsAnyObject=AnyObject>={assistantMessage:AssistantMessage;// AI 的原始消息tool:Tool<Input>;// 工具实例(用于 switch 映射)description:string;// 工具描述input:z.infer<Input>;// AI 输出的结构化数据(questions 等)toolUseContext:ToolUseContext;// 工具执行上下文toolUseID:string;// 工具调用 IDpermissionResult:PermissionDecision;// 权限决策结果// 回调函数——用户操作后触发onAllow(updatedInput,permissionUpdates,feedback?,contentBlocks?):void;onReject(feedback?,contentBlocks?):void;recheckPermission():Promise<void>;}

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

组件路由——switch-case 映射

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

functionpermissionComponentForTool(tool:Tool):React.ComponentType<PermissionRequestProps>{switch(tool){caseFileEditTool:returnFileEditPermissionRequestcaseFileWriteTool:returnFileWritePermissionRequestcaseBashTool:returnBashPermissionRequestcaseAskUserQuestionTool:returnAskUserQuestionPermissionRequest// ← 这里caseReviewArtifactTool:returnReviewArtifactPermissionRequest??FallbackPermissionRequest// ... 其他工具default:returnFallbackPermissionRequest}}

设计选择:没有使用通用的 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管理多问题场景下的状态流转:

typeQuestionState={selectedValue?:string|string[]// 选中的选项(单选=string,多选=string[])textInputValue:string// 自定义文本输入}typeState={currentQuestionIndex:number// 当前显示第几个问题answers:Record<string,AnswerValue>// 已收集的回答(question→answer)questionStates:Record<string,QuestionState>// 每个问题的 UI 状态isInTextInput:boolean// 是否正在文本输入模式}typeAction=|{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}functionreducer(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':{constnewState={...state,answers:{...state.answers,[action.questionText]:action.answer}}if(action.shouldAdvance){return{...newState,currentQuestionIndex:newState.currentQuestionIndex+1,isInTextInput:false}}returnnewState}// ...}}

UI 控件映射规则

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

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

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

AskUserQuestionPermissionRequestBody是核心渲染组件,它:

  1. inputSchema.safeParse()解析 AI 输出的 JSON
  2. 提取questions数组
  3. 调用useMultipleChoiceState()初始化状态
  4. 根据currentQuestionIndex决定渲染问题还是提交页
functionAskUserQuestionPermissionRequestBody(t0){const{toolUseConfirm,onDone,onReject,highlight}=t0// 解析 AI 输出的结构化数据constresult=AskUserQuestionTool.inputSchema.safeParse(toolUseConfirm.input)constquestions=result.success?result.data.questions||[]:[]// 初始化多选状态conststate=useMultipleChoiceState()const{currentQuestionIndex,answers,questionStates,isInTextInput,nextQuestion,prevQuestion,updateQuestionState,setAnswer,setTextInputMode}=state// 当前问题 or 提交页constcurrentQuestion=currentQuestionIndex<questions.length?questions[currentQuestionIndex]:nullconstisInSubmitView=currentQuestionIndex===questions.length// 单问题单选时隐藏提交页consthideSubmitTab=questions.length===1&&!questions[0]?.multiSelect// 是否所有问题都已回答constallQuestionsAnswered=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方法不做处理,直接返回数据:

asynccall({questions,answers={},annotations},_context){return{data:{questions,answers,...(annotations&&{annotations})}}}

mapToolResultToToolResultBlockParam——格式化回传

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

mapToolResultToToolResultBlockParam({answers,annotations},toolUseID){constanswersText=Object.entries(answers).map(([questionText,answer])=>{constannotation=annotations?.[questionText]constparts=[`"${questionText}"="${answer}"`]if(annotation?.preview){parts.push(`selected preview:\n${annotation.preview}`)}if(annotation?.notes){parts.push(`user notes:${annotation.notes}`)}returnparts.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底层选择控件
http://www.jsqmd.com/news/847601/

相关文章:

  • 5分钟掌握GoldHEN金手指管理器:PS4游戏修改终极指南
  • FPGA信号发生器设计避坑指南:DDS Compiler IP核里Phase Width到底该设多少?
  • TqApi 初始化参数组合:回测、模拟与实盘怎么配
  • 加州大学圣地亚哥分校揭示大模型其实早就知道什么时候该用工具
  • Windows热键冲突终极解决方案:Hotkey Detective让你告别快捷键失灵
  • 新手入门如何在Taotoken模型广场选择适合自己任务的模型
  • MLX90640官方库在STM32上跑不起来?手把手教你搞定I2C通信那些坑
  • 别再只把JTAG当下载器了!聊聊它在ARM/DSP/FPGA调试中的那些‘隐藏’玩法
  • 缓存:Redis7.0+、多级缓存设计、缓存三大问题解决方案
  • ARM SMMUv3架构里的“快递员”:手把手拆解DTI-ATS与DTI-TBU协议(附官方文档下载)
  • ADI物联网平台实战:从传感器到云端的工业级开发指南
  • 5步掌握12306智能抢票助手:告别手动刷票的烦恼
  • 网盘直链下载助手:九大网盘免费获取真实下载链接的终极解决方案
  • 别再只盯着CS4344了!这5款低成本I2S DAC芯片实测对比(含ES7149/MAX98357A)
  • AI 系统中的过拟合:从直觉到原理
  • 树莓派Zero 2 W转4B扩展板:集成RS485与4G的物联网边缘节点方案
  • d2dx:3大技术突破让20年老游戏在Windows 10重获新生
  • 从SQL Server/MySQL转战GaussDB:一个DBA的gsql命令行实战避坑笔记
  • 避开这3个坑,你的运动想象分类准确率能翻倍:OpenBMI实战经验谈
  • 教程使用Node.js和Taotoken为网站构建一个AI客服接口
  • 从大彩换到迪文串口屏,DMG80480C070_03WTC上手体验与避坑全记录
  • OpenHarmony环境搭建实战:从小凌派开发板入门到系统编译烧录
  • 为团队内部工具配置 Taotoken CLI 实现一键环境统一
  • 德国人工智能研究中心造出了一双“透视眼“
  • MT6737 4G智能模块开发全解析:从硬件设计到量产落地
  • 二氧化碳培养箱百度百科介绍 - 实了个验
  • Python数据分析:用Pandas和Matplotlib实现数据可视化
  • 探索macOS系统优化:Pearcleaner开源清理工具实践指南
  • DataCleaner终极指南:开源数据质量解决方案的完整安装与配置教程
  • 测试工程师驾驭大语言模型的第一步