WRITING / 2026.07.14

Rill Flow:用 DAG 组织分布式任务与 LLM 工作流

从图模型、调度状态到失败恢复,讨论为什么 DAG 仍然是组织分布式任务与 LLM 工作流的可靠抽象。

当一个任务从单次函数调用长成多步骤系统,真正困难的通常不再是“调用哪个模型”,而是:步骤如何依赖、状态如何流转、失败如何恢复,以及人怎样理解正在发生什么。

Rill Flow 是一个公开的分布式工作流编排项目。我参与开源贡献的过程中,再次感受到 DAG(有向无环图)这种经典抽象在 LLM 时代仍然有效:它不替我们消除复杂性,却能把复杂性放到可观察、可验证的位置。

为什么选择 DAG

DAG 把工作流拆成节点和边。节点表示可执行单元,边表示依赖关系。只要一个节点的前置依赖全部完成,它就具备运行条件。

这种表达有三个直接好处:

  1. 依赖显式化:不再把执行顺序藏在层层回调或条件分支里。
  2. 并行机会可计算:互不依赖的节点可以同时运行。
  3. 故障边界清晰:失败属于具体节点,恢复策略可以围绕节点状态设计。

一个最小工作流可以写成:

输入校验 ──→ 资料检索 ──→ 答案生成 ──→ 质量检查
                 └────────→ 引用整理 ───────┘

这张图同时是执行计划、可视化界面和排障地图。对于需要频繁变化的 AI 应用,它比一段不断增长的过程式代码更容易演进。

调度器真正管理的是状态

“把节点按顺序跑起来”只是调度的表面。一个可用的工作流引擎需要定义稳定的状态模型,例如:

状态 含义 常见后续动作
Pending 等待依赖满足 继续检查前置节点
Ready 已具备执行条件 投递到执行器
Running 正在执行 记录心跳和超时
Succeeded 执行成功 释放后继节点
Failed 执行失败 重试、降级或终止
Cancelled 被主动取消 清理相关资源

状态转换必须具备幂等性。网络抖动、进程重启和重复投递都可能让同一事件到达多次;如果一次“完成”通知能够重复释放后继节点,工作流就会产生难以复现的分叉。

因此,调度器需要围绕持久化状态做条件更新,而不是只相信内存中的执行顺序。可以把核心原则概括为:

事件可能重复,状态转换只能成功一次。

失败恢复不是简单重试

不同节点的失败语义并不相同。一次只读检索通常可以安全重试;一次向外部系统写入的动作,则需要幂等键、去重记录或补偿机制。

设计重试策略时,我通常会区分四个问题:

  • 失败是否可恢复,例如临时限流与参数错误不能用同一种策略处理;
  • 节点是否幂等,重复执行会不会造成二次影响;
  • 重试间隔如何退避,是否需要加入随机抖动;
  • 超过阈值以后,是终止全图、跳过节点,还是进入人工处理。

LLM 节点还会带来新的失败类型:输出格式不合法、上下文超限、工具调用缺少参数,或者结果通过了协议校验却没有达到质量要求。这意味着“成功”不能只看 HTTP 状态码,还需要结构校验和业务层评估。

LLM 工作流中的边界

模型适合处理语义不确定性,工作流引擎适合管理执行确定性。把两者混在一起,系统会很快失去可解释性。

一种更稳妥的分工是:

  • 模型负责分类、提取、生成和判断;
  • 节点协议约束输入输出结构;
  • DAG 管理依赖、并行、重试和取消;
  • 观测系统记录每个节点的耗时、结果摘要与错误原因;
  • 人工节点处理高风险或低置信度分支。

例如,模型可以决定“这条请求需要检索”,但检索节点的超时、最大重试次数和输出字段应该由工程系统定义,而不是交给提示词临时决定。

从能运行到可运营

工作流上线后,最有价值的界面往往不是编辑器,而是运行轨迹。一次执行应该能回答:

  1. 当前停在哪个节点?
  2. 为什么这个节点被执行?
  3. 它消费了什么输入,产生了什么类型的输出?
  4. 如果失败,系统采取了什么恢复动作?

这也是 “Trace” 对工作流系统的意义。轨迹不是附加日志,而是理解系统行为的第一等数据。只有当执行过程可以回放、比较和定位,工作流才从一个开发工具变成可以长期运营的基础设施。

小结

DAG 并不是所有任务的答案。极短的线性流程,用普通代码可能更直接;包含大量动态循环的智能体,也需要在图模型之外定义清晰的终止条件。但当系统开始出现并行依赖、失败恢复和跨节点状态时,DAG 提供了一种足够克制、又足够强大的共同语言。

LLM 改变了节点内部能做什么,却没有取消分布式系统的基本问题。依赖、状态、幂等、恢复与观测,仍然决定一个工作流能否稳定地走完。