给Agent CLI加上Plan-and-Execute,让Agent先规划后执行,支持DAG。
大家好,我是二哥呀。
第 1 期的 ReAct 已经能干活了,读文件、改代码、跑命令,一步接一步。
可任务一长,比如“读 pom.xml、看看源码结构、再写一份升级方案”,问题就来了。我们在 Agent 动手之前不知道它打算怎么做,互不相干的几步也没法同时跑。中途哪一步错了,只能等它自己发现。

这一期我们给 PaiCLI 加上 Plan-and-Execute。模型先出一份带依赖关系的任务清单,我们确认之后,PaiCLI 按依赖分批执行这些任务,互不依赖的任务还可以并行。
01、先规划再执行
Plan-and-Execute 把一次任务交给两个角色。规划器(Planner)负责出计划,它调用模型时不给任何工具,只要求模型输出一份 JSON 格式的计划。执行器(PlanExecuteAgent)拿着这份计划,按依赖关系一批一批地跑任务。正常情况下规划器只调用一次模型;用户审阅时补充要求,或者任务失败后需要重新规划,规划器会再调用模型。
“Plan 模式”在不同产品里做法不一样。Pi 是一款开源的终端编程 Agent,它的 Plan 模式只做只读调研,通过
setActiveTools把 edit 和 write 两个写文件工具拿掉,bash 也只放行只读命令。PaiCLI 的做法不同,只有规划器调用模型时不带工具,执行器执行每个任务时,读写文件、执行命令这些工具都照常可用。两种做法的对比可以阅读《Plan模式死了吗?》

每个任务内部,跑的还是第 1 期 ReAct。模型决定调用什么工具,工具结果交回模型,直到模型不再调用工具,这个任务就算完成。
在 Agent 开始动手之前,我们能看到完整的计划,可以确认、补充要求或者直接取消。读 pom.xml 和列出 src 目录这种没有先后关系的任务,可以同时跑。每个任务拿到的上下文也更干净,只有总目标、任务描述和直接依赖的结果。
两种模式放在一起比一下。
| 对比项 | ReAct | Plan-and-Execute |
|---|---|---|
| 模型调用次数 | 由 Action 轮数决定 | 多一次规划,每个任务至少一次 |
| 耗时 | 取决于 Action 轮数 | 多一次规划等待,独立任务并行时可能更快 |
| 执行前能否看到步骤 | 不能 | 能,还能补充要求后修改计划 |
| 适合的任务 | 简单任务、边看边决定的探索 | 步骤多、依赖明确、想先看清流程的任务 |
02、计划长什么样
一个任务就是一个节点。
// src/main/java/com/paicli/plan/Task.java
public class Task {
private final String id;
private final String description;
private final TaskType type;
private volatile TaskStatus status;
private volatile String result;
private volatile String error;
private final List dependencies; // 依赖的其他任务ID
private final List dependents; // 依赖此任务的其他任务ID
}
任务类型有 5 种,FILE_READ、FILE_WRITE、COMMAND、ANALYSIS、VERIFICATION。执行任务时,任务类型会填进发给模型的提示词里,提醒模型这一步是读任务、写任务还是分析任务。类型不限制模型能用哪些工具。
dependencies 记录这个任务依赖哪些任务,用来判断它能不能开始执行;dependents 记录哪些任务依赖它,这个任务失败时,顺着 dependents 就能找出所有受影响的任务。下文把依赖某个任务的任务叫作它的“下游”。

任务状态有 5 种。
PENDING → RUNNING → COMPLETED / FAILED
PENDING → SKIPPED(依赖的任务失败,或者重新规划次数达到上限,见第 07 节)
能不能开始执行,只看一个条件,所有依赖是否都已经完成。
public boolean isExecutable(Map allTasks) {
if (status != TaskStatus.PENDING) return false;
for (String depId : dependencies) {
Task dep = allTasks.get(depId);
if (dep == null || dep.getStatus() != TaskStatus.COMPLETED) {
return false;
}
}
return true;
}
依赖失败或者被跳过,下游会等不到 COMPLETED,所以失败处理必须主动把下游标成 SKIPPED,不然会一直停在 PENDING。
并行执行时有两类线程。调度线程是运行执行器主循环的那个线程,任务的状态和结果都由它写,任务开始前标记为 RUNNING,收回结果后标记为完成或失败。工作线程是线程池里真正执行任务的线程,它们只读取依赖任务的结果。调度线程写完再把任务提交给线程池,工作线程执行完,调度线程通过 Future.get() 取回结果。Java 保证提交之前的写入对工作线程可见,工作线程的写入在 Future.get() 返回后对调度线程可见,所以不会读到旧值,字段上的 volatile 算多一层保险。
拓扑排序和环检测
多个任务组成一个执行计划(ExecutionPlan),任务按插入顺序存在 LinkedHashMap 里,另外维护一份拓扑排序后的顺序。拓扑排序是把有依赖关系的任务排成一个序列,保证每个任务都排在它依赖的所有任务后面。
PaiCLI 的拓扑排序用 DFS(深度优先搜索)后序遍历实现。“后序”的意思是,先递归处理完一个任务的所有依赖,再把这个任务本身加进结果,所以得到的顺序天然就是依赖在前。
// src/main/java/com/paicli/plan/ExecutionPlan.java
private boolean topologicalSort(Task task, Set visited, Set visiting) {
String id = task.getId();
if (visiting.contains(id)) {
return false; // 有环
}
if (visited.contains(id)) {
return true;
}
visiting.add(id);
for (String depId : task.getDependencies()) {
Task dep = tasks.get(depId);
... 未能定位到原文的划线
企业级Agent工作流编排项目PaiFlow
Vibe Coding版本的PaiAgent
派聪明RAG AI知识库Java版本+Go版本
微服务 PmHub、技术派、MYDB
求职派JobClaw(OpenClaw/Hermes架构
PaiCLI(类似Claude Code的Agent
派简历(代码已完成)
等实战项目。
1. 微信扫右侧的优惠券加入知识星球
2. 解锁星球的实战项目教程和源码: 项目源码+教程获取
暂无评论
回复