Plan-and-Execute 里的 DAG 是怎么工作的

文章目录

    • [一、什么是 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 拓扑排序思想的具体实现:在任何一个时刻,系统中都会有一批"就绪任务",它们要么没有依赖,要么依赖已经满足。

这种机制带来了两个关键好处:

  1. 并行执行:所有就绪任务可以同时启动。上面的例子中,步骤2和步骤3在步骤1完成后可以并发执行,而不需要一个等另一个。这就是现代智能体高效处理多步骤任务的基础。
  2. 有序推进:复杂的任务永远不会出现"先跑了下游任务却发现上游还没完成"的混乱情况。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,所以解析分两步走:

  1. 第一遍 :遍历 tasks 数组,创建所有 Task 节点,同时做 ID 映射(防止 LLM 用奇怪字符串当 ID,内部统一转成 task_1task_2)。
  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 易于局部修改的特性,它还让失败重试和动态重规划变得异常清晰。

相关推荐
ch.ju1 小时前
Java Programming Chapter 4——The difference between overloading and overwriting.
java·开发语言
我命由我123451 小时前
Android 开发问题:View 的 getWidth、getHeight 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
满怀冰雪1 小时前
第12篇-二分答案法-当答案不好求时如何反向搜索
java·算法
我登哥MVP1 小时前
SpringCloud 核心组件解析:服务网关
java·spring boot·后端·spring·spring cloud·java-ee·maven
lulu12165440781 小时前
OpenAI 如何用开源前端生态为 GPT-5.6 铺路? - 微元算力(weytoken)
java·前端·人工智能·python·gpt·开源·ai编程
searchforAI1 小时前
坚持用AI做笔记,我的知识留存与学习速度大幅提升
人工智能·笔记·学习·ai·知识图谱·知识管理
北城以北88882 小时前
RocketMQ简介
java·spring boot·后端·rocketmq
折哥的程序人生 · 物流技术专研10 小时前
Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
java·网络·数据库·算法·面试
GoGeekBaird10 小时前
从 Prompt Engineering 到 Loop Engineering,我觉得 AI 开发这事儿终于开始变味了
后端·github