🐦 第4章 扣子工作流详解
📄 一句话概括

工作流是一张有向无环图:节点是函数,边是数据流。 节点只有两件事要定义——吃什么(入参)、吐什么(出参)。边就是「上游的出参喂给下游的入参」。 存盘前要过四道校验:有环、孤儿节点、悬空出口、结束节点个数。

📝 本章和前面几章的关系

上一节讲它值什么,这一节讲它长什么样。这一节是全章最抽象的一篇,也是后面九个案例的地基。 如果你写过函数,把它当「函数的调用的图」看就行。区别只有一个:图上的箭头有方向、不能成环。 「有向无环图」这个概念在第 17 课(编程原理)里出现过,讲的是依赖关系; 这里讲的是同一样东西的另一个用法。

一、节点是函数,边是数据

每个节点干两件事:吃进一些变量,吐出一些变量。 就这样。 「开始」节点吃用户的输入,吐出一个 text;「大模型」节点吃 text,吐出一段回答; 「结束」节点把回答交出去。 边不是「下一步执行谁」,而是「谁的出参喂给谁的入参」。 这个区别很重要:连线的动作,本质是在接数据。

所以连线之前先问一句:这一步要吃什么?上一步给得出吗? 给不出,就是连错了。 演示里每条轨迹的「输出端口」那一列,就是这一步吐出来的东西。 一个节点可以有好几个端口,下游挑一个接。

二、开始与结束

开始节点是入口,只有一个。它定义「这次跑要传进来什么」——一句用户的话,或者一组变量。 结束节点是出口,也只有一个。它定义「交出去什么」——可以是一个字段,也可以是好几个字段打包。 中间可以有多个节点,但入口和出口各只有一个。这是平台强制的,不是习惯。

为什么只能有一个出口?因为调用方要知道「去哪儿取结果」。两个出口,它不知道该等哪个。

三、变量与引用

节点之间的数据靠变量传。引用上游的写法通常是「节点名.字段名」。 演示里大模型节点引用的是 kb.hit——知识库节点吐出来的命中段落。这个名字对不上,引用就是空的。 变量引用错不会报错,只会是空值。 这一条和上一章插件那个坑是同一个:链通、日志绿、答案缺个角。

所以规矩是:连完线,逐个节点点开看一眼入参有没有值。别只看图绿不绿。

四、存盘前过四道校验

第一道,有没有环。 有环就排不出拓扑序,排到某个节点时它永远在等另一个。 第二道,有没有孤儿节点。 连进来了但没连出去、或者根本没接上的节点,永远不会执行。 第三道,有没有悬空出口。 某个节点的输出接不到任何地方,这一支跑完就断了。

第四道,结束节点是不是恰好一个。 零个没有结果,两个平台不知道回哪个。 演示里四种毛病各报各的,报的话都很直白。 这四项都是存盘前能查出来的,但真机上很多人是跑完才发现——连完先点一下校验。

五、拓扑序:图是怎么跑起来的

调度器不按你画的顺序跑,按拓扑序跑:一个节点的所有上游都跑完了,它才能跑。 这个规则有个好处:没有依赖关系的节点可以并行。 三条支路互不相干,理论上可以同时跑。 也有个代价:你画的先后不一定是你以为的先后。 两条独立支路,谁先跑由调度决定。

所以别在两个节点之间偷偷依赖「执行顺序」——要依赖,就明明白白连一条边。

六、图与代码的对应

图上的东西 代码里的对应
节点 一个函数
节点的入参 函数的参数
节点的出参 函数的返回值
边 把返回值传给下一个函数
选择器 if / else
循环节点 for 循环
结束节点 return

这张表能帮你把熟悉的写代码经验搬过来。图就是一段代码的可视化,不是另一种东西。 反过来也成立:如果你的图连自己都读不懂,说明你对这段逻辑还没想清。先写成伪代码,再画图。

七、动手 Demo:图合不合法:三种存不进去的图

造四张图:一张正常的、一张有环的、一张有孤儿节点的、一张有两个结束的。逐个过校验,看报什么。

def find_cycle(ns, edges):     # DFS 找环,返回环上的节点
    ...

def check(ns, edges):
    bad = []
    cyc = find_cycle(ns, edges)
    if cyc:
        bad.append('存在循环:%s' % ' -> '.join(cyc))
    # 孤儿:没有任何入边(除开始),或没有任何出边(除结束)
    # 悬空出口:出边指向不存在的节点
    # 结束节点个数:必须恰好 1 个
    return bad

f = Flow(); f.add(a, b, c); f.link(a, b); f.link(b, c)
print(f.topo())                # ['a', 'b', 'c']
print(f.check())               # []

实跑输出:

--------------------------------------------------------------------
图合不合法:三种存不进去的图
--------------------------------------------------------------------


  ① 一张正常的图
  --------------------------------------------------------------------
  拓扑序:a -> b -> c
  校验:通过

  ② 有环
  --------------------------------------------------------------------
  找到的环:b -> c -> d -> b
  平台保存时会报「存在循环」,因为拓扑排序排不进去——排到 b 的时候它永远在等 d。
  想做循环怎么办?用「循环节点」:一个节点里面转,图上没有环。

  ③ 孤儿与多出口
  --------------------------------------------------------------------

| 图的毛病 | 检查器怎么说 |
|---|---|
| 孤儿节点 | 孤儿节点:x、悬空出口:x |
| 两个结束 | 结束节点不止一个(2 个) |
| 悬空出口 | 悬空出口:b、没有结束节点 |

  三种毛病各对应一类事故:孤儿节点永远不执行、两个出口平台不知道回哪个、悬空出口跑完没有结果。
  ! 这三条都是「存盘前能查出来的」,但真机上很多人是跑完才发现。连完先看一眼校验。

①第一张图正常:拓扑序 a、b、c,校验通过。拓扑序就是「谁能跑了」的顺序。 ②有环那张,报的是 b -> c -> d -> b。 排到 b 的时候它永远在等 d,所以拓补排序排不进去——平台保存时就报「存在循环」。 ③第三张表三种毛病各一行:孤儿节点加悬空出口、两个结束、悬空出口到没有结束。报的话都很直白。 ④三种毛病对应三类事故:孤儿永远不执行、两个出口不知道回哪个、悬空出口跑完没结果。 ⑤最后那句提醒值钱:这四项都是存盘前能查的,但真机上很多人跑完才发现。 连完先点一下校验,两秒钟的事。

30 秒实验

  1. 把你那张图逐节点点开,看每个节点的入参有没有值。有空的就是引用写错了。
  2. 数一下图里有几个结束节点。只能有一个。
  3. 把你最复杂的那张图写成伪代码。写的过程中你会发现几个多余的节点。

常见误区

  1. 「连线的意思是下一步执行谁。」不是。边是数据流——谁的出参喂给谁的入参。
  2. 「引用写错会报错。」不会。它只是取到空值,链照样跑完,答案缺个角。
  3. 「节点越多能力越强。」节点是函数,函数越多出错点越多。能合并的合并。
自测三问(点开对答案) 1. 工作流的边表示什么? —— 数据流:上游节点的出参喂给下游节点的入参。不是「下一步执行谁」。 2. 存盘前要过哪四道校验? —— 有没有环、有没有孤儿节点、有没有悬空出口、结束节点是不是恰好一个。 3. 为什么两个结束节点不行? —— 调用方不知道去哪儿取结果。平台强制只留一个出口。

骨架懂了,接下来是砖头。 下一节把十五个节点归成四类,并且只用六个把一条链真跑一遍——这一节是全章最长的一篇。

相关笔记