文章目录
-
- [一、什么是 DAG?](#一、什么是 DAG?)
- [二、Plan-and-Execute 架构中的 DAG 从何而来?](#二、Plan-and-Execute 架构中的 DAG 从何而来?)
- [三、DAG 如何驱动任务执行?](#三、DAG 如何驱动任务执行?)
-
- 1、任务节点:Task是最小执行单元
- [2、计划容器:ExecutionPlan 维护整张图](#2、计划容器:ExecutionPlan 维护整张图)
- [3、Planner:让 LLM 把自然语言变成 DAG](#3、Planner:让 LLM 把自然语言变成 DAG)
- [四、为什么 Plan-and-Execute 选了 DAG 而不是一条直线?](#四、为什么 Plan-and-Execute 选了 DAG 而不是一条直线?)
- 总结
在 ReAct 模式里,Agent 是"走一步看一步"的------每轮 LLM 调用决定下一个动作,没有全局蓝图。当任务变复杂(例如"分析项目结构,找出性能瓶颈,给出优化方案并验证"),这种局部决策容易陷入反复试探。
而在 Plan-and-Execute模式里,先让 LLM 把目标拆解成一张任务依赖图(DAG),再按拓扑顺序调度执行,能并行的地方绝不串行。这篇文章就拆解这套 DAG 从生成到落地的完整生命周期。
一、什么是 DAG?
DAG,全称 Directed Acyclic Graph(有向无环图),本质上是一种特殊的数据结构。它由两部分组成:
- 顶点(vertex):代表一个个的实体,比如任务、交易、数据节点;
- 有向边(directed edge):代表实体之间的单向关系,比如依赖、验证、执行顺序。
之所以叫"无环",是因为沿着这些有向边永远无法走回起点,也就是说图里不存在闭环。这个性质让 DAG 天然适合用来表达具有依赖关系的流程------你只能沿着一个方向推进,不会出现循环等待。
在区块链和分布式系统中,DAG 常用于表示交易的验证关系,比如每一笔新交易需要验证前两笔未被验证的交易。而在 Plan-and-Execute 智能体架构中,DAG 扮演的角色更加"日常":它负责把一个大任务拆解成小步骤,并且清晰地管理步骤之间的依赖顺序。
二、Plan-and-Execute 架构中的 DAG 从何而来?
Plan-and-Execute,顾名思义,是一种"先规划,后执行"的智能体工作模式。它有三个核心角色:
- 规划器(Planner):把用户的复杂任务拆解成一个有序的子任务列表,并指定每个子任务依赖哪些前面的结果;
- 执行器(Executor):按照依赖关系,逐步(或者并行)执行这些子任务;
- 重规划器(Replanner):如果某个子任务失败或者遇到意外情况,动态调整后续计划。
注意,规划器给出的子任务列表并不是一个简单的线性清单,而是一个带有依赖声明的结构化计划。举个例子,假设我们要分析市场并制作一份竞争报告,规划可能会是这样:
步骤1: 收集竞争对手列表 [web_search]
步骤2: 分析产品特点 [web_scrape] (依赖步骤1)
步骤3: 收集市场份额 [data_query] (依赖步骤1)
步骤4: 生成报告 [document_generate] (依赖步骤2, 3)
这里每一步都明确写了自己需要前面哪些步骤的输出。把这些步骤看作顶点,依赖关系看作有向边,我们立刻就能画出一个 DAG:
步骤1 → 步骤2
步骤1 → 步骤3
步骤2 → 步骤4
步骤3 → 步骤4
这个图没有环,方向清晰,Plan-and-Execute 中的 DAG 精确地指出每条执行的分支和汇合点。
三、DAG 如何驱动任务执行?
有了DAG,执行器就知道该如何行动了。它的核心逻辑就像是一个聪明的调度器,不断扫描当前所有可以执行的任务,一个任务能开始的前提是:它的所有前置依赖都已经成功完成。
1、任务节点:Task是最小执行单元
java
private final List<String> dependencies; // 我依赖谁
private final List<String> dependents; // 谁依赖我
dependencies 是前置条件。只有所有依赖都进入 COMPLETED 状态,该任务才变成可执行:
java
public boolean isExecutable(Map<String, Task> 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,这个任务的状态才会从 PENDING 变为 RUNNING。这正是 DAG 拓扑排序思想的具体实现:在任何一个时刻,系统中都会有一批"就绪任务",它们要么没有依赖,要么依赖已经满足。
这种机制带来了两个关键好处:
- 并行执行:所有就绪任务可以同时启动。上面的例子中,步骤2和步骤3在步骤1完成后可以并发执行,而不需要一个等另一个。这就是现代智能体高效处理多步骤任务的基础。
- 有序推进:复杂的任务永远不会出现"先跑了下游任务却发现上游还没完成"的混乱情况。DAG 的无环性质保证了执行流程的确定性。
2、计划容器:ExecutionPlan 维护整张图
ExecutionPlan 是 DAG 的载体。它用 LinkedHashMap<String, Task> 保存所有节点,并在添加任务时双向建立依赖:
java
public void addTask(Task task) {
tasks.put(task.getId(), task);
for (String depId : task.getDependencies()) {
Task dep = tasks.get(depId);
if (dep != null) {
dep.addDependent(task.getId()); // 反向索引
}
}
}
拓扑排序:DFS 检测环 + 定序
执行前必须确定合法顺序。用经典 DFS 拓扑排序:
java
private boolean topologicalSort(Task task, Set<String> visited, Set<String> visiting) {
if (visiting.contains(id)) return false; // 发现回边,有环
if (visited.contains(id)) return true;
visiting.add(id);
for (String depId : task.getDependencies()) {
if (!topologicalSort(dep, visited, visiting)) return false;
}
visiting.remove(id);
visited.add(id);
executionOrder.add(id); // 后序加入,自然形成拓扑序
return true;
}
如果 LLM 生成的计划中不小心形成了循环依赖(A 依赖 B,B 依赖 A),这里会直接失败,Planner 会抛出异常并要求重新生成。
3、Planner:让 LLM 把自然语言变成 DAG
Planner负责"画图"。它向 LLM 发送专用的 PLANNER Prompt,要求返回 JSON:
json
{
"summary": "...",
"tasks": [
{ "id": "1", "description": "读取 pom.xml", "type": "FILE_READ", "dependencies": [] },
{ "id": "2", "description": "分析依赖版本", "type": "ANALYSIS", "dependencies": ["1"] }
]
}
两遍解析处理前向引用
JSON 中的 dependencies 可能引用后面才出现的任务 ID,所以解析分两步走:
- 第一遍 :遍历
tasks数组,创建所有Task节点,同时做 ID 映射(防止 LLM 用奇怪字符串当 ID,内部统一转成task_1、task_2)。 - 第二遍 :再次遍历,根据
dependencies建立真正的依赖和反向依赖关系。
java
// 第一遍:创建节点
for (JsonNode taskNode : tasksNode) {
String newId = "task_" + taskIndex++;
plan.addTask(new Task(newId, description, type));
}
// 第二遍:连线
for (JsonNode taskNode : tasksNode) {
Task task = plan.getTask(newId);
for (JsonNode depNode : depsNode) {
task.addDependency(newDepId);
dep.addDependent(task.getId());
}
}
四、为什么 Plan-and-Execute 选了 DAG 而不是一条直线?
直白的线性列表(Step 1 → Step 2 → Step 3...)当然也能描述执行顺序,但它无法表达"多个任务可以同时开始"的并行可能性,也无法清晰地管理复杂的依赖关系。一旦任务多了,线性列表很快会变得臃肿且低效。
DAG 的精髓就在于用图的结构显式地暴露并行度。执行器只要遵守"依赖满足即可执行"的原则,就能自动实现最大程度的并发,这正是 Plan-and-Execute 架构高效、可扩展的关键所在。
总结
Plan-and-Execute 里的 DAG,不是高高在上的理论概念,而是一个实实在在的"任务路线图"。它用顶点表示子任务,用有向边表示依赖,形成一张无环的网,驱动着系统自动、并行、有序地完成复杂工作。而且得益于 DAG 易于局部修改的特性,它还让失败重试和动态重规划变得异常清晰。