Gemini CLI 的 ask_user 工具深度解析:从参数 Schema 到确认总线实现的交互式提问机制
在 Gemini CLI 中,模型与用户之间的双向沟通主要依赖 ask_user 工具:它允许 Agent 在执行过程中暂停并弹出交互式对话框,以选择题、自由文本或 Yes/No 三种形式向用户收集偏好、澄清需求或请求决策。本文基于仓库文档 docs/tools/ask-user.md,结合 packages/core/src/tools/ask-user.ts 的实现、packages/core/src/confirmation-bus/types.ts 的总线协议与 JSON Schema 定义,完整讲解该工具的参数结构、运行行为、结果格式与源码级工作原理,读完后可理解其提问流程如何在确认总线(confirmation bus)上闭环,并能编写符合其 Schema 约束的调用参数。
工具定位与基本信息
ask_user 是 Gemini CLI 内置的"沟通类"(Kind.Communicate)工具,用于把模型的疑问转化为用户可操作的 UI 交互。其基本信息如下:
| 属性 | 值 |
|---|---|
| 工具名(Tool name) | ask_user |
| 显示名(Display name) | Ask User |
| 实现文件 | packages/core/src/tools/ask-user.ts |
| 是否需要确认 | 是。该工具天然包含用户交互,执行前必须等待用户在对话框中作答或关闭 |
| 返回给模型的内容 | 以题目序号为键的 JSON 字符串,如 {"answers":{"0": "Option A", "1": "Some text"}} |
从源码结构看,工具类 AskUserTool 在构造时接收一个 MessageBus 实例(packages/core/src/tools/ask-user.ts),说明其提问-作答流程是通过确认总线(MessageBus)在 core 层与 UI 层之间传递消息完成的,而不是在工具内部直接渲染界面。
参数 Schema:questions 数组的完整结构
ask_user 只接受一个参数 questions:一个 1 到 4 个问题的对象数组。该约束不仅写在文档里,也直接体现在工具的 JSON Schema 中——default-legacy.ts 中 parametersJsonSchema 声明了 minItems: 1、maxItems: 4,并将 question、header、type 列为每个问题的必填字段。
单个问题对象(Question)的字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
question |
string | 是 | 完整的提问文本,Schema 描述要求"清晰、具体、以问号结尾" |
header |
string | 是 | 短标签(文档约定最长 16 字符),以 chip/tag 形式展示,例如 "Auth"、"Database";Schema 描述建议用缩写(用 "Auth" 而非 "Authentication") |
type |
string | 是(Schema 层) | 取值 'choice' | 'text' | 'yesno',默认 'choice' |
options |
object 数组 | choice 类型时必填 |
2-4 个可选项;对 text/yesno 类型会被忽略 |
multiSelect |
boolean | 否 | 仅对 choice 生效;为 true 时允许多选,且存在多个标准选项时会自动追加 "All the above" 选项 |
placeholder |
string | 否 | 输入框提示文本:text 类型显示在主输入框;choice/yesno 类型显示在自动附加的 "Other" 自定义输入框中 |
type 的三种取值对应不同的 UI 渲染形态:
'choice':多选项列表,支持多选(multiSelect);'text':自由文本输入;'yesno':是/否确认。源码类型注释(types.ts)进一步说明yesno会附带一个可选的 "Other" 反馈输入框,而 Schema 描述也指出 "Other" 选项会为choice与yesno类型自动追加。
参数名的集中定义在 base-declarations.ts:ASK_USER_PARAM_QUESTIONS、ASK_USER_QUESTION_PARAM_QUESTION/HEADER/TYPE/OPTIONS/MULTI_SELECT/PLACEHOLDER、ASK_USER_OPTION_PARAM_LABEL/DESCRIPTION,Schema 中所有字段均引用这些常量,保证声明与运行时校验使用同一套命名。
TypeScript 侧的类型定义
除了面向模型的 JSON Schema,core 层还定义了供程序使用的 Question 接口(packages/core/src/confirmation-bus/types.ts):
export enum QuestionType {
CHOICE = 'choice',
TEXT = 'text',
YESNO = 'yesno',
}
export interface Question {
question: string;
header: string;
type: QuestionType;
options?: QuestionOption[];
multiSelect?: boolean;
placeholder?: string;
/** 允许题目占用更多纵向空间,而非被严格限高 */
unconstrainedHeight?: boolean;
}
值得注意的是,该接口比文档列出的参数多了一个 unconstrainedHeight 可选字段:从源码结构看,它用于控制对话框渲染时题目区域的高度约束,属于面向 UI 层的扩展能力,并不要求模型在工具调用中提供。
三类问题的调用示例
以下是文档给出的三个标准示例,均可直接作为 ask_user 的参数体使用。
选择题(Multiple Choice)
{
"questions": [
{
"header": "Database",
"question": "Which database would you like to use?",
"type": "choice",
"options": [
{
"label": "PostgreSQL",
"description": "Powerful, open source object-relational database system."
},
{
"label": "SQLite",
"description": "C-library that implements a SQL database engine."
}
]
}
]
}
文本输入(Text Input)
{
"questions": [
{
"header": "Project Name",
"question": "What is the name of your new project?",
"type": "text",
"placeholder": "for example, my-awesome-app"
}
]
}
是/否确认(Yes/No)
{
"questions": [
{
"header": "Deploy",
"question": "Do you want to deploy the application now?",
"type": "yesno"
}
]
}
编写调用参数时的实用约束(均由 Schema 与运行时校验共同保证):
- 一次最多问 4 个问题,
questions为空会直接报错; choice类型必须提供 2-4 个options,每个 option 的label必须是非空字符串、description必须提供(源码校验见 ask-user.ts);label建议 1-5 个词,description给出一句话说明,便于用户在对话框中快速判断。
运行行为、结果格式与边界情况
行为模型
ask_user 的执行流程为:弹出包含全部题目的对话框 → 暂停执行,等待用户作答或关闭对话框 → 把用户答案返回给模型。从源码看,这一"暂停-等待"语义由确认流程实现:AskUserInvocation.shouldConfirmExecute 返回 type: 'ask_user' 的确认详情,把归一化后的 questions 交给 UI,并通过 onConfirm 回调捕获用户的最终结果(ask-user.ts):
return {
type: 'ask_user',
title: 'Ask User',
questions: normalizedQuestions,
onConfirm: async (outcome, payload?) => {
this.confirmationOutcome = outcome;
if (payload && 'answers' in payload) {
this.userAnswers = payload.answers;
}
},
};
返回给模型的 llmContent
execute 的返回分两种情况(ask-user.ts):
- 用户正常提交:
llmContent为JSON.stringify({ answers: this.userAnswers }),即文档所述的以题目位置为键的 JSON 字符串,如{"answers":{"0": "Option A", "1": "Some text"}};returnDisplay会以header → 答案的缩进格式展示在终端中,未作答时显示 "User submitted without answering questions." - 用户关闭对话框(Cancel):
llmContent固定为'User dismissed ask_user dialog without answering.',并附带dismissed: true的指标数据,让模型明确知道提问被放弃,可以据此决定跳过或重新提问。
两种路径都会写入 data.ask_user 指标:question_types(各题类型列表)、dismissed、empty_submission、answer_count,可用于遥测与会话统计。
参数的防御性归一化
模型生成的 JSON 字符串常携带字面转义序列(如 \\n)。createInvocation 在进入执行前会对 question、header、placeholder 以及每个 option 的 label/description 做 unescape 处理(把字面 \r\n、\n 还原为真实换行),并 trim option 的 description(ask-user.ts)。这保证了用户在对话框里看到的是干净的文本,而不是带转义符的原始字符串。
消息总线:ask_user 在确认总线上的协议
ask_user 与 UI 层之间的通信走 core 层的 MessageBus。总线定义了独立的请求/响应消息类型(types.ts):
export enum MessageBusType {
// ...
ASK_USER_REQUEST = 'ask-user-request',
ASK_USER_RESPONSE = 'ask-user-response',
// ...
}
对应消息结构为:
export interface AskUserRequest {
type: MessageBusType.ASK_USER_REQUEST;
questions: Question[];
correlationId: string;
}
export interface AskUserResponse {
type: MessageBusType.ASK_USER_RESPONSE;
correlationId: string;
answers: { [questionIndex: string]: string };
/** true 表示用户未提交答案而取消了对话框 */
cancelled?: boolean;
}
correlationId 用于把响应与请求配对;answers 以题目序号(字符串形式的下标)为键,与 llmContent 的格式一致;cancelled 标志对应前文所述的"用户关闭对话框"分支。
此外,工具确认体系中的可序列化确认详情(SerializableConfirmationDetails)也内置了 ask_user 变体,携带 title 与 questions(types.ts),供确认队列组件渲染。UI 侧的消费方包括 AskUserActionsContext.tsx(提问动作上下文)以及确认队列组件 ToolConfirmationQueue.tsx。政策引擎侧同样把 ask_user 作为一种可能的决策方向:ToolConfirmationRequest 允许 forcedDecision?: 'allow' | 'deny' | 'ask_user'(types.ts),且多份策略文件(如 plan.toml、non-interactive.toml、write.toml)中包含对 ask_user 的规则配置,从源码结构看,非交互与计划模式下的行为受这些策略约束。
Schema 的管理:基线定义与按模型族覆盖
ask_user 的声明不是硬编码的单份 Schema,而是采用"基线 + 覆盖"的管理方式。在 coreTools.ts 中:
export const ASK_USER_DEFINITION: ToolDefinition = {
get base() {
return DEFAULT_LEGACY_SET.ask_user;
},
overrides: (modelId) => getToolSet(modelId).ask_user,
};
base 指向 default-legacy.ts 中的默认声明(前文引用的完整 JSON Schema 即来自这里,工具描述为 "Ask the user one or more questions to gather preferences, clarify requirements, or make decisions.");overrides 则允许针对特定模型族返回不同的声明(仓库中存在 gemini-3.ts、default-legacy.ts 等模型族定义集)。工具类通过 getSchema(modelId) 调用 resolveToolDeclaration 完成解析(ask-user.ts),因此不同模型拿到的是适配其能力的同一语义 Schema。工具在注册时的描述也取自该定义的 base.description。
测试、行为评估与可运行示例
围绕该工具,仓库提供了多层次的验证与演示资源:
- 单元测试:ask-user.test.ts 覆盖参数校验、归一化与执行结果等行为;
- 行为评估(eval):ask_user.eval.ts 以端到端方式评估模型在真实会话中提问的行为是否符合预期(evals 目录的用途见 evals/README.md);
- UI 演示:packages/cli/examples/ask-user-dialog-demo.tsx 是一个独立的可运行示例,展示对话框的交互形态;
- 相关文档:工具总览见 docs/reference/tools.md;
ask_user也在 docs/cli/plan-mode.md、docs/cli/telemetry.md 等文档中被引用,说明它与计划模式确认、遥测统计等场景存在联动。
小结
ask_user 把"模型想问什么"和"用户如何作答"解耦成了清晰的三段式协议:模型按严格的 JSON Schema(1-4 题、三种题型、2-4 选项)发起调用 → core 层通过 AskUserInvocation 的确认回调把问题投递到 UI 并挂起执行 → 用户答案经确认总线以 { [questionIndex]: string } 形式回流,最终序列化为 {"answers": {...}} 返回给模型;用户关闭对话框则得到明确的取消信号与指标。理解这条链路(ask-user.ts、confirmation-bus/types.ts、model-family-sets)后,你既可以准确构造该工具的调用参数,也能判断其在非交互模式、计划模式等受策略约束场景下的行为边界。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00