第16章 当路线不止一条——Workflow & Graph

小A最近同时接触了三套东西:自己在写的 Agent 循环,同事负责的“审批流程”,还有一篇讲“图编排”的文章。他有点慌,端着咖啡去找老Z。
“我是不是得在这三种里选一个,选错了就完了?”他问。
“先别急。检索解决‘带什么资料’,不回答‘由谁决定下一步’。”老Z说,“你看到的三样东西,循环、工作流和图,不是等着你三选一的竞争对手,它们是控制权与状态的三种不同安排。搞清楚它们各管什么,你自然知道怎么用。”
本章比较 Loop、Workflow 与 Graph;不绑定某个框架,也不把图形表示误当成可靠性保证。
先把名字对齐
| 中文术语 | 英文全称或缩写 | 本书工作定义 | 不等于什么 |
|---|---|---|---|
| Agent 循环 | Agent Loop | 模型输出与工具结果驱动的受限迭代 | 无边界的自主执行 |
| 工作流/图 | Workflow/Graph | 固定步骤/节点边和路由表达 | 彼此严格互斥 |
| 状态机 | state machine | 状态、事件与转移条件的明确模型 | 只画流程图 |
| 检查点/重放/幂等 | checkpoint/replay/idempotency | 保存恢复点、再次执行、避免重复副作用的契约 | 自动事务保证 |
控制权的三种安排
“那它们到底谁决定下一步?”小A问。
“看这张表,问题就清楚了,关键在‘下一步由谁决定’:”
| 形式 | 下一步主要由谁决定 | 适合的边界 |
|---|---|---|
| Loop | 运行时根据模型输出与停止条件迭代 | 探索性任务,必须有预算和中止 |
| Workflow | 开发者预先规定步骤 | 审批、ETL、可预测的业务流程 |
| Graph | 节点与边规定可达路线,条件决定分支 | 多路径、可恢复的状态流程 |
“三种形式的停止条件呢,分别由谁负责?”小A追问。
“这一点最容易被忽略。”老Z说,“Loop 的停止条件必须由宿主显式给出:轮数、token、费用或超时,模型自己不负责刹停。Workflow 的结束是定义好的末节点,走到就停;Graph 的停止通常由显式终态、执行器规则或外部暂停条件决定;无出边只是其中一种静态结构,图也可以循环、暂停或等待外部事件。不管哪种形式,都得能回答‘什么时候停、停下来时留下什么状态’,否则谈不上恢复和审计。”
“那 Graph 是不是一定比 Workflow 高级?”小A问。
“不是。Graph 不必包含模型,Workflow 也可以包含分支。两者的区别在表达方式和状态管理,不是互斥的分类:Workflow 按顺序展开步骤,Graph 把分支显式画成节点和边。同一个业务逻辑两者都能表达,差别只在改一个分支时动哪里:Workflow 改步骤定义,Graph 改路由条件。”
“要是我一开始选错了形式,后面还能换吗?”
“能,但代价不同。”老Z说,“从 Workflow 改成 Graph,不只是把固定步骤重写成节点和转移;状态、错误、并发、检查点和恢复契约都要重新确认。从 Graph 改成 Loop,等于把路由权从代码交到模型手里,停止条件和护栏都得重新设计。换形式的工作量,主要就花在把路由权从一方挪到另一方。所以开工前先问:这个任务的路径到底有多固定?”
“路径固不固定,我看就是看分支能不能提前写全?”小A抢着说。
“对,就看能不能预先穷举分支。”老Z说,“‘失败就重试,缺资料就请求补充,完成就结束’,每种走向都写得清楚,路径就是固定的,用 Workflow 表达就够。分支数量大、条件本身要运行时才知道的,路径就不固定,这才轮到 Graph 或 Loop。判断的是分支可不可预知,不是任务复不复杂。”
“能不能拿几个常见任务对一对?”
“可以,用几个常见任务对照:”
| 任务 | 分支可预知? | 合适的形式 |
|---|---|---|
| 表单审批(通过/退回/拒绝) | 是 | Workflow |
| 发布流程(检查→审批→部署→验证) | 基本是 | Workflow / Graph |
| 客服工单(按类型分流,再按回复效果跟进) | 部分 | Graph + 受限 Loop |
| 新功能探索(方案生成→验证→迭代) | 否 | Loop + 人工确认 |
“照这个对照,同一个人可以同时用三种形式?”
“对。同一个团队的不同任务,往往就同时用着三种形式。”老Z说,“与其给每个任务贴一个‘形式标签’,不如把每个任务的路由权归谁记清楚。”
比喻:路线图
Workflow 是固定班次的地铁,Graph 是能换乘的线路图,Loop 是没固定路线的出租车。没有谁更好,只看你要去哪。
三种形式的工程特征
“除了‘谁决定下一步’,三者在工程上还有什么实质差别?”小A问。
“有,而且这些差别常常被‘形式选择’掩盖。”老Z说,“从可观测性、成本、失败恢复,到测试和调试,三者的差别摆在一起看得很清楚:”
| 维度 | Loop | Workflow | Graph |
|---|---|---|---|
| 可观测性 | 每轮事件可记录,但下一步不确定 | 步骤预先可知,进度一目了然 | 节点与边可见,当前在哪可查 |
| 成本 | 每次迭代都产生模型调用成本 | 固定步骤,成本可预估 | 分支越多,测试组合越多 |
| 失败恢复 | 从停止原因恢复,可能丢中间状态 | 从失败步骤重放,需幂等 | 从 checkpoint 恢复,路径可重建 |
| 测试 | 需要大量边界输入样本 | 每条路径单独测试 | 每对节点转移单独测试 |
| 调试 | 问题可能藏在模型决策里 | 问题定位到具体步骤 | 问题定位到具体节点 |
“光看测试那一行,三家要考的东西就不一样?”
“完全不一样。”老Z说,“Loop 可能在模型决策的任何一步出错,考题是停止条件和边界输入,要用样本覆盖‘该停的时候停没停’。Workflow 的考题是每条固定路径:输入 A 走 1、2、3,输入 B 走 1、2、4,路径走错了就是定义写错了。Graph 的考题是转移条件:同一个节点,条件满足该走哪条边,条件不满足又该走哪条。顺带一句,测试组合的数量本身就是选形式时要付的一笔隐性成本。”
“有没有先按一种形式做、后期再切换的常见路子?”
“有,而且很常见。”老Z说,“大多数系统从 Workflow 起步:路径固定、可测试、成本可控。等任务复杂度上去、分支穷举不动了,才把局部改成 Graph 或 Loop。切换的方向通常是从确定走向灵活,反过来很少见,因为确定性部分省下的成本和可审计性是宝贵资产,轻易不该丢。”
状态、路由与恢复
“不管画成什么形状,工程上要关心什么?”
“五个问题:当前状态是什么,谁能改它,分支条件从哪里来,失败后从哪里恢复,动作是否幂等。”老Z画了个简化图式:
接收请求 → 校验 → [资料充分?] ─否→ 请求补充 → 校验
│是
↓
执行任务 → 记录结果“为什么这五个问题是‘状态’的问题,不是‘图形’的问题?”
“因为图是给人看的,状态是给程序用的。”老Z说,“同一个流程画成什么样都行,但状态必须落在某个可持久化的地方,数据库、文件或内存对象都可以,否则进程一重启,流程就不知道自己走到哪了。判断一个编排方案是否可靠,不看图画得对不对,看这五个问题是不是每个都有落点。”
“五个问题分别对应什么工程部件?”
“一一对应。”老Z把五个问题摆成一张表:
| 状态问题 | 对应的工程部件 | 没实现时的表现 |
|---|---|---|
| 状态在哪 | 持久化存储(库/文件/内存) | 进程重启后流程丢失 |
| 谁能改 | 写入权限控制 | 多个进程并发改同一状态 |
| 条件怎么来 | 分支判断逻辑(规则/模型/外部输入) | 分支走错且无法追溯 |
| 从哪恢复 | checkpoint + 恢复入口 | 失败后只能重头再来 |
| 重跑是否幂等 | 幂等键/去重逻辑 | 重放后副作用重复 |
“这张表就是检查一个编排实现完不完整的清单。”老Z说,“五个问题各有一个答案,实现才算闭环。哪个问题答不上来,那个位置就是故障点:不是说那里可能出错,而是出错时你手里没有处理它的手段。”
“图里‘执行任务’那个节点要是挂了呢?”
“‘执行任务’若会写入外部系统,恢复时就不能简单重跑,要记幂等键,或者采用补偿动作。”老Z说,“图把路线画出来,不会自动解决重复写入、竞争更新或模型分类错误。举个具体的:一个节点做了‘创建订单’并写入外部系统,进程在写回本地 checkpoint 之前崩溃。重放时没有幂等键,就会创建第二张订单;有幂等键,外部系统能认出这是同一次调用。”
“补偿动作是什么?”
“补偿是把已发生的副作用‘还回去’的动作:创建了订单就发取消,发了邮件就撤回通知。但补偿不等于回滚,外部系统可能已经执行、可能执行了一半、也可能根本没收到,补偿只能尽量收敛,保证不了回到执行前的样子。所以设计顺序永远是先定义幂等,再谈补偿。”
“那是不是不确定性越高,就越得用 Loop?”
“不一定。可以把不确定的部分圈在一个节点里,外层仍用确定的工作流保住审批、权限和交付。”老Z说,“选哪种,看的是可预测性、失败代价与可观测性,不是谁更智能。失败代价极高的固定流程,哪怕里面有模型参与,也宁可把模型决策关在一个有预算的节点里,别让整条流程跟着自由发挥。”
“系统已经在跑了,想往灵活的方向演化,从哪儿动最安全?”
“从局部替换开始。”老Z说,“不推翻整个 Workflow,挑一个分支点,把它从固定判断换成受预算限制的模型判断。比如原来‘异常类型固定分三类’,改成‘先用模型判断异常类型,再走原分支’。一次只换一个点,其余结构不动,替换点要配上独立的预算、停止条件和回退路径。”
| 演化动作 | 风险 | 降险手段 |
|---|---|---|
| Workflow 的某步换成模型判断 | 模型判断不稳定 | 限定输入输出、给预算、设回退 |
| 固定分支改成 Graph 路由 | 路由条件测不全 | 先保留原分支作为 fallback |
| 某节点内部允许自由发挥 | 节点失控 | 限制轮数、强停止、人工确认兜底 |
| 整条流程换成 Loop | 失去可审计性 | 极少采用,一般只局部换 |
“反过来呢?模型判断太多,想收回来。”
“反向演化更简单。”老Z说,“给模型判断的输出先加一道规则校验,不合格就退回固定分支;等规则覆盖得足够充分,再彻底移除模型判断。从灵活退回确定,靠的是规则一步步取代模型,每一步都要有数据证明规则达到了原模型的判断质量,不能凭感觉砍。”
要点
编排的可靠性来自五个状态问题的答案,不来自流程图本身:状态在哪、谁能改、条件怎么来、从哪恢复、重跑是否幂等。
混合编排
“那我能把三种混着用吗?”
“可以,而且常见。”老Z说,“一种常见组合是确定的路由先选择任务类型,再把开放式分析交给有限轮次的 Loop;或者在图节点中调用检索、工具和模型。每一层都要有自己的输入输出 schema、超时、预算和审计事件。把一个大型 Loop 伪装成‘节点’而不暴露这些边界,只会让恢复与测试更困难。”
“听下来,混的是形式,边界一处都不能混?”
“对,看的就是有没有边界。”老Z说,“混合编排的每一段都有明确的输入输出、预算和停止条件;‘没有编排’是整条流程糊在一起,既没有段落也没有边界。判断标准很简单:能不能指着一段说‘这段最多跑 5 轮、这段有人工确认、这段走固定分支’。能,是混合编排;不能,是混乱。”
| 特征 | 混合编排 | 混乱编排 |
|---|---|---|
| 段落边界 | 每段有输入输出契约 | 各段职责模糊 |
| 预算 | 每段有独立轮数/token 限制 | 全程无预算 |
| 停止条件 | 每段有明确停止原因 | 依赖模型自觉 |
| 审计 | 每段有事件日志 | 只有最终结果 |
| 恢复 | 每段可从 checkpoint 恢复 | 断了就重头再来 |
“维护成本是不是比单一形式高?”
“高,但高在有回报的地方。”老Z说,“你要维护的不再是一种形式的逻辑,而是多段的契约、切换点和各自的恢复策略。代价真实存在,回报是每段都用最合适的形式表达、每段都能独立测试。要是哪一段的复杂度撑不起单独维护,就别为它单开一种形式。多数系统用一两种形式就够了,第三种是拿复杂度和成本换来的。”
“画个混合结构给我看看?”
“可以,给一个简化示意:”
入口路由(确定)─── 任务类型 A → 固定审批流程(Workflow)
└── 任务类型 B → 受限调查节点(Loop,最多 5 轮)
↓
图编排:按调查结果选择分支
↓
人工确认(必须)“这个结构里,每种形式各管一段。”老Z说,“入口路由和审批是确定的部分,负责授权与交付;调查节点是受限的 Loop,负责开放性分析;图在调查结果出来后选择下一步。每段边界都明确:Loop 有轮数上限,Workflow 有人工关卡,图有确定的分支条件。”
“为什么不干脆把整个任务都交给 Loop?”
“因为代价不同。”老Z说,“Loop 每多一轮,就多一次模型调用、多一点上下文成本、多一分行为上的不可预测。确定的部分用确定的形式表达,成本低、可审计;真正需要开放判断的部分才交给模型,预算才花在刀刃上。混合编排做的事,说穿了就是给不确定性画一个有限的盒子。”
“那这个盒子该画在哪?”
“画在失败代价会不会被放大的地方。”老Z说,“审批、付款、权限变更这类动作,失败代价高且不可逆,倾向放进确定的工作流,用人工关卡和审计兜底;开放式分析、信息收集、方案生成这类动作,失败代价低且可重试,才适合放进受限的 Loop。判断的准绳是这里的失败会造成多大损失、能不能重来,不是这里要不要用模型。”
“同一个任务里,确定部分和开放部分谁先谁后,有讲究吗?”
“有,而且通常长成固定的形状:外层确定、内层开放。”老Z说,“入口用确定路由分流,中间用受限 Loop 分析,出口再回到确定流程收尾,审批、落库都在这一段。倒过来、外层开放内层确定的很少见,那等于让模型决定整个流程的骨架,确定性部分反倒成了模型的执行细节。”
恢复点必须属于状态,而非图形
“为什么说恢复点属于状态,不属于图形?”
“因为恢复点必须能被程序读回来,画在图上不算数。”老Z说,“拿报销流程举例:‘生成草稿’可以安全重跑;‘提交报销’就必须先查幂等键,或者转入人工处理。Loop、Workflow 与 Graph 可以混合:确定的工作流负责授权和交付,图负责分支,受轮次限制的 Loop 只处理开放式分析。这样做牺牲少量灵活性,换来一个可恢复、可测试的控制面。”
“那一个节点的定义,到底要写全哪些东西?”
“可以按四件事检查。”老Z说,
节点的四份定义
- 输入:进入节点需要哪些数据,缺了会怎样
- 输出:节点成功时产出的结果,供下游消费
- 提交点:副作用落地的位置,提交前可重跑,提交后要幂等
- 幂等策略:重跑一次与跑一次的差别,靠幂等键、查询外部状态还是转人工
“最常见的错误,是把恢复点画在图里、却只落在脑子里。”老Z说,“图上有‘已提交’,程序里却没有对应的字段,进程一重启就等于没提交。恢复点要从可持久化的状态里读出来,这件事那张图替你做不了。”
把恢复设计在副作用之前
“这三种形式有没有一个统一的看法?”
“有,都能落到状态机上:状态、事件、转移条件,再加一个可持久化的 checkpoint。”老Z说,“Workflow 通常把转移写成顺序步骤;Graph 显式记录节点和边;Loop 把‘下一步’部分委托给模型,但轮次预算和停止状态一样不能少。差异在谁拥有路由权,不在能不能画成图。”
“状态机的状态和转移,具体怎么定义?”
“状态是‘流程现在在哪’,转移是‘发生什么事件、满足什么条件,才从这里走到那里’。”老Z说,“拿报销流程举例,一个最小状态机长这样:”
draft ──(提交)→ submitted ──(审批通过)→ approved ──(打款)→ paid
│ │
└──(退回)──→ draft └──(拒绝)→ rejected| 状态 | 含义 | 允许的转移 | 转移触发的事件 |
|---|---|---|---|
| draft | 草稿,可编辑 | → submitted | 用户提交 |
| submitted | 已提交,等待审批 | → draft / approved | 退回 / 审批通过 |
| approved | 已批准 | → paid / rejected | 打款 / 审批拒绝 |
| paid | 已打款,终态 | 无 | — |
| rejected | 已拒绝,终态 | 无 | — |
“定义状态机的价值,是让非法转移变得可见。”老Z说,“‘已打款’的工单不该能回到‘草稿’,状态机里没有这条转移,代码想让它回去都回不去。没有状态机的时候,这类非法状态靠开发者自觉避免,一旦漏掉,数据里就会留下‘已打款却还在审批’的矛盾记录。”
“那状态机和图,不就一回事吗?”
“不是一回事。”老Z说,“状态机强调‘状态与转移的合法性’,关心哪些转移是允许的;图强调‘节点与路径的表达力’,关心哪些路径是可达的。同一个业务,两种视角可以同时存在:用图表达路由结构,用状态机约束转移合法性。它们是互补的两个检查面,不是互相替代。”
“串个例子听听?”
“拿创建发布工单当贯穿场景。”老Z说,“先 checkpoint ‘验证完成’,再执行一次带幂等键的外部创建;重放时读取 checkpoint 和键,不能盲目再次创建。失败路径有三种:一是写入后进程崩溃,恢复要查询外部状态;二是模型路由错误,Graph 应送到人工或安全终点;三是 checkpoint 未写入,这个风险要和副作用顺序一并测试。混合编排可以把固定审批包在外层,把受限调查 Loop 放在节点内,但节点用了模型,不能就此放弃重放、审计和恢复契约。”
“这几种失败,各自怎么恢复?”
“按副作用是否已经落地来分。”老Z说,
| 失败位置 | 副作用状态 | 恢复方式 | 反例 |
|---|---|---|---|
| checkpoint 写入前 | 无副作用 | 重跑当前节点 | 若节点本身会写外部,仍需幂等 |
| checkpoint 写入后 | 副作用已落地 | 从 checkpoint 重放,靠幂等键去重 | 无幂等键 → 重复写入 |
| 外部系统已执行 | 不可见 | 查询外部状态、用幂等键、或转人工 | 盲目重发 → 重复订单 |
| 模型路由错误 | 走错分支 | 送到人工或安全终点 | 让模型自己纠正 → 可能再次走错 |
“这张表的反例列很关键。”老Z说,“每一种恢复方式都有一个想当然会出事的版本:以为重放安全就重放,以为重发没事就重发。恢复设计要先把‘会怎么出事’列出来,再决定恢复方式,不是先写恢复代码,再祈祷别出事。”
图的绘制与实现的落差
“我注意到一个问题:很多流程图画得很好看,代码却跟图对不上。”
“这是 Workflow 和 Graph 最容易踩的坑。”老Z说,“图是给人看的,实现是给机器跑的,两者之间没有自动的一致性保证。画图时,人会自然省略错误处理、超时、幂等这些细节,因为图表达的是主路径;实现不能省,每个节点都要处理失败,每条边都要有失败时的去向。”
“那图的作用到底是什么?”
“当沟通与设计的起点,不当验收的终点。”老Z说,“图的价值在于让参与讨论的人看到主路径长什么样、谁决定下一步、哪里有人工关卡。但图的正确性不代表实现的正确性,实现与图一致与否要靠测试核对,‘图是这么画的’证明不了任何事。”
| 图画层 | 实现层 | 常见落差 |
|---|---|---|
| 节点 | 函数/服务/子流程 | 节点里实际做的事比图上的多(隐藏副作用) |
| 边 | 函数调用/消息/路由 | 图上一条边,代码里可能有重试、降级、超时 |
| 分支条件 | if/switch/路由规则 | 图上的菱形,实现里可能有多层嵌套判断 |
| 终止 | 返回/退出/异常 | 图上没画的异常路径,实现里必须画 |
“落差能缩小吗?”
“两个手段。”老Z说,“一是在图上标注非主路径,用虚线或注解标出超时、失败、人工介入的走向,讨论时就能看到边界;二是用实现反向核对图,代码写完后把每个节点的实际行为回填到图上,看图上的承诺和代码的实现是否一致。落差消不掉,但可以显式化:摆在明面上的是已知边界,藏起来的迟早变成没人防的故障。”
“模型参与的节点,图能表达吗?”
“能表达‘这个节点会调用模型’,表达不了‘模型会怎么决策’。”老Z说,“模型节点在图上能画的是输入输出和停止条件,画不出决策内容。所以给模型节点做标注要特别小心:画的是边界,不是行为。边界是预算、停止条件、输入输出契约;行为是模型在边界内自己决定的,画不出来,也不该假装画得出来。”
常见编排反例
“有没有那种看起来合理、实际会翻车的编排设计?”
“有三类很典型。”老Z说,
三个编排反例
- 把整个任务塞进一个大 Loop,假装它是节点:外部看不到它的内部状态,预算、轮次、停止原因全被藏起来,恢复和审计无从谈起
- 图节点里直接做模型自由发挥,没有任何预算:图只保证了路由确定性,没保证节点内部不失控
- 把所有模型调用都当成可重放:模型调用有概率性,重放会得到不同结果,不能和确定性计算一样安全重跑
“这三个反例的共同点是什么?”
“都是把某一层的约束,误当成了整个系统的保证。”老Z说,“Loop 有停止条件,不等于整条流程安全;Graph 有路由,不等于节点内不失控;重放有幂等键,不等于所有节点都幂等。每一层只能保证自己那一层的边界,跨层的假设必须显式声明、显式验证。”
“做编排设计的时候,该从哪里开始想?”
“从最坏情况开始想。”老Z说,“先问‘这个流程最贵的一次失败是什么’,是重复扣款、发错通知,还是生成错了文档;然后从最贵的失败反推:它在哪一层发生、那一层靠什么拦住、如果拦不住会怎样。把最坏情况写下来,再回头选形式,形式只是这个思考的落点。”
“最后收个尾:怎么才算把编排想清楚了?”小A问。
“能同时回答三个问题就算。”老Z说,“每一步的路由权在谁手里,是代码还是模型;每个节点的预算和停止条件是什么,保证不会无限转;每次失败后从哪恢复、会不会重复副作用。三个问题都有答案,形式选哪种都站得住;缺一个,形式再对也会在某个角落塌。”
小结
Loop、Workflow 和 Graph 是三种表达控制权和状态的工具,它们不互斥,按“路径有多固定”来选择。Agent Loop 把控制权交给模型,让它在每一步自主决定下一步,适合开放性的、路径无法预先确定的问题。Workflow 把路径固定下来,每一步做什么、按什么顺序、满足什么条件才往下走,都写在定义里,适合出错代价高、需要可审计可回放的流程。Graph 介于两者之间,用节点和边表达可能的分支,既允许条件路由,又保留整体结构的可观测性。三种形式在工程特征上差别明显:可观测性、成本、失败恢复、测试组合都不同,选择形式时这些隐性成本往往比“形式名字”更重要。
检查点和重放是 Workflow 与 Graph 的配套能力:保存恢复点让流程可以中断后续接,重放要求每一步都幂等,否则中断重跑会重复扣款。恢复方式按副作用是否已落地分四类:checkpoint 前重跑、checkpoint 后靠幂等键去重、外部已执行时查外部状态或转人工、模型走错路时送人工或安全终点。三种形式都可落到状态机,也就是状态、事件、转移条件和可持久化 checkpoint。状态机的价值在于让非法转移变得可见,图和状态机是互补的两个检查面,不是替代关系。
混合编排是常态:入口用确定路由分流,中间用受限 Loop 分析,出口回到确定流程收尾,每个开放节点都有预算和停止条件。常见的三个反例是:把整个任务塞进大 Loop 假装它是节点、图节点里做无预算的模型自由发挥、把所有模型调用当成可安全重放。它们的共同点是把某一层的约束误当成整个系统的保证:每一层只能保证自己那一层的边界,跨层的假设要显式声明和验证。
动手核验
- 将一个报销流程画成节点、状态和条件,给每个外部写入标出幂等策略。
- 把其中一个开放式“解释异常”节点改为最多三轮的 Loop,并定义停止原因。
- 注入一次超时和一次重复投递,验证流程能否恢复且不会重复提交。
