Agent Scope Java 2.x 系列【33】Harness:计划模式(Plan Mode)
1. 标准流程
计划模式(Plan Mode)的核心是 将 Agent 所有复杂任务统一拆分为「规划阶段 + 人工审批 + 执行阶段」,并非仅限代码开发,适用于所有多步骤、高风险、长周期的智能体任务。
1.1 规划阶段(PLAN 模式)
全程仅允许信息调研、梳理逻辑、制定方案,禁止一切新增、修改、删除等变更操作,从根源避免边想边做、盲目执行。
标准流程:
- 进入计划模式,通过只读工具全面调研现状、收集信息、梳理任务细节;
- 完整梳理可落地的任务方案,固化为
PLAN.md计划文档; - 提交方案,等待人工审核确认。
1.2 执行阶段(BUILD 执行模式)
仅在人工审批通过后解锁全部操作权限,有序落地任务,保障过程可控、可追溯。
标准流程:
- 审核通过后,退出只读规划状态,解除操作限制;
- 将整体方案拆解为精细化、可分步执行的待办任务;
- 按清单稳步推进执行,全程贴合既定方案,避免任务跑偏、出错。
2. 相关工具
2.1 三大核心工具
| 工具名 | 参数 | 作用 |
|---|---|---|
| plan_enter | 无 | 正式切入规划只读阶段 |
| plan_write | content | 将方案写入PLAN.md,专用写入API,规避通用文件写入带来的安全漏洞 |
| plan_exit | rationale(可选) | 结束规划,弹出HITL人工审批,审批通过才解除工具限制 |
如果把通用文件写入工具 write_file 开放给规划阶段,模型可以自定义文件路径,能向服务器里任意文件写入内容,存在越权篡改、覆盖配置文件的安全漏洞。一旦模型失控,会破坏系统文件。
专门定制只用于规划模式的写入 plan_write 工具,只能写入固定文件 plans/PLAN.md,规划阶段模型只能修改计划文档,不能操作其他任何文件,彻底杜绝任意文件写入的风险。
示例,修改项目接口代码任务中,会依次调用:
- 调用
plan_enter:进入只读规划模式,不能改文件,只能查看代码。 - 多次调用
plan_write,生成PLAN.md,全程只写入这一个计划文件,碰不到业务代码。 - 调用
plan_exit弹出人工确认(HITL)- 同意:退出规划,进入执行
BUILD阶段,放开文件编辑权限。 - 驳回:继续留在规划模式,只能修改
PLAN.md,啥代码都改不了。
- 同意:退出规划,进入执行
2.2 shell 工具
命令行工具 execute(shell) 具备双重能力:
- 只读查询:
ls、cat、grep、git log,只查看代码,不修改数据; - 变更写入:
rm、重定向写入、提交代码、安装依赖,会改动文件。
框架仅依靠工具名称做权限控制,没办法识别你本次执行命令是查询还是修改。为严格保证「规划阶段只能调研、不能修改代码」,默认情况下,计划阶段默认直接禁用 shell,守住只读底线。
写靠谱的执行计划,往往需要遍历目录、查看源码、查阅 git 历史。shell 是最高效的代码调研手段。为了提升规划质量,可以手动开启开关:
.allowShellInPlanMode()
开启后的权限规则:
execute允许在规划阶段调用,模型可以执行shell命令来勘察项目;- 系统会增加提示词,强制约束模型仅使用只读指令(
ls/cat/grep/git查询类),禁止执行删改、写入类变更命令; - 文件写入类工具
write_file、edit_file依旧保持封禁。文件编辑入口全程锁住,只放开命令行查询通道。
安全取舍与风险:
- 安全性相比默认配置有所下降:提示词约束不可靠,模型依然有可能拼出
shell写入语句篡改文件; - 最佳配套方案:必须搭配沙箱文件系统,把
Agent运行环境隔离在沙箱内。就算出现违规写操作,破坏范围也被限制在沙箱内部,不会污染主机环境。
这套权限逻辑对齐
OpenCode规划Agent:开放shell用于代码调研 + 彻底禁用文件编辑工具 + 依靠提示词约束shell为只读操作。
2.3 todo_write 工具
todo_write 工具是 Agent Scope Core 包提供的工具,对标 OpenCode 的 todowrite ,用于在执行阶段中:
- 全量覆盖式更新结构化待办列表;
- 强约束:同一时刻只能有一条
in_progress(当前正在执行)任务,不允许同时两条任务都在进行中,保证串行干活,不能多开; - 待办清单存入
Agent状态,每一轮推理自动提醒模型。
示例,在规划阶段生成的 PLAN.md 文件内容:
整体计划:
1. 打开Controller源码
2. 修改入参校验逻辑
3. 补充单元测试
4. 运行测试验证结果
用户同意进入执行阶段时,模型执行调用 todo_write,一次性把整条计划拆成结构化清单(全量覆盖):
{
"todos": [
{"content":"打开Controller源码","status":"in_progress"},
{"content":"修改入参校验逻辑","status":"pending"},
{"content":"补充单元测试","status":"pending"},
{"content":"运行测试验证结果","status":"pending"}
]
}
会先执行【打开Controller源码】任务,打开文件操作后,再次调用 todo_write(必须把整张列表完整重写),把第一条改成 completed,第二条变成进行中:
{
"todos": [
{"content":"打开Controller源码","status":"completed"},
{"content":"修改入参校验逻辑","status":"in_progress"},
{"content":"补充单元测试","status":"pending"},
{"content":"运行测试验证结果","status":"pending"}
]
}
继续往下推进,直到所有任务全部 completed。
3. 计划模式四种终态
是否开启计划模式由模型自主决策。仅通过 isPlanModeActive() == false 无法区分结果,极易造成指标统计歧义。必须同时观测:工具调用记录 + 计划文件是否存在 + 当前模式状态,才能区分 4 种运行终态。
| 终态分类 | plan_enter调用 | PLAN.md文件 | PlanMode状态 | 结果说明 |
|---|---|---|---|---|
| 从未进入规划 | 否 | 不存在 | 关闭 | 直接执行,未走规划流程 |
| 规划中(已存文档) | 是 | 存在 | 开启 | 写完计划,等待审批放行 |
| 规划中(无文档) | 是 | 不存在 | 开启 | 无效规划,只构思不存档 |
| 正常退出闭环 | 是 | 存在 | 关闭 | 规划流程完整执行成功 |
3.1 从未进入计划模式
模型自主跳过整个规划环节,直接进入 BUILD 执行模式开始干活。
这是合法行为,常见诱因:
- 任务步骤简单,无需撰写正式方案;
- 当前代码工作区与任务目标不匹配,模型放弃编写计划;
- 简短问答类任务,规划无实际意义。
判定条件(三者同时成立):
- 无任何
plan_enter工具调用事件; - 会话状态:
isPlanModeActive() = false; - 文件系统:不存在
plans/PLAN.md。
该状态属于“跳过规划直接执行”,不能算作规划流程执行成功,需要和走完规划再退出的场景分开统计。
3.2 进入计划模式
进入 Plan Mode 存在两条独立入口,权限约束一致,但日志观测方式完全不同。
3.2.1 方式1:模型自主调用工具
模型自主调用工具:
plan_enter
执行后,PlanModeManager 打开规划模式。
- 触发时机:模型判断任务复杂,需要先写方案再动手;
- 运行效果:会话切换至只读规划环境,所有文件修改、破坏性
Shell命令全部被中间件拦截,仅允许执行plan_enter / plan_write / plan_exit三个工具。 - 可观测证据:会捕获到
ToolCallStartEvent事件,可以在代码里埋点记录本次会话开启过规划模式。 - 使用场景:正常人机对话,由
LLM自主判断任务复杂度来决定是否开启规划。
3.2.2 方式2:外部代码强制开启
适用场景:自动化评测、批量基准测试、管理后台手动管控会话,强制任务必须进入规划阶段。
(1)Java 原生API
RuntimeContext ctx = RuntimeContext.builder().sessionId("my-session").build();
agent.enterPlanMode(ctx);
- 等价效果:等同于模型调用
plan_enter; - 关键区别:不会产生任何工具调用事件,无法通过工具事件捕获。
(2)Admin 远程HTTP接口
引入 agentscope-admin-spring-boot-starter 后,可通过 HTTP 远程切换会话状态:
POST /v1/admin/sessions/{sessionId}:enter-plan-mode
(3)观测手段
只能依靠状态查询:
agent.isPlanModeActive(ctx);
3.3 停留在计划模式(未退出)
进入Plan Mode之后,任务可能中途终止,形成两类截然不同的未退出终态。
3.3.1 子终态1:已起草计划,文件已落地
判定条件:
- 已执行
plan_enter进入规划; - 调用过
plan_write; isPlanModeActive() = true;plans/PLAN.md文件真实存在。
业务含义:
- 整体方案已经撰写完成并保存到文档,只是没有调用
plan_exit提交审批。 - 会话仍然锁定在只读规划阶段,无法执行代码修改。
后续在同一会话发送消息,即可人工批准进入执行阶段。
3.3.2 子终态2:仅有构思,没有写出计划文件(无效规划)
判定条件:
- 成功进入
Plan Mode; isPlanModeActive() = true;- 从未调用 plan_write;
- 磁盘不存在
PLAN.md。
业务含义:
- 模型只在对话文本里口头构思方案,始终没有把计划落盘到文件。
- 属于不合格的规划行为:“只思考、不存档”,没有产出正式计划文档。
优化方案:
- 增强系统提示词,强制要求必须调用
plan_write保存方案; - 校验规则:进入规划模式后必须产出
PLAN.md,否则判定为流程不达标; - 更换与任务匹配的代码仓库,减少模型敷衍行为。
3.4 退出计划模式
退出同样分为模型主动申请、程序强制关闭两条路径,二者在审批流程上有本质区别。
3.4.1 方式1:模型调用 plan_exit
模型调用工具 plan_exit,附带可选的计划摘要。
工具内置权限拦截,一定会触发 HITL 人工确认弹窗:
- 用户审批通过:解除只读限制,退出
Plan Mode,切换到BUILD执行模式; - 用户驳回:继续停留在规划模式,只能修改
PLAN.md,不能执行改动操作。
完整成功链路:plan_enter → 多次 plan_write 迭代方案 → plan_exit + 人工审批放行。
3.4.2 方式2:代码程序化强制退出
(1)Java API
agent.exitPlanMode(ctx);
(2)Admin HTTP接口
- 核心特性:直接绕过HITL人工审批,无弹窗、无需人工确认;
- 适用场景:自动化测试、后台批量任务、管理端一键放行会话;
- 状态变化:
isPlanModeActive()直接变为 false。
POST /v1/admin/sessions/{sessionId}:exit-plan-mode
3.4.3 完整闭环成功的严格判定条件
只有同时满足下面全部条件,才能认定本次规划流程完整执行成功:
- 会话成功进入规划模式(有
enter调用或状态为true); - 调用过
plan_write,磁盘存在plans/PLAN.md; - 最终
isPlanModeActive() = false; - 流程走完:规划起草 → 保存文档 → 审批退出。
3.5 代码埋点实现方案
- 订阅
ToolCallStartEvent,记录会话内是否触发plan_enter、plan_write; - 任务结束后检查文件路径,判断
PLAN.md是否生成; - 读取
PlanMode激活状态; - 按照上表四分支做枚举分类,禁止仅依靠布尔开关判断成功与否。
4. 状态持久化
4.1 运行时状态持久化机制
Plan Mode 开关属于会话运行时状态,保存在 AgentState 内部。
- 只要
Agent状态开启自动落盘,规划模式激活状态会跟着一起持久保存; - 会话中断、应用进程重启、服务节点切换、多副本负载均衡恢复会话上下文时,会完整还原上一次的
Plan模式状态:- 中断时处于
PLAN模式,恢复后依然保持只读规划环境; - 中断时已经退出到
BUILD执行模式,恢复后依旧保持可写入权限。
- 中断时处于
4.2 两部分数据分开存储
4.2.1 模式开关(内存状态)
- 存储载体:
AgentState→ 由AgentStateStore统一持久化; - 保存内容:
isPlanModeActive布尔标记; - 恢复效果:会话断点续跑时,阶段不会错乱。
4.2.2 计划文档(磁盘文件)
plan_write 生成的 PLAN.md 写入工作目录 plans/,不受会话状态存储约束,完全跟随文件存储策略:
- 本地部署:直接保存在本地磁盘目录;
- 沙箱环境:文件隔离在沙箱文件系统内;
- 分布式集群:可对接远端对象存储、KV数据库,文件在多节点之间共享读取。
4.3 分布式断点续跑能力
- 会话上下文(规划模式开关)存入状态存储,多实例之间可以无缝迁移;
- 计划文档独立存入共享文件存储;
- 任意节点拉起同一会话,既能还原“处于规划中/已退出规划”的运行阶段,又能读取到已经写好的
PLAN.md; - 不会出现“模式恢复了,但是计划文件丢失”或者“文件还在,环境权限已经放开”的数据不一致问题。
4.4 对四种终态判断的影响
- 进程重启不会丢失
PlanMode状态标记,断点恢复后依然可以准确区分:未进入规划、处于规划中、已退出; - 只要文件存储配置一致,
PLAN.md是否存在这一判断条件在会话恢复后依然有效; - 批量评测、分布式任务场景下,前后两次读取到的终态结论保持一致,不会因为服务重启导致统计结果失真。
4.5 关键总结
- 阶段开关(
Plan是否激活)跟着AgentState自动持久化,支持会话断点恢复; - 计划文件独立存储在
plans/,跟随文件存储方案实现多节点共享; - 状态+文件两套数据同时可恢复,保证跨实例、跨重启之后,规划阶段上下文完整不丢失。
5. 与子 Agent 的关系
5.1 状态隔离基本原则
Plan Mode是单个Agent会话独立状态。
父Agent开启规划只读环境,该限制不会自动向下传递。- 父子
Agent拥有完全独立的AgentState、独立的规划开关,互不干扰。 - 主线
todo_write待办清单,与子Agent的task_list / task_output后台任务队列分属两套体系,数据完全隔离。
5.2 当前版本存在的缺口(重点)
- 父
Agent正处于Plan Mode(只读模式),此时调用agent_spawn拉起子Agent:
子Agent不会自动继承父级的只读权限限制。
-即便父进程被禁止修改文件,子Agent依然拥有完整的文件写入、命令执行权限,存在权限逃逸风险。
5.3 两种临时解决方案(手动管控子Agent权限)
方案一:限制子Agent工具白名单
在创建子Agent时,主动过滤工具列表,只保留只读类工具:
- 只开放:
read_file、grep、只读shell查询命令; - 直接剔除所有修改类工具:
write_file、edit_file、文件覆盖类能力。保证子Agent全程只能查看代码,不能产生任何变更。
方案二:给子Agent也开启规划模式
构建子 Agent 时开启规划能力:
builder.enablePlanMode()
- 再由代码主动调用程序化接口,让子
Agent自行进入Plan只读环境。 - 子
Agent被强制锁定在规划阶段,和父Agent保持一致的只读约束。
5.4 版本演进规划
后续新版本会实现权限自动传播机制:父会话一旦进入 Plan Mode,所有通过 agent_spawn 派生出来的子Agent,会自动继承只读策略,不需要人工额外配置。
5.5 补充边界说明
Plan Mode 只管控文件修改权限,父子Agent的任务调度互不影响;- 父在规划阶段只能写
PLAN.md,但子不受管控是当前版本的安全漏洞,必须手动加固; - 子
Agent独立的PlanMode不会反向影响父Agent的执行状态。
更多推荐

所有评论(0)