当一个任务从单次函数调用长成多步骤系统,真正困难的通常不再是“调用哪个模型”,而是:步骤如何依赖、状态如何流转、失败如何恢复,以及人怎样理解正在发生什么。
Rill Flow 是一个公开的分布式工作流编排项目。我参与开源贡献的过程中,再次感受到 DAG(有向无环图)这种经典抽象在 LLM 时代仍然有效:它不替我们消除复杂性,却能把复杂性放到可观察、可验证的位置。
为什么选择 DAG
DAG 把工作流拆成节点和边。节点表示可执行单元,边表示依赖关系。只要一个节点的前置依赖全部完成,它就具备运行条件。
这种表达有三个直接好处:
- 依赖显式化:不再把执行顺序藏在层层回调或条件分支里。
- 并行机会可计算:互不依赖的节点可以同时运行。
- 故障边界清晰:失败属于具体节点,恢复策略可以围绕节点状态设计。
一个最小工作流可以写成:
输入校验 ──→ 资料检索 ──→ 答案生成 ──→ 质量检查
└────────→ 引用整理 ───────┘
这张图同时是执行计划、可视化界面和排障地图。对于需要频繁变化的 AI 应用,它比一段不断增长的过程式代码更容易演进。
调度器真正管理的是状态
“把节点按顺序跑起来”只是调度的表面。一个可用的工作流引擎需要定义稳定的状态模型,例如:
| 状态 | 含义 | 常见后续动作 |
|---|---|---|
| Pending | 等待依赖满足 | 继续检查前置节点 |
| Ready | 已具备执行条件 | 投递到执行器 |
| Running | 正在执行 | 记录心跳和超时 |
| Succeeded | 执行成功 | 释放后继节点 |
| Failed | 执行失败 | 重试、降级或终止 |
| Cancelled | 被主动取消 | 清理相关资源 |
状态转换必须具备幂等性。网络抖动、进程重启和重复投递都可能让同一事件到达多次;如果一次“完成”通知能够重复释放后继节点,工作流就会产生难以复现的分叉。
因此,调度器需要围绕持久化状态做条件更新,而不是只相信内存中的执行顺序。可以把核心原则概括为:
事件可能重复,状态转换只能成功一次。
失败恢复不是简单重试
不同节点的失败语义并不相同。一次只读检索通常可以安全重试;一次向外部系统写入的动作,则需要幂等键、去重记录或补偿机制。
设计重试策略时,我通常会区分四个问题:
- 失败是否可恢复,例如临时限流与参数错误不能用同一种策略处理;
- 节点是否幂等,重复执行会不会造成二次影响;
- 重试间隔如何退避,是否需要加入随机抖动;
- 超过阈值以后,是终止全图、跳过节点,还是进入人工处理。
LLM 节点还会带来新的失败类型:输出格式不合法、上下文超限、工具调用缺少参数,或者结果通过了协议校验却没有达到质量要求。这意味着“成功”不能只看 HTTP 状态码,还需要结构校验和业务层评估。
LLM 工作流中的边界
模型适合处理语义不确定性,工作流引擎适合管理执行确定性。把两者混在一起,系统会很快失去可解释性。
一种更稳妥的分工是:
- 模型负责分类、提取、生成和判断;
- 节点协议约束输入输出结构;
- DAG 管理依赖、并行、重试和取消;
- 观测系统记录每个节点的耗时、结果摘要与错误原因;
- 人工节点处理高风险或低置信度分支。
例如,模型可以决定“这条请求需要检索”,但检索节点的超时、最大重试次数和输出字段应该由工程系统定义,而不是交给提示词临时决定。
从能运行到可运营
工作流上线后,最有价值的界面往往不是编辑器,而是运行轨迹。一次执行应该能回答:
- 当前停在哪个节点?
- 为什么这个节点被执行?
- 它消费了什么输入,产生了什么类型的输出?
- 如果失败,系统采取了什么恢复动作?
这也是 “Trace” 对工作流系统的意义。轨迹不是附加日志,而是理解系统行为的第一等数据。只有当执行过程可以回放、比较和定位,工作流才从一个开发工具变成可以长期运营的基础设施。
小结
DAG 并不是所有任务的答案。极短的线性流程,用普通代码可能更直接;包含大量动态循环的智能体,也需要在图模型之外定义清晰的终止条件。但当系统开始出现并行依赖、失败恢复和跨节点状态时,DAG 提供了一种足够克制、又足够强大的共同语言。
LLM 改变了节点内部能做什么,却没有取消分布式系统的基本问题。依赖、状态、幂等、恢复与观测,仍然决定一个工作流能否稳定地走完。