Gemini CLI 规划工具体系:enter_plan_mode 与 exit_plan_mode 如何驱动"先规划、后实施"的工作流
本文基于 Gemini CLI 仓库中的 planning 工具文档 与对应源码,深入解析规划(Planning)工具的设计与实现:enter_plan_mode 如何让 Agent 安全地切入只读"Plan Mode"进行调研与规划,exit_plan_mode 如何把最终计划提交给用户正式审批并切换回实施模式。读完后,你将掌握这两个工具的参数定义、确认(confirmation)流程、计划文件的路径校验机制、审批模式(Approval Mode)切换逻辑,以及背后的路径安全设计与测试验证方式。
1. 规划工具的定位:为什么需要 Plan Mode
复杂改动往往需要先充分调研、再动手修改。Gemini CLI 的规划工具(Planning tools)让 Gemini 能够:
- 切换到一个安全的只读"Plan Mode",在该模式下 Agent 只被允许使用只读类工具,用于安全地探索代码库、梳理改动方案;
- 在规划完成后,向用户发出"计划定稿"信号,把最终计划呈现给用户并请求正式批准,批准后才进入实施阶段。
这一机制的核心价值是"读写分离":规划阶段绝不产生副作用,实施阶段则基于用户已审阅的书面计划进行,显著降低了 Agent 在不确定的情况下直接改动代码的风险。规划工具由两个工具组成,源码分别位于 enter-plan-mode.ts 与 exit-plan-mode.ts。
2. enter_plan_mode:进入只读规划模式
2.1 工具定义与参数
| 项目 | 内容 |
|---|---|
| 工具名 | enter_plan_mode |
| 显示名 | Enter Plan Mode |
| 源码文件 | packages/core/src/tools/enter-plan-mode.ts |
| 工具分类 | Kind.Plan |
| 参数 | reason(string,可选):进入计划模式的简短原因,例如 "Starting a complex feature implementation" |
| 是否需要确认 | 是。用户会看到确认提示:"This will restrict the agent to read-only tools to allow for safe planning." |
该工具通常由 Agent 在用户以自然语言要求"先制定一个计划"时调用。从 tool-names.ts 中的常量 ENTER_PLAN_MODE_TOOL_NAME 可知,工具名在多处(注册表、策略引擎、测试)统一引用,避免硬编码漂移。
注意(来自官方文档):当 CLI 处于 YOLO 模式(全自动执行、不请求确认)时,该工具不可用。这一限制在源码注释中同样有印证——exit-plan-mode.ts 的注释写明 "By default, YOLO mode in interactive environment cannot enter/exit plan mode"。也就是说,Plan Mode 本质上是"需要人工把关"的流程,与 YOLO 的无人值守语义天然冲突。
2.2 执行流程(源码级)
从 EnterPlanModeInvocation.execute 的实现可以看到完整的执行链路:
- 检查确认结果:如果用户在确认对话框中取消了操作,直接返回
llmContent: 'User cancelled entering Plan Mode.',不做任何模式切换; - 切换审批模式:调用
this.config.setApprovalMode(ApprovalMode.PLAN),把 CLI 的审批模式正式切换为PLAN,后续工具调用即受只读限制约束; - 预创建计划目录:调用
this.config.storage.getPlansDir()获取项目的计划目录,若不存在则递归创建(fs.mkdirSync(plansDir, { recursive: true }))。源码注释解释了原因:"In sandboxed environments, the plans directory must exist on the host before it can be bound/allowed in the sandbox"——在沙箱环境中,计划目录必须先宿主机上存在,才能被绑定/放行给沙箱。创建失败不会中断流程(仅记录 debug 日志),后续write_file时还会再尝试; - 返回 LLM 可见的输出:
llmContent固定为'Switching to Plan mode.',同时给 UI 的returnDisplay会带上用户可读的原因(若提供了reason参数)。
此外,工具的确认逻辑 shouldConfirmExecute 遵循统一的策略管线:先通过 getMessageBusDecision 向消息总线(confirmation-bus/message-bus.ts)征询策略决策:
- 决策为
allow(企业策略/用户策略显式放行):跳过确认直接执行; - 决策为
deny:抛出 "denied by policy" 错误,执行被拦截; - 决策为
ask_user(默认交互场景):弹出type: 'info'类型的确认框,标题为 "Enter Plan Mode"。
这种"策略先行、确认兜底"的设计,使 Plan Mode 入口同时受控于策略引擎与用户交互两个层面。
3. exit_plan_mode:计划定稿与正式审批
3.1 工具定义与参数
| 项目 | 内容 |
|---|---|
| 工具名 | exit_plan_mode |
| 显示名 | Exit Plan Mode |
| 源码文件 | packages/core/src/tools/exit-plan-mode.ts |
| 工具分类 | Kind.Plan |
| 参数 | 计划文件的文件名(string,必填),必须位于项目计划目录内,例如 ~/.gemini/tmp/<project>/plans/ 下的 feature-x.md |
| 是否需要确认 | 是。向用户展示最终计划并请求正式批准实施 |
关于参数名需要做一个精确说明:planning.md 文档 中写作 plan_path,而当前仓库源码中的实际参数名是 plan_filename(见 base-declarations.ts 中的常量 EXIT_PLAN_PARAM_PLAN_FILENAME = 'plan_filename',以及 ExitPlanModeParams 接口)。工具描述要求 Agent 只给文件名(如 "feature-x.md","Do not provide an absolute path"),而底层解析逻辑对绝对路径和项目相对路径同样做了兼容处理(见下一节)。
该工具的官方描述(来自 dynamic-declaration-helpers.ts)包含两条强约束,这也是文档中强调的行为要求:
- 调用前必须先与用户达成非正式共识(informal agreement):Agent 必须先在聊天中就拟议方案与用户讨论达成一致,然后才能调用本工具提交正式审批;
- 这是退出 Plan Mode 的必经之路:"This tool MUST be used to exit Plan Mode before any source code edits can be performed"——未走审批就不能实施。
3.2 计划路径的三重安全校验
exit_plan_mode 的核心安全点在于:模型提供的是不受信任的路径输入,必须防止路径穿越、访问计划目录之外的文件。这一职责由 planUtils.ts 承担,关键函数是 resolveAndValidatePlanPath,它按优先级尝试三种解析方式:
- 绝对路径:若模型直接给了绝对路径,先解析出真实路径(
resolveToRealPath会消除符号链接影响),必须落在 plans 目录之内(isSubpath判定)才接受; - 项目相对路径:相对项目根(
projectRoot)解析后,同样要求落在 plans 目录之内; - 默认情况——相对 plans 目录:把路径当作相对 plans 目录处理,再解析真实路径做边界校验。
三种方式都失败时抛出 PlanErrorMessages.PATH_ACCESS_DENIED,错误文案形如:Access denied: plan path (xxx) must be within the designated plans directory (yyy)。validatePlanPath 在此之上补充文件存在性校验(FILE_NOT_FOUND),validatePlanContent 则用 isEmpty 校验文件非空(FILE_EMPTY)——空文件不允许提交审批。这些标准错误消息由 PlanErrorMessages 集中定义,注释说明它们是"Shared between backend tools and CLI UI for consistency",保证前后端文案一致。
校验失败时,工具并不会抛错,而是把错误信息作为 llmContent 返回给模型(例如 llmContent: this.planValidationError,returnDisplay: 'Error: Invalid plan'),使模型能够自我纠正:先创建计划文件、再重新请求审批。参数层面还有前置的 validateToolParamValues,在 schema 校验阶段就拒绝空文件名和越界路径。
3.3 审批流程与模式切换
ExitPlanModeInvocation.shouldConfirmExecute 的执行顺序体现了"先校验、后交互"的原则:
- 依次执行路径校验(
validatePlanPath)与内容校验(validatePlanContent),任一失败则记录planValidationError并不弹出确认框; - 向消息总线征询策略决策:
deny:抛错拦截;allow:自动批准,confirmationOutcome直接置为ProceedOnce,并按 getAllowApprovalMode 选择目标审批模式——非交互环境下默认切到YOLO以支持自动化执行,交互环境下默认切到DEFAULT;ask_user:弹出type: 'exit_plan_mode'的专用审批对话框(标题 "Plan Approval"),向用户展示计划内容并请求正式批准。用户在对话框中选择批准后进入的实施模式(DEFAULT或AUTO_EDIT)与是否附带的反馈(feedback),都会通过payload(ToolExitPlanModeConfirmationPayload)回传给工具。
execute 的分支逻辑如下:
- 验证失败:返回错误信息供模型修正;
- 用户取消:返回
llmContent: 'User cancelled the plan approval dialog. The plan was not approved and you are still in Plan Mode.',保持在 Plan Mode; - 用户批准(
payload.approved === true):- 防御性检查:目标模式若仍是
PLAN则抛错("Unexpected approval mode"); this.config.setApprovalMode(newMode)切换到用户选择的模式;this.config.setApprovedPlanPath(resolvedPlanPath)将已批准的计划路径登记到配置中;- 通过 logPlanExecution 上报
PlanExecutionEvent遥测事件; - 返回给模型的
llmContent会明确指示:"Read and follow the plan strictly during implementation",并附上计划文件路径;
- 防御性检查:目标模式若仍是
- 用户拒绝:
- 有反馈时返回
Plan rejected. User feedback: <feedback>,要求模型据此修订计划; - 无反馈时返回 "No feedback provided",提示模型主动向用户询问具体意见。
- 有反馈时返回
两条路径都保持在 Plan Mode,形成"提出计划 → 反馈 → 修订 → 再提交"的闭环。
3.4 策略直通(policy allow)的边界情况
源码中有一段值得注意的注释(exit-plan-mode.ts#L207-L213):当用户策略对 exit_plan_mode 授予 allow 时,调度器会完全跳过确认阶段,shouldConfirmExecute 根本不会被调用,approvalPayload 会是 null。因此 execute 中对 approvalPayload 做了兜底:若为空,则按"已批准 + 策略默认审批模式"处理。这一细节保证了自动化场景(CI、headless)下计划流程不会因为缺少 UI 交互而卡死。
4. 与 Plan Mode 的整体关系
enter_plan_mode / exit_plan_mode 只是 Plan Mode 状态机的两个入口点。完整的模式语义、UI 表现与手动切换方式在 plan-mode 文档 中有更广泛的说明;本文聚焦的两个工具承担的是由模型主动发起的模式切换职责:
enter_plan_mode对应状态机的"进入"边:setApprovalMode(ApprovalMode.PLAN)+ 预建 plans 目录;exit_plan_mode对应"退出"边:审批通过后切换到DEFAULT/AUTO_EDIT(或自动化场景下的YOLO),并登记批准计划路径;- 审批模式常量定义于 policy/types.ts,工具可用性由策略引擎(policy 目录)按模式过滤,从而保证 Plan Mode 下写类工具被禁用。
工具声明层面,coreTools.ts 中的 ENTER_PLAN_MODE_DEFINITION 与 getExitPlanModeDefinition 遵循"base 声明 + 按模型覆盖(overrides)"的模式:不同模型族(如 gemini-3 工具集)可以针对特定模型提供差异化的参数 schema,resolveToolDeclaration 在运行时按 modelId 解析最终声明。
5. 测试如何验证这些行为
两个工具均有对应的单元测试,可作为行为契约参考:
- enter-plan-mode.test.ts:覆盖确认流程、取消分支、模式切换等;
- exit-plan-mode.test.ts:覆盖路径校验失败(目录外路径、文件不存在、空文件)、批准/拒绝/取消各分支的输出内容,以及策略
allow直通场景; - planUtils 相关校验 的错误消息常量被前后端共享,保证"Access denied / file does not exist / file is empty"等文案在工具层与 UI 层一致。
6. 小结
| 维度 | enter_plan_mode |
exit_plan_mode |
|---|---|---|
| 作用 | 切入只读 Plan Mode,安全探索与规划 | 提交定稿计划,请求正式批准并退出 Plan Mode |
| 关键参数 | reason(可选) |
plan_filename(必填,须位于计划目录内且非空) |
| 核心副作用 | setApprovalMode(PLAN) + 预建 plans 目录 |
校验通过并批准后切换到 DEFAULT/AUTO_EDIT,登记批准计划路径,上报遥测 |
| 确认机制 | 策略 allow/deny/ask_user 三级决策 + info 确认框 | 专用 "Plan Approval" 审批框(展示计划、返回反馈) |
| YOLO 限制 | 交互 YOLO 模式下不可用 | 同左;策略 allow 时非交互环境自动切 YOLO 执行 |
| 失败行为 | 用户取消 → 不切换模式 | 校验失败/拒绝 → 保持 Plan Mode,返回信息供模型修订 |
从源码结构看,Gemini CLI 的规划工具把"安全"落实到了三个层面:模式层(Plan Mode 下只读限制)、路径层(三重解析 + 真实路径边界校验 + 非空校验)、交互层(策略决策与用户确认双通道)。对希望在自己的 Agent 产品中实现"计划/实施"两阶段流程的开发者而言,这套"策略总线决策 → 用户确认 → 路径白名单校验 → 审批模式状态机"的组合是一个值得参考的完整实现范本。
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 StartedRust0624
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