工作流是一张有向无环图:节点是函数,边是数据流。 节点只有两件事要定义——吃什么(入参)、吐什么(出参)。边就是「上游的出参喂给下游的入参」。 存盘前要过四道校验:有环、孤儿节点、悬空出口、结束节点个数。
上一节讲它值什么,这一节讲它长什么样。这一节是全章最抽象的一篇,也是后面九个案例的地基。 如果你写过函数,把它当「函数的调用的图」看就行。区别只有一个:图上的箭头有方向、不能成环。 「有向无环图」这个概念在第 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. 为什么两个结束节点不行? —— 调用方不知道去哪儿取结果。平台强制只留一个出口。骨架懂了,接下来是砖头。 下一节把十五个节点归成四类,并且只用六个把一条链真跑一遍——这一节是全章最长的一篇。
- ☐ (本篇完,下一篇 4.4 扣子工作流的常见节点)
相关笔记
- 工作流及其分类:三种骨架与循环节点
- 工作流能解决什么问题:稳和准是两件事
- 17 编程原理 · 索引
- 🐦 扣子开发 AI Agent · 索引