Plan模式的完整流程
摘要
Plan模式是当前主流AI编程助手(如Cursor、Claude Code、Cline等)中一种非常重要的工作机制:在真正动手修改代码之前,先让AI产出一份可审阅的执行方案,由人类确认或调整后再落地执行。本文将围绕 Plan 模式的完整生命周期——开启 → 补充信息 → 审阅方案 → 确认执行或调整计划——展开深入剖析,结合可运行的代码示例,手把手带你理解并实现一个最小可用的 Plan 模式工作流。无论你是想理解AI Agent的交互设计,还是想在自己的工具中落地类似机制,本文都能提供可复用的思路与代码。
引言
在"直接对话式改代码"的模式下,我们常常遇到这样的问题:
- AI 一上来就大改特改,改完才发现方向完全错了;
- 需求含糊时,AI 自行脑补,产出的代码与预期南辕北辙;
- 涉及多文件、多步骤的重构,AI 边想边改,中途逻辑断裂;
- 人类失去了对"改动范围"的把控,review 成本极高。
Plan 模式正是为了解决这些痛点而诞生的。它的核心思想是:把"思考"和"执行"分离。先产出一份结构化的计划(Plan),让人来审阅、补充、调整,确认无误后再进入执行阶段。这本质上是一种"人在回路(Human-in-the-loop)"的工程设计。
下面我们按完整流程逐步拆解。
一、开启 Plan 模式:从"直接执行"切换到"先规划"
1.1 为什么要有一个显式的"开启"动作
Plan 模式不应该默认开启,也不应该悄悄开启。它需要用户明确表达"先给我方案,别急着改"。原因有三:
- 成本考量:规划本身要消耗 token 和时间,简单任务不值得;
- 预期管理:用户需要知道当前处于"只读/规划"状态,避免误以为代码已被修改;
- 权限边界:Plan 阶段通常应禁用写文件、执行命令等有副作用的工具。
1.2 用状态机建模
一个清晰的实现方式是把整个流程建模为有限状态机:
fromenumimportEnum,autoclassPlanState(Enum):IDLE=auto()# 普通对话模式PLANNING=auto()# Plan 模式:正在生成/补充计划REVIEWING=auto()# 计划已生成,等待用户审阅EXECUTING=auto()# 用户已确认,正在执行DONE=auto()# 执行完成1.3 开启时的工具约束
在 Plan 状态下,必须对 Agent 可用的工具做白名单限制,只允许"只读"工具:
READ_ONLY_TOOLS={"read_file","list_dir","search_code","grep"}WRITE_TOOLS={"write_file","edit_file","run_command","delete_file"}defget_allowed_tools(state:PlanState)->set[str]:"""根据当前状态返回允许使用的工具集合"""ifstatein(PlanState.PLANNING,PlanState.REVIEWING):# 规划/审阅阶段:只读,禁止任何副作用操作returnREAD_ONLY_TOOLSifstate==PlanState.EXECUTING:returnREAD_ONLY_TOOLS|WRITE_TOOLSreturnREAD_ONLY_TOOLS|WRITE_TOOLS这样一来,即便模型"想"去改文件,工具层也会直接拦截,从机制上保证 Plan 阶段是安全的。
1.4 触发开启的几种常见方式
- 显式指令:用户输入"先给方案"、“plan first”、“别改代码,先说说思路”;
- 快捷键/开关:IDE 中的 Plan 模式按钮(如 Cursor 的 Plan Mode);
- 自动升级:当检测到任务涉及多文件或高风险操作时,Agent 主动建议进入 Plan 模式。
二、补充信息:让计划建立在充分上下文之上
2.1 为什么"补充信息"是独立的一步
一个高质量的计划,前提是信息充分。如果需求含糊、代码库上下文缺失,模型只能靠猜,产出的计划必然空洞。因此 Plan 模式在生成计划前(或生成初稿后),应主动向用户澄清关键信息。
这一步的产物通常包括两类:
- 向用户提问:范围、约束、优先级、验收标准;
- 主动检索代码库:读取相关文件、搜索调用链,把事实性上下文补齐。
2.2 结构化澄清:把"问题"变成数据
与其让模型自由发挥地问,不如定义一个结构化的澄清对象:
fromdataclassesimportdataclass,field@dataclassclassClarification:question:str# 要问用户的问题reason:str# 为什么需要这个信息options:list[str]=field(default_factory=list)# 可选项(可选)blocking:bool=True# 是否阻塞计划生成# 示例:模型在规划前提出的澄清项clarifications=[Clarification(question="这次重构是否需要保持对外 API 完全兼容?",reason="决定能否修改函数签名,影响改动范围",options=["必须兼容","可以破坏性变更"],blocking=True,),Clarification(question="是否需要同步补充单元测试?",reason="影响计划中的步骤数量与工作量",options=["需要","不需要"],blocking=False,),]2.3 主动检索:用只读工具补齐上下文
在等待用户回答的同时,Agent 可以并行地用只读工具收集事实:
defgather_context(agent,target_symbol:str)->dict:"""在 Plan 阶段收集与目标符号相关的上下文(只读)"""context={}# 1. 找到定义位置context["definition"]=agent.call_tool("search_code",{"query":f"def{target_symbol}"})# 2. 找到所有调用点,评估影响面context["callers"]=agent.call_tool("grep",{"pattern":f"{target_symbol}(","exclude":"tests/"})# 3. 读取相关测试文件context["tests"]=agent.call_tool("search_code",{"query":target_symbol,"path":"tests/"})returncontext2.4 补充信息的收敛判断
不能无限地问下去。一个实用的收敛策略是:
- 只对
blocking=True的问题强制等待回答; - 非阻塞问题给出默认假设,并在计划中显式标注"假设:……";
- 当关键上下文(定义、调用点、测试)都已获取,即可进入下一步。
三、审阅方案:把计划变成可读、可评估的产物
3.1 计划的推荐结构
一份好的计划不是一段散文,而是结构化、可逐步核对的清单。推荐包含以下字段:
fromdataclassesimportdataclass,field@dataclassclassPlanStep:id:inttitle:strdescription:strfiles:list[str]# 预计涉及的文件risk:str# low / medium / highdone_when:str# 验收标准(如何判断这步完成)@dataclassclassPlan:goal:str# 一句话目标assumptions:list[str]# 明确列出的假设steps:list[PlanStep]out_of_scope:list[str]# 明确不做什么,防止范围蔓延open_questions:list[str]# 仍需确认的问题3.2 生成计划的提示词骨架
PLAN_PROMPT="""你正在 Plan 模式,禁止修改任何文件。请基于以下上下文产出执行计划。 ## 目标 {goal} ## 已收集的上下文 {context} ## 已确认的信息 {confirmed} ## 输出要求 1. 用结构化步骤描述改动,每步标注涉及文件、风险等级、验收标准; 2. 明确列出你的假设(assumptions)与不做的事(out_of_scope); 3. 若仍有阻塞性问题,放入 open_questions,不要臆测。 """3.3 审阅时人类关注什么
审阅阶段,人类主要评估四件事,我们可以在 UI 上对应地高亮:
| 关注点 | 对应字段 | 典型问题 |
|---|---|---|
| 方向对不对 | goal | 这是我要解决的问题吗? |
| 范围合不合理 | steps[].files、out_of_scope | 改动面是否过大? |
| 风险可不可接受 | steps[].risk | 有没有破坏性变更? |
| 假设成不成立 | assumptions | 这些假设我认可吗? |
3.4 一个渲染示例
把 Plan 渲染成人类易读的 Markdown:
defrender_plan(plan:Plan)->str:lines=[f"## 目标\n{plan.goal}\n"]lines.append("## 假设")lines+=[f"-{a}"forainplan.assumptions]or["- (无)"]lines.append("\n## 执行步骤")forsinplan.steps:lines.append(f"{s.id}. **{s.title}**(风险:{s.risk})\n"f" - 涉及文件:{', '.join(s.files)}\n"f" - 完成标准:{s.done_when}")lines.append("\n## 不在本次范围")lines+=[f"-{x}"forxinplan.out_of_scope]or["- (无)"]ifplan.open_questions:lines.append("\n## 待确认问题")lines+=[f"-{q}"forqinplan.open_questions]return"\n".join(lines)输出的效果大致如下:
## 目标 将 UserService 中的同步查询改为异步,保持对外 API 兼容。 ## 假设 - 调用方均通过 await 调用,无同步调用残留 - 数据库驱动支持 async ## 执行步骤 1. **抽取数据访问层**(风险: medium) - 涉及文件: services/user_service.py, db/repo.py - 完成标准: 新 repo 通过现有单测 2. **改造 UserService 为 async**(风险: high) - 涉及文件: services/user_service.py - 完成标准: 所有方法标注 async 且调用链更新 ## 不在本次范围 - 不修改 HTTP 层四、确认执行:从计划到落地的受控切换
4.1 确认动作本身要有"契约"
用户的"确认"不应只是一个模糊的"好的",而应是一个明确的信号,最好带上是否允许偏离计划的授权范围:
@dataclassclassApproval:approved:boolallow_deviation:bool=False# 执行中是否允许偏离计划max_files_changed:int|None=None# 可选的硬性约束4.2 执行器按步骤推进
进入EXECUTING状态后,执行器应当逐步推进,并在每步完成后对照done_when自检:
defexecute_plan(agent,plan:Plan,approval:Approval)->dict:ifnotapproval.approved:raiseRuntimeError("计划未获批准,禁止执行")results=[]forstepinplan.steps:# 1. 执行前再次确认工具权限(此时已放开写权限)agent.set_allowed_tools(get_allowed_tools(PlanState.EXECUTING))# 2. 执行该步骤output=agent.run_step(step)# 3. 自检:对照完成标准check=agent.verify(step.done_when,output)results.append({"step":step.id,"title":step.title,"passed":check.passed,"detail":check.detail,})# 4. 若不允许偏离且自检失败,立即中止,回到审阅ifnotcheck.passedandnotapproval.allow_deviation:agent.set_state(PlanState.REVIEWING)return{"aborted_at":step.id,"results":results}agent.set_state(PlanState.DONE)return{"results":results}4.3 偏差检测:执行时守护"计划边界"
即使批准了执行,也应监控实际改动是否越界(比如改了计划外的文件):
defcheck_deviation(step:PlanStep,changed_files:list[str])->list[str]:"""返回计划外被修改的文件列表"""planned=set(step.files)return[fforfinchanged_filesiffnotinplanned]一旦发现越界,就应暂停并向用户报告,而不是默默改完——这正是 Plan 模式价值的关键体现。
五、调整计划:审阅不通过时的回退与迭代
5.1 调整不是"重新来一遍"
用户审阅后可能给出三类反馈:
- 微调:某步拆分/合并、调整顺序、改风险描述;
- 范围变更:增删步骤、修改
out_of_scope; - 方向否定:目标本身理解错了,需要重来。
前两类应当在保留已有上下文的基础上做增量修改,避免每次都从头生成。
5.2 用 diff 方式修改计划
defapply_feedback(plan:Plan,feedback:str)->Plan:"""根据用户反馈增量修改计划(示意)"""# 实际实现中可再次调用模型,仅要求输出变更部分delta_prompt=f"""现有计划:{render_plan(plan)}用户反馈:{feedback}请仅输出需要变更的步骤(新增/删除/修改),保持其余步骤不变。 """delta=call_model(delta_prompt)returnmerge_delta(plan,delta)# 将增量合并回原计划5.3 调整后的再确认
调整完成后,状态回到REVIEWING,再次请用户确认。整个流程形成一个可循环的闭环:
PLANNING ──► REVIEWING ──┬──► EXECUTING ──► DONE ▲ │ └──── 调整计划 ◄──────┘5.4 完整驱动循环示例
把前面各部分串起来,得到一个可运行的骨架:
defrun_plan_mode(agent,goal:str):# 1. 开启agent.set_state(PlanState.PLANNING)agent.set_allowed_tools(get_allowed_tools(PlanState.PLANNING))# 2. 补充信息context=gather_context(agent,target_symbol=extract_symbol(goal))clarifications=agent.propose_clarifications(goal,context)confirmed=ask_user(clarifications)# 阻塞式收集回答# 3. 生成并审阅方案plan=agent.generate_plan(goal,context,confirmed)agent.set_state(PlanState.REVIEWING)whileTrue:print(render_plan(plan))feedback=input("确认执行?(y/调整内容): ")iffeedback.strip().lower()=="y":approval=Approval(approved=True)break# 5. 调整计划plan=apply_feedback(plan,feedback)# 4. 确认执行agent.set_state(PlanState.EXECUTING)returnexecute_plan(agent,plan,approval)六、工程实践中的几个关键细节
6.1 计划要"可验证"
每个步骤都必须有done_when,否则执行阶段无法自检,也无法判断是否需要回退。模糊的"完成改造"是坏计划,"所有调用点编译通过且单测绿"才是好计划。
6.2 显式记录假设
把模型的隐含假设摆到台面上,是 Plan 模式最有价值的部分之一。用户在审阅时看到"假设:调用方无同步残留",往往能立刻发现模型理解是否有偏差。
6.3 权限与状态强绑定
安全边界必须由代码保证,而非依赖模型自觉。前面get_allowed_tools的设计就是这一原则的体现:Plan 阶段物理上无法写文件。
6.4 日志与可追溯
建议对每个状态迁移、每次计划版本都留痕,便于事后复盘"为什么走了这条路径":
importlogging logging.basicConfig(level=logging.INFO)deftransition(agent,from_state,to_state,reason=""):logging.info(f"[PlanMode]{from_state.name}->{to_state.name}|{reason}")agent.set_state(to_state)总结
Plan 模式的本质,是把 AI 的"思考"与"执行"解耦,通过人在回路的方式,在动手之前先对齐目标、范围与风险。本文完整梳理了它的四个核心环节:
- 开启:用状态机显式建模,并在规划阶段用只读工具白名单从机制上禁止副作用;
- 补充信息:通过结构化澄清 + 主动检索,让计划建立在充分上下文之上,并明确收敛条件;
- 审阅方案:把计划结构化为目标、假设、步骤、范围、待确认问题,让人能高效评估方向与风险;
- 确认执行或调整计划:以明确的审批契约进入执行,逐步推进并检测偏差,审阅不通过时以增量 diff 方式迭代,最终形成闭环。
一句话概括:好的 Plan 模式,让 AI 先"说清楚要做什么",再在人类的确认下"只做说好的事"。掌握了这套流程,你不仅能更好地使用现有的 AI 编程工具,也能在自己的 Agent 系统中落地一套安全、可控、可追溯的规划工作流。