首页
/ Gemini CLI 的 ask_user 工具深度解析:从参数 Schema 到确认总线实现的交互式提问机制

Gemini CLI 的 ask_user 工具深度解析:从参数 Schema 到确认总线实现的交互式提问机制

2026-09-06 13:39:03作者:凤尚柏Louis

在 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.tsparametersJsonSchema 声明了 minItems: 1maxItems: 4,并将 questionheadertype 列为每个问题的必填字段。

单个问题对象(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" 选项会为 choiceyesno 类型自动追加。

参数名的集中定义在 base-declarations.tsASK_USER_PARAM_QUESTIONSASK_USER_QUESTION_PARAM_QUESTION/HEADER/TYPE/OPTIONS/MULTI_SELECT/PLACEHOLDERASK_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 与运行时校验共同保证):

  1. 一次最多问 4 个问题,questions 为空会直接报错;
  2. choice 类型必须提供 2-4 个 options,每个 option 的 label 必须是非空字符串、description 必须提供(源码校验见 ask-user.ts);
  3. 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):

  • 用户正常提交llmContentJSON.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(各题类型列表)、dismissedempty_submissionanswer_count,可用于遥测与会话统计。

参数的防御性归一化

模型生成的 JSON 字符串常携带字面转义序列(如 \\n)。createInvocation 在进入执行前会对 questionheaderplaceholder 以及每个 option 的 label/descriptionunescape 处理(把字面 \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 变体,携带 titlequestionstypes.ts),供确认队列组件渲染。UI 侧的消费方包括 AskUserActionsContext.tsx(提问动作上下文)以及确认队列组件 ToolConfirmationQueue.tsx。政策引擎侧同样把 ask_user 作为一种可能的决策方向:ToolConfirmationRequest 允许 forcedDecision?: 'allow' | 'deny' | 'ask_user'types.ts),且多份策略文件(如 plan.tomlnon-interactive.tomlwrite.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.tsdefault-legacy.ts 等模型族定义集)。工具类通过 getSchema(modelId) 调用 resolveToolDeclaration 完成解析(ask-user.ts),因此不同模型拿到的是适配其能力的同一语义 Schema。工具在注册时的描述也取自该定义的 base.description

测试、行为评估与可运行示例

围绕该工具,仓库提供了多层次的验证与演示资源:

小结

ask_user 把"模型想问什么"和"用户如何作答"解耦成了清晰的三段式协议:模型按严格的 JSON Schema(1-4 题、三种题型、2-4 选项)发起调用 → core 层通过 AskUserInvocation 的确认回调把问题投递到 UI 并挂起执行 → 用户答案经确认总线以 { [questionIndex]: string } 形式回流,最终序列化为 {"answers": {...}} 返回给模型;用户关闭对话框则得到明确的取消信号与指标。理解这条链路(ask-user.tsconfirmation-bus/types.tsmodel-family-sets)后,你既可以准确构造该工具的调用参数,也能判断其在非交互模式、计划模式等受策略约束场景下的行为边界。

登录后查看全文
热门项目推荐
相关项目推荐