Skip to content

Part I · 概念篇 / Ch 16

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

小A与老Z俯看桌面微缩花园中的石板直路、分岔小径、带护栏的环路和发光检查点。

小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说,“从可观测性、成本、失败恢复,到测试和调试,三者的差别摆在一起看得很清楚:”

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

“光看测试那一行,三家要考的东西就不一样?”

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

“有没有先按一种形式做、后期再切换的常见路子?”

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

状态、路由与恢复 ​

“不管画成什么形状,工程上要关心什么?”

“五个问题:当前状态是什么,谁能改它,分支条件从哪里来,失败后从哪里恢复,动作是否幂等。”老Z画了个简化图式:

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

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

“因为图是给人看的,状态是给程序用的。”老Z说,“同一个流程画成什么样都行,但状态必须落在某个可持久化的地方,数据库、文件或内存对象都可以,否则进程一重启,流程就不知道自己走到哪了。判断一个编排方案是否可靠,不看图画得对不对,看这五个问题是不是每个都有落点。”

“五个问题分别对应什么工程部件?”

“一一对应。”老Z把五个问题摆成一张表:

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

“这张表就是检查一个编排实现完不完整的清单。”老Z说,“五个问题各有一个答案,实现才算闭环。哪个问题答不上来,那个位置就是故障点:不是说那里可能出错,而是出错时你手里没有处理它的手段。”

“图里‘执行任务’那个节点要是挂了呢?”

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

“补偿动作是什么?”

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

“那是不是不确定性越高,就越得用 Loop?”

“不一定。可以把不确定的部分圈在一个节点里,外层仍用确定的工作流保住审批、权限和交付。”老Z说,“选哪种,看的是可预测性、失败代价与可观测性,不是谁更智能。失败代价极高的固定流程,哪怕里面有模型参与,也宁可把模型决策关在一个有预算的节点里,别让整条流程跟着自由发挥。”

“系统已经在跑了,想往灵活的方向演化,从哪儿动最安全?”

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

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

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

“反向演化更简单。”老Z说,“给模型判断的输出先加一道规则校验,不合格就退回固定分支;等规则覆盖得足够充分,再彻底移除模型判断。从灵活退回确定,靠的是规则一步步取代模型,每一步都要有数据证明规则达到了原模型的判断质量,不能凭感觉砍。”

要点

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

混合编排 ​

“那我能把三种混着用吗?”

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

“听下来,混的是形式,边界一处都不能混?”

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

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

“维护成本是不是比单一形式高?”

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

“画个混合结构给我看看?”

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

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

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

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

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

“那这个盒子该画在哪?”

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

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

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

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

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

“因为恢复点必须能被程序读回来,画在图上不算数。”老Z说,“拿报销流程举例:‘生成草稿’可以安全重跑;‘提交报销’就必须先查幂等键,或者转入人工处理。Loop、Workflow 与 Graph 可以混合:确定的工作流负责授权和交付,图负责分支,受轮次限制的 Loop 只处理开放式分析。这样做牺牲少量灵活性,换来一个可恢复、可测试的控制面。”

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

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

节点的四份定义

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

“最常见的错误,是把恢复点画在图里、却只落在脑子里。”老Z说,“图上有‘已提交’,程序里却没有对应的字段,进程一重启就等于没提交。恢复点要从可持久化的状态里读出来,这件事那张图替你做不了。”

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

“这三种形式有没有一个统一的看法?”

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

“状态机的状态和转移,具体怎么定义?”

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

text
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说,

三个编排反例

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

“这三个反例的共同点是什么?”

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

“做编排设计的时候,该从哪里开始想?”

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

“最后收个尾:怎么才算把编排想清楚了?”小A问。

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

小结 ​

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

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

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

动手核验 ​

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