第 17 章 · DAG 工作流

一份卡在半路的旺季备货报告

那个卖家要做一份旺季备货决策报告。他一开始让编排系统“从头到尾一步步来”:先拉过去两年销量、再拉当前库存、再查供应商交期、再算补货量、再画趋势图、最后排版成稿。跑是能跑,但慢得让人抓狂——明明“拉销量”和“查供应商交期”是两件互不相干的事,系统却傻等着前一件做完才做后一件。

他心一横,改成“全部一起并行”。这下更糟:负责“算补货量”的 Agent 在销量数据还没拉回来时就开工了,算出一堆基于空数据的废话;“排版成稿”的 Agent 在正文和图表都没好的时候就开始排,排了个空壳。

问题的根子在于:这些任务之间有依赖关系(dependency)——有的能同时做,有的必须等别人做完才能做。全串行浪费时间,全并行破坏依赖。他需要一种办法,精确描述“谁依赖谁”,让能并行的并行、该等待的等待。这种办法,就是 DAG 工作流

DAG 是什么:节点、边、无环

DAG 是 Directed Acyclic Graph(有向无环图) 的缩写,拆开看正好是三个关键概念:

  • 节点(node):一个个任务,或负责该任务的 Agent。“拉销量”“查交期”“算补货量”各是一个节点。
  • 有向边(directed edge):表示依赖关系。一条从 A 指向 B 的边(A → B)意思是“B 依赖 A 的输出,B 必须等 A 完成才能开始”。方向不能反,所以叫“有向”。
  • 无环(acyclic):整张图里不能存在循环依赖。如果 A 等 B、B 又等 A,谁也没法先开始,就死锁了。DAG 用“无环”这条硬性约束从根上排除死锁。

把旺季备货报告画成 DAG,依赖关系一目了然:

拉销量 ─┐
        ├─→ 算补货量 ─┐
查库存 ─┘             ├─→ 排版成稿
拉销量 ───→ 画趋势图 ─┘
查交期 ───→ 算补货量
设计模板 ─────────────→ 排版成稿

“拉销量”“查库存”“查交期”“设计模板”没有任何前置依赖,可以一起并行起步;“算补货量”要等销量、库存、交期都到齐;“排版成稿”要等补货量、趋势图、模板全好。

DAG 的调度逻辑:既并行又保序

有了这张图,调度就变成一套清晰的机械流程,不用人操心先后:

  1. 找出当前所有“依赖已满足”的节点(一开始就是那些没有入边的节点),全部并行执行
  2. 某个节点完成后,检查它的下游节点:如果某个下游的所有前置依赖都满足了,就解锁它、投入执行。
  3. 重复第 2 步,直到所有节点完成。

这就是工作流引擎(workflow engine)的核心逻辑。它的价值在于同时拿到两个好处:一方面榨干了并行的效率(能同时跑的绝不排队),另一方面严格保证了依赖顺序(该等的一定等到)。旺季备货报告用 DAG 跑,四个数据源并行拉、算补货量自动等数据齐、排版自动等三样都好——又快又不出错。

回头看第 16 章的三种基础结构:Pipeline(流水线)就是一条直线的 DAG,Parallel(并行)就是一批没有相互依赖的节点。DAG 是它俩的一般化——能表达任意复杂的依赖,而不只是“一条线”或“一起上”。

用 pi 实现:声明依赖,调度交给编排层

pi 的 orchestrator(实验性,第 16 章提醒过)可以表达这种带依赖的编排。它的思路是声明式的——你只描述任务和它们的依赖,剩下的并行与等待交给编排层去算:

// 概念示意:声明任务与依赖,编排层自动并行 + 等待
const workflow = orchestrator.createWorkflow({
  pullSales:      { agent: salesAgent },
  checkStock:     { agent: stockAgent },
  checkLeadTime:  { agent: supplierAgent },
  designTemplate: { agent: templateAgent },
  calcRestock:    { agent: restockAgent, dependsOn: ["pullSales", "checkStock", "checkLeadTime"] },
  makeCharts:     { agent: chartAgent,   dependsOn: ["pullSales"] },
  layout:         { agent: layoutAgent,  dependsOn: ["calcRestock", "makeCharts", "designTemplate"] },
});

const report = await workflow.run();
// 前四个无依赖,并行起步;
// calcRestock 等三个数据源齐了才跑,makeCharts 等销量就能跑;
// layout 等补货量、图表、模板都好才排版。

dependsOn 声明的就是那些有向边,编排层据此构建 DAG 并调度。你只描述依赖关系,完全不用手写“先做什么、后做什么、谁等谁”的调度逻辑——这正是声明式的好处:把“要什么”和“怎么调度”分开,你负责前者,引擎负责后者。

静态 DAG vs 动态 DAG

DAG 什么时候定下来,分两种:

  • 静态 DAG(static):像上面这样,整张依赖图在运行前就固定好了。适合流程稳定、可预测的任务——定期报表、ETL 数据管道、CI 流水线。它清晰、可控、好调试,出了问题你对着图就能定位。
  • 动态 DAG(dynamic):Agent 在运行过程中根据情况动态生成子任务和依赖。比如调研时发现某家竞品又出了新品,临时加一个“调研新品”的节点挂进图里。它更灵活,能应对事先想不全的情况,但也更难预测、更难调试——图长什么样得跑起来才知道。

一条生产经验:能用静态 DAG 就别用动态。 生产环境里“可预测性”的价值极高——你能提前知道要跑哪些任务、大概花多少钱、哪一步可能卡住。动态 DAG 那种“跑起来才知道”的不确定性,在生产里往往是负担。这也呼应了后面第 24 章要讲的主题:可靠、可预测的编排。

动手看看

拿旺季备货报告(或你手头任何一个多步任务),把它画成 DAG:先列出所有任务节点,再逐对问“这个必须等那个吗”,是就画一条有向边。画完检查一遍有没有环——如果发现 A 等 B、B 又绕回来等 A,说明你的任务拆分有问题,得重新拆。

画完你会发现一个规律:图里“最长的那条依赖链”决定了整个流程最快能多快完成(其余分支都能藏在这条链的时间里并行跑完)。这条最长链就是所谓的“关键路径”——想加速整个流程,优化它才有用,优化旁支是白费力气。

实战中的几个坑

坑一:不小心画出了环,直接死锁。

  • 现象:工作流跑不起来,或某几个节点永远处于等待。
  • 原因:任务之间存在循环依赖(A 等 B、B 等 A),违反了“无环”。
  • 对策:画图时逐条边检查方向;发现环就重新拆分任务,打破循环。

坑二:依赖声明漏了,下游拿到空数据。

  • 现象:某个 Agent 在前置数据还没好时就开工,算出一堆废话。
  • 原因:dependsOn 漏写了某个前置节点。
  • 对策:对每个节点问清“它真正需要哪些输入”,把所有来源都声明进 dependsOn

坑三:无脑追求动态 DAG,调试无从下手。

  • 现象:图跑起来才成形,出问题定位不到是哪个环节、为什么。
  • 原因:本可静态的流程,却用了动态生成。
  • 对策:能静态就静态;只在流程确实无法预排时(如开放式探索)才用动态。

坑四:一个节点失败,整张图连锁崩溃。

  • 现象:某个 Agent 报错,导致所有下游全部失败,前面并行跑完的成果也白费。
  • 原因:没给节点设失败处理和重试。
  • 对策:给节点加重试/降级;把已完成节点的结果持久化,失败时能从断点续跑而非从头再来。

对比:其他框架

就“用依赖图(DAG)描述任务、让能并行的并行、该等待的等待”这件事:

框架DAG 怎么做特点
LangGraph本身就是“图”框架,节点 + 边天然是 DAG,能并行无依赖分支、汇聚等待多前置对 DAG 支持最直接,还支持条件边/循环
CrewAI用 Process 编排,任务的 context 隐含依赖非显式 DAG + 自动并行,复杂依赖表达不如图框架
PydanticAI无内建 DAG/工作流引擎,依赖与并行靠 asyncio.gather 等普通 Python 自己编排胜在每步强类型,但 DAG 得手写
AgnoWorkflow 抽象,可组合串行/并行步骤偏“常见流程结构”,任意依赖 DAG 非重点
piorchestrator 用 dependsOn 声明边、构建 DAG 调度(实验性)重型生产级则引入 Airflow/Temporal/Prefect(第 24 章)

底层都是“声明依赖、由调度器决定并行与等待”,差别在于框架是把 DAG 当核心抽象(LangGraph),还是让你用步骤(Agno / CrewAI)或普通代码(PydanticAI,需自己拼)去逼近它。

小结

  1. DAG = 节点(任务)+ 有向边(依赖)+ 无环,精确描述“谁依赖谁”;无环这条约束从根上排除死锁。
  2. 调度逻辑:依赖满足的节点并行执行,完成后解锁下游——同时拿到并行的效率和保序的正确。
  3. 第 16 章的 Pipeline 和 Parallel 都是 DAG 的特例,DAG 是它们的一般化。
  4. 静态 DAG vs 动态 DAG:能用静态就别用动态,生产环境看重可预测性。
  5. pi 用 orchestrator 的 dependsOn 声明式构建 DAG(实验性),重型场景则接专门的工作流引擎。

DAG 是高度结构化的编排——一切都得提前排好图。下一章看一种截然相反的思路:不预排、去中心、让 Agent 临场决定把活儿交给谁的 Swarm 模式