Skip to content

第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 也可以包含分支。”老z说,“这里的区别是表达方式与状态管理,不是严格互斥的分类。Workflow 是顺序展开的步骤,Graph 把分支显式画成节点和边;同一个业务逻辑,两者都能表达,只是改一个分支时,Workflow 改步骤定义、Graph 改路由条件。”

“那我一开始选错形式,后面能换吗?”小a问。

“能,但代价不同。”老z说,“从 Workflow 改成 Graph,通常只要把固定步骤重写成节点和转移;从 Graph 改成 Loop,等于把路由权从代码交给模型——这意味着要重新设计停止条件和护栏。**换形式的成本,本质是换‘路由权在谁手里’的成本。**所以开工前先问:这个任务的路径到底有多固定?”

“那怎么判断一个任务的路径有多固定?”小a追问。

“看能不能预先穷举分支。”老z说,“如果任务的每种可能走向都能写清楚——‘失败就重试,缺资料就请求补充,完成就结束’——路径就是固定的,用 Workflow 表达;如果分支数量大、且条件本身要运行时才知道,路径就不固定,才轮到 Graph 或 Loop。判断标准不是任务复不复杂,而是分支可不可预知。”

“能不能给个对照?”

“可以,用几个常见任务对照:”

任务分支可预知?合适的形式
表单审批(通过/退回/拒绝)是Workflow
发布流程(检查→审批→部署→验证)基本是Workflow / Graph
客服工单(按类型分流,再按回复效果跟进)部分Graph + 受限 Loop
新功能探索(方案生成→验证→迭代)否Loop + 人工确认

“这个对照说明,同一个人可以同时用三种形式?”小a问。

“对。同一个团队的不同任务,通常就用着三种形式。”老z说,“关键不是给每个任务贴一个‘形式标签’,而是清楚每个任务的路由权在谁手里——这是比形式名字更本质的判断。”

比喻:路线图

Workflow 是固定班次的地铁,Graph 是能换乘的线路图,Loop 是没固定路线的出租车——没有谁更好,只看你要去哪。

三种形式的工程特征 ​

“除了‘谁决定下一步’,三者在工程上还有什么实质差别?”小a问。

“有,而且这些差别常常被‘形式选择’掩盖。”老z说,“从可观测性、成本、失败恢复三个角度看,三者差别很大:”

维度LoopWorkflowGraph
可观测性每轮事件可记录,但下一步不确定步骤预先可知,进度一目了然节点与边可见,当前在哪可查
成本每次迭代都产生模型调用成本固定步骤,成本可预估分支越多,测试组合越多
失败恢复从停止原因恢复,可能丢中间状态从失败步骤重放,需幂等从 checkpoint 恢复,路径可重建
测试需要大量边界输入样本每条路径单独测试每对节点转移单独测试
调试问题可能藏在模型决策里问题定位到具体步骤问题定位到具体节点

“那这三者的测试重点也不一样?”小a追问。

“完全不一样。”老z说,“Loop 的测试重点是停止条件和边界输入——它可能在模型决策的任何一步出错,你要用样本覆盖‘该停的时候停没停’;Workflow 的测试重点是每条固定路径——输入 A 走 1→2→3,输入 B 走 1→2→4,路径错了就是定义错了;Graph 的测试重点是转移条件——同一个节点,条件满足该走哪条边,条件不满足又该走哪条。测试组合的数量,也是选择形式的一个隐性成本。”

“那有没有‘先按一种形式做,后期再切换’的常见路径?”

“有,而且很常见。”老z说,“大多数系统从 Workflow 起步,因为路径固定、可测试、成本可控;当任务复杂度上升、分支难以穷举时,才把局部改成 Graph 或 Loop。切换的方向通常是‘确定性 → 灵活性’,而不是反过来——因为确定性部分省下的成本和可审计性是宝贵资产,轻易不该丢掉。”

状态、路由与恢复 ​

“那不管画成什么,工程上要关心什么?”小a问。

“无论画成什么形状,工程上需要回答同样的问题:当前状态是什么、谁能改它、分支条件如何得到、失败后从哪里恢复、动作是否幂等。”老z画了个简化图式:

text
接收请求 → 校验 → [资料充分?] ─否→ 请求补充 → 校验
                       │是
                       ↓
                    执行任务 → 记录结果

“这五个问题,为什么是‘状态’的问题而不是‘图形’的问题?”小a问。

“因为图是给人看的,状态是给程序用的。”老z说,“同一个流程,画成什么样都可以,但状态必须落在某个可持久化的地方——数据库、文件或内存对象——否则进程一重启,流程就不知道自己走到哪了。判断一个编排方案是否可靠,不是看它的图画得对不对,而是看五个问题的答案是否都有落点:状态存哪、谁有权改、条件由谁计算、从哪个点恢复、重跑会不会重复生效。”

“那这五个问题,分别对应什么工程部件?”小a追问。

“一一对应。”老z说,

状态问题对应的工程部件没实现时的表现
状态在哪持久化存储(库/文件/内存)进程重启后流程丢失
谁能改写入权限控制多个进程并发改同一状态
条件怎么来分支判断逻辑(规则/模型/外部输入)分支走错且无法追溯
从哪恢复checkpoint + 恢复入口失败后只能重头再来
重跑是否幂等幂等键/去重逻辑重放后副作用重复

“这套对应关系,是检查一个编排实现是否完整的清单。”老z说,“五个问题各有一个答案,实现才算闭环。任何一个问题答不上来,那个位置就是故障点——不是‘可能出错’,而是‘出错时你没有处理它的手段’。”

“那‘执行任务’挂了怎么办?”小a问。

“若‘执行任务’会写入外部系统,恢复时不能简单重跑;应记录幂等键或采用补偿动作。”老z说,“图把路线画出来不会自动解决重复写入、竞争更新或模型分类错误。示意: 一个节点做了‘创建订单’并写入外部系统,但进程在写回本地的 checkpoint 之前崩溃。重放时若没有幂等键,就会创建第二张订单;有幂等键,外部系统能认出这是同一次调用。”

“那补偿动作是什么?”小a问。

“补偿是把已发生的副作用‘还回去’的动作——创建了订单就发取消,发了邮件就撤回通知。补偿并不等于回滚:外部系统可能已经执行、可能执行了一半、也可能根本没收到,补偿只能尽量收敛,不能保证回到执行前的样子。所以设计顺序永远是:先定义幂等,再谈补偿。”

“是不是不确定性越高,就一定要用 Loop?”小a问。

“也不一定。可把不确定部分限制在一个节点中,外层仍用确定的工作流保护审批、权限和交付。”老z说,“选择依据是可预测性、失败代价与可观测性,而不是‘越智能越好’。一个失败代价极高的固定流程,即使里面有模型参与,也宁可把模型决策关在一个有预算的节点里,而不是让整条流程都变成自由发挥。”

“那一个已经跑起来的系统,想往更灵活的方向演化,怎么动最安全?”小a追问。

“从‘局部替换’开始。”老z说,“不推翻整个 Workflow,而是挑一个分支点,把它从固定判断换成受预算限制的模型判断——比如原来‘异常类型固定分三类’,改成‘先用模型判断异常类型,再走原分支’。一次只换一个点,其余结构保持不动,替换点要有独立的预算、停止条件和回退路径。”

演化动作风险降险手段
Workflow 的某步换成模型判断模型判断不稳定限定输入输出、给预算、设回退
固定分支改成 Graph 路由路由条件测不全先保留原分支作为 fallback
某节点内部允许自由发挥节点失控限制轮数、强停止、人工确认兜底
整条流程换成 Loop失去可审计性极少采用,一般只局部换

“那反过来,模型判断太多想收回来呢?”

“反向演化更简单。”老z说,“把模型判断的结果先经过一道规则校验,不合格就退回固定分支;等规则足够覆盖时,再彻底移除模型判断。从灵活到确定,是靠‘规则逐步取代模型’完成的——每步都要有数据证明规则覆盖了原模型的判断质量,而不是凭感觉砍掉模型。”

要点

编排的可靠性来自五个状态问题的答案,不来自流程图本身:状态在哪、谁能改、条件怎么来、从哪恢复、重跑是否幂等。

混合编排 ​

“那我可以混着用吗?”小a问。

“可以,而且常见。常见的组合是确定的路由先选择任务类型,再把开放式分析交给有限轮次的 Loop;或在图节点中调用检索、工具和模型。”老z说,“每层都应有自己的输入输出 schema、超时、预算和审计事件。把一个大型 Loop 伪装成‘节点’而不暴露这些边界,只会让恢复与测试更困难。”

“那混合编排和‘没有编排’怎么区分?”小a追问。

“看有没有边界。”老z说,“混合编排的每一段都有明确的输入输出、预算和停止条件——它混的是形式,不混的是边界;‘没有编排’是整条流程糊在一起,既没有段落也没有边界。**判断标准很简单:能不能指着一段说‘这段最多跑 5 轮、这段有人工确认、这段走固定分支’。**能,是混合编排;不能,是混乱。”

特征混合编排混乱编排
段落边界每段有输入输出契约各段职责模糊
预算每段有独立轮数/token 限制全程无预算
停止条件每段有明确停止原因依赖模型自觉
审计每段有事件日志只有最终结果
恢复每段可从 checkpoint 恢复断了就重头再来

“那混合编排的维护成本,是不是比单一形式高?”

“高,但高在有回报的地方。”老z说,“你要维护的不再是一种形式的逻辑,而是多段的契约、切换点和各自的恢复策略。代价是真实存在的;回报是每段都用最合适的形式表达,且每段都能独立测试。如果一段的复杂度不值得单独维护,就不要为它单开一种形式——多数系统只要一两种形式就够了,第三种形式是用复杂度和成本换来的。”

“能给我一个看得见的混合结构吗?”小a问。

“可以,以下是一个简化示意:”

text
入口路由(确定)─── 任务类型 A → 固定审批流程(Workflow)
              └── 任务类型 B → 受限调查节点(Loop,最多 5 轮)
                                    ↓
                              图编排:按调查结果选择分支
                                    ↓
                              人工确认(必须)

“这个结构里,每种形式各管一段。”老z说,“入口路由和审批是确定的部分,负责授权与交付;调查节点是受限的 Loop,负责开放性分析;图负责在调查结果出来后选择下一步。每一段的边界都明确:Loop 有轮数上限,Workflow 有人工关卡,图有确定的分支条件。”

“那为什么不能把整个任务都交给 Loop?”

“因为代价不同。”老z说,“Loop 每多一轮,就多一次模型调用、多一点上下文成本、多一点行为不可预测性。把确定的部分用确定的形式表达,成本低、可审计;只把真正需要开放判断的部分交给模型,才是把预算花在刀刃上。混合编排的本质,是给‘不确定性’画一个有限的盒子。”

“那混合编排的边界,应该画在哪?”小a追问。

“画在‘失败代价会不会被放大’的地方。”老z说,“审批、付款、权限变更这类动作,失败代价高且不可逆,倾向放进确定的工作流,用人工关卡和审计兜底;开放式分析、信息收集、方案生成这类动作,失败代价低且可重试,才适合放进受限的 Loop。判断的准绳不是‘这里要不要用模型’,而是‘这里的失败会造成多大损失、能不能重来’。”

“那同一个任务里,确定部分和开放部分的先后顺序有讲究吗?”

“有,而且通常有固定的形状。”老z说,“常见的形状是外层确定、内层开放:入口用确定路由分流,中间用受限 Loop 分析,出口再回到确定流程收尾(审批、落库)。倒过来——外层开放、内层确定——很少见,因为那等于让模型决定整个流程的骨架,确定性部分反而成了模型的执行细节。”

恢复点必须属于状态,而非图形 ​

“为什么说恢复点属于状态,不属于图形?”小a问。

“每个节点应定义输入、输出、提交点和幂等策略。”老z说,“示意: ‘生成草稿’可安全重跑,‘提交报销’须先查询幂等键或转入人工处理。Loop、Workflow 与 Graph 可以混合:确定工作流负责授权和交付,图负责分支,受轮次限制的 Loop 只处理开放式分析。这样牺牲了少量灵活性,换来可恢复和可测试的控制面。”

“那一个节点的定义,到底该写全哪些东西?”

“可以按四件事检查。”老z说,

节点的四份定义

  1. 输入:进入节点需要哪些数据,缺了会怎样
  2. 输出:节点成功时产出的结果,供下游消费
  3. 提交点:副作用落地的位置,提交前可重跑,提交后要幂等
  4. 幂等策略:重跑一次与跑一次的差别,靠幂等键、查询外部状态还是转人工

“最常见的错误,是把恢复点画在图里、却只落在脑子里。”老z说,“图上有‘已提交’,程序里却没有对应的字段,进程一重启就等于没提交。恢复点必须能从一个可持久化的状态里读出来——它属于状态,不属于那张图。”

把恢复设计在副作用之前 ​

“那三种形式有没有一个统一的看法?”小a问。

“三种形式都可落到状态机:状态、事件、转移条件和可持久化 checkpoint。”老z说,“Workflow 通常把转移写成顺序步骤;Graph 显式记录节点和边;Loop 则把‘下一步’部分委托给模型,但仍需要轮次预算和停止状态。差异在谁拥有路由权,而不是是否能画成图。”

“那状态机的状态和转移,具体怎么定义?”小a问。

“状态是‘流程现在在哪’,转移是‘发生什么事件、满足什么条件,才从这里走到那里’。”老z说,“拿报销流程举例,一个最小状态机长这样:”

text
draft ──(提交)→ submitted ──(审批通过)→ approved ──(打款)→ paid
                  │                            │
                  └──(退回)──→ draft       └──(拒绝)→ rejected
状态含义允许的转移转移触发的事件
draft草稿,可编辑→ submitted用户提交
submitted已提交,等待审批→ draft / approved退回 / 审批通过
approved已批准→ paid / rejected打款 / 审批拒绝
paid已打款,终态无—
rejected已拒绝,终态无—

“定义状态机的价值是非法转移变得可见。”老z说,“比如‘已打款’的工单不该能回到‘草稿’——状态机里没有这条转移,代码想让它回去都回不去。没有状态机时,这类非法状态靠开发者自觉避免,一旦漏掉,就会在数据里留下‘已打款却还在审批’的矛盾记录。”

“那状态机和图的差别是什么?”小a问。

“状态机强调‘状态与转移的合法性’,图强调‘节点与路径的表达力’。”老z说,“状态机关心‘哪些转移是允许的’,图关心‘哪些路径是可达的’。同一个业务,两种视角可以同时存在——用图表达路由结构,用状态机约束转移合法性。它们不是替代关系,是互补的两个检查面。”

“能串个例子吗?”小a问。

“贯穿场景是创建发布工单。可先 checkpoint ‘验证完成’,再执行一次带幂等键的外部创建;重放时读取 checkpoint 和键,不能盲目再次创建。”老z说,“失败路径一是写入后进程崩溃,恢复需要查询外部状态;二是模型路由错误,Graph 应送到人工或安全终点;三是 checkpoint 未写入,必须把该风险与副作用顺序一并测试。混合编排可把固定审批包在外层,把受限调查 Loop 放在节点内;不能因节点使用模型就放弃重放、审计和恢复契约。”

“那这几种失败,各自怎么恢复?”小a追问。

“按副作用是否已经落地来分。”老z说,

失败位置副作用状态恢复方式反例
checkpoint 写入前无副作用重跑当前节点若节点本身会写外部,仍需幂等
checkpoint 写入后副作用已落地从 checkpoint 重放,靠幂等键去重无幂等键 → 重复写入
外部系统已执行不可见查询外部状态、用幂等键、或转人工盲目重发 → 重复订单
模型路由错误走错分支送到人工或安全终点让模型自己纠正 → 可能再次走错

“这张表的反例列很关键。”老z说,“每一种恢复方式都有一个‘想当然会出事’的版本——以为重放安全就重放、以为重发没事就重发。恢复设计的本质,是先把‘会怎么出事’列出来,再决定恢复方式,而不是先写恢复代码再祈祷不出事。”

图的绘制与实现的落差 ​

“我注意到一个问题:很多流程图画得很好看,代码却跟图对不上。”小a说。

“这是 Workflow 和 Graph 最容易踩的坑——图是给人看的,实现是给机器跑的,两者之间没有自动的一致性保证。”老z说,“画图时,你会自然省略错误处理、超时、幂等这些细节——因为图表达的是‘主路径’。但实现不能省略:每个节点都要处理失败,每条边都要有失败时的去向。”

“那图的作用到底是什么?”

“作为沟通与设计的起点,不是作为验收的终点。”老z说,“图的价值在于:让参与讨论的人看到‘主路径长什么样、谁决定下一步、哪里有人工关卡’。但图的正确性不代表实现的正确性——实现是否与图一致,要靠测试来核对,不能靠‘图是这么画的’来证明。”

图画层实现层常见落差
节点函数/服务/子流程节点里实际做的事比图上的多(隐藏副作用)
边函数调用/消息/路由图上一条边,代码里可能有重试、降级、超时
分支条件if/switch/路由规则图上的菱形,实现里可能有多层嵌套判断
终止返回/退出/异常图上没画的异常路径,实现里必须画

“那怎么缩小这个落差?”

“两个手段。”老z说,“一是在图上标注非主路径——用虚线或注解标出超时、失败、人工介入的走向,让讨论时就能看到边界;二是用实现反向核对图——代码写完,把每个节点的实际行为回填到图上,看图上的承诺和代码的实现是否一致。落差不可消除,但可以显式化——显式化的落差是已知边界,藏起来的落差是定时炸弹。”

“那模型参与的节点,图能表达吗?”

“能表达‘这个节点会调用模型’,但表达不了‘模型会怎么决策’。”老z说,“图只能画‘模型节点的输入输出和停止条件’,画不出模型的决策内容。**所以模型节点的图标注要特别小心:它画的是边界,不是行为。**边界是预算、停止条件、输入输出契约;行为是模型在边界内自己决定的,画不出来也不该假装画得出来。”

常见编排反例 ​

“那有没有几类‘看起来合理、实际会翻车’的编排设计?”小a问。

“有三类很典型。”老z说,

三个编排反例

  1. 把整个任务塞进一个大 Loop,假装它是节点——外部看不到它的内部状态,预算、轮次、停止原因全被藏起来,恢复和审计无从谈起
  2. 图节点里直接做模型自由发挥,没有任何预算——图只保证了路由确定性,没保证节点内部不失控
  3. 把所有模型调用都当成可重放——模型调用有概率性,重放会得到不同结果,不能和确定性计算一样安全重跑

“这三个反例的共同点是什么?”小a问。

“都是把某一层的约束,误当成了整个系统的保证。”老z说,“Loop 有停止条件,不等于整条流程安全;Graph 有路由,不等于节点内不失控;重放有幂等键,不等于所有节点都幂等。每一层只能保证自己那一层的边界,跨层的假设必须显式声明和验证。”

“那做编排设计的时候,该从哪里开始想?”小a问。

“从最坏情况开始想。”老z说,“先问‘这个流程最贵的一次失败是什么’——是重复扣款、是发错通知、还是生成错了文档——然后从最贵的失败反推:它在哪一层发生、那一层靠什么拦住、如果拦不住会怎样。**编排设计的起点不是选形式,是把最坏情况写下来。**形式选择只是这个思考的落点,不是起点。”

“最后总结一句,怎么才算把编排想清楚了?”小a问。

“能同时回答三个问题就算。”老z说,“第一,每一步的路由权在谁手里——代码还是模型;第二,每个节点的预算和停止条件是什么——不会无限转;第三,每次失败后从哪恢复、会不会重复副作用。三个问题都有答案,形式选哪种都能站稳;缺一个,形式再对也会在某个角落塌掉。”

小结 ​

Loop、Workflow 和 Graph 是三种表达控制权和状态的工具,它们不是互斥的,而是按"路径有多固定"来选择。Agent Loop 把控制权交给模型,让它在每一步自主决定下一步,适合开放性的、路径无法预先确定的问题。Workflow 把路径固定下来,每一步做什么、按什么顺序、满足什么条件才往下走,都写在定义里,适合出错代价高、需要可审计可回放的流程。Graph 介于两者之间,用节点和边表达可能的分支,既允许条件路由,又保留整体结构的可观测性。三种形式在工程特征上差别明显:可观测性、成本、失败恢复、测试组合都不同,选择形式时这些隐性成本往往比"形式名字"更重要。

检查点和重放是 Workflow 与 Graph 的配套能力:保存恢复点让流程可以中断后续接,重放要求每一步都幂等,否则中断重跑会重复扣款。恢复方式按副作用是否已落地分四类:checkpoint 前重跑、checkpoint 后靠幂等键去重、外部已执行时查外部状态或转人工、模型走错路时送人工或安全终点。三种形式都可落到状态机——状态、事件、转移条件和可持久化 checkpoint——状态机的价值在于让非法转移变得可见,图和状态机是互补的两个检查面,不是替代关系。

混合编排是常态:入口用确定路由分流,中间用受限 Loop 分析,出口回到确定流程收尾,每个开放节点都有预算和停止条件。常见的三个反例是:把整个任务塞进大 Loop 假装它是节点、图节点里做无预算的模型自由发挥、把所有模型调用当成可安全重放。它们的共同点是把某一层的约束误当成整个系统的保证——每一层只能保证自己那一层的边界,跨层的假设要显式声明和验证。

动手核验 ​

  1. 将一个报销流程画成节点、状态和条件,给每个外部写入标出幂等策略。
  2. 把其中一个开放式“解释异常”节点改为最多三轮的 Loop,并定义停止原因。
  3. 注入一次超时和一次重复投递,验证流程能否恢复且不会重复提交。