工作流的数据模型:节点 / 边 / 引用 / 变量
30 秒导读: FastGPT 让你在画布上拖节点、连线,搭出一条 AI 工作流。但在代码里,这张图不过是两个数组——一个装节点、一个装边。这一章只干一件事:把「节点、边、引用、变量」这四个词讲成你后面看任何工作流代码都能用的词汇表。不讲怎么跑(留给 03-workflow-engine),只讲这张图长什么样。
本章是全组最浅的一章。读完你应该能回答:一张 Flow 存进数据库时是什么结构?两个节点之间"连一根线"到底连的是什么?一个节点怎么知道自己的输入该从别人哪个输出取值?
1. 这是什么:一张 Flow 就是「节点数组 + 边数组」
先建立最粗的心智模型。你在 FastGPT 画布上看到的东西,落到数据层只有两类:
- 节点(Node):画布上的一个个方块——"知识库搜索""AI 对话""判断器"。每个节点自带一组输入和一组输出。
- 边(Edge):连接方块的那根线,记录"从哪 个节点的哪个桩,连到哪个节点的哪个桩"。
一整张工作流存进数据库,就是 { nodes: [...], edges: [...] } 两个数组。没有别的魔法。
一句话直觉: 把节点想成"函数",边想成"调用顺序的箭头",引用想成"函数参数从哪个变量取值"。画布只是这堆数据的可视化外壳。
这一章要拆的四个词,各管一层:
| 词 | 在数据里是什么 | 管什么 |
|---|---|---|
| 节点 Node | 带 inputs/outputs 的对象 | 一个可执行单元 |
| 边 Edge | {source, sourceHandle, target, targetHandle} | 控制流:谁跑完轮到谁 |
| 引用 Reference | 输入值写成 [nodeId, outputId] | 数据流:这个输入的值从哪取 |
| 变量 Variable | 特殊"节点"VARIABLE_NODE_ID 的输出 | 全局变量,供任意节点引用 |
⚠ 本章最重要的一个认知:边和引用是两套独立的线。边决定"执行顺序",引用决定"值怎么流"。它们经常在画布上重合成同一根连线,但在代码里是分开存、分开解析的。记住这条,后面全通。
2. 顶层全景:四个概念怎么拼在一起
先看一张最小工作流的数据结构。假设画布上是「开始 → 知识库搜索 → AI 对话」三个节点:
┌──────────────── 边(控制流) ────────────────┐
│ 谁跑完轮到谁,靠 source/target 连节点 │
▼ ▼
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ workflowStart│──edge─▶│ datasetSearch│──edge─▶│ chatNode │
│ (开始) │ │ (知识库搜索) │ │ (AI 对话) │
└─────────────┘ └──────────────┘ └─────────────┘
out: userChatInput in: 检索词◀┐ in: 引用文本◀┐
▲ │ │
└── 引用(数据流) ─────────┘ │
输入值 = [开始节点id, "userChatInput"] │
│
知识库输出 quoteQA ── 引用 [搜索节点id,"quoteQA"] ┘
读法:横向实线箭头 = 边(决定执行先后);竖向虚线 = 引用(把上游输出喂给下游输入)。
同一对节点之间,这两条线可以各走各的。
四个概念的职责,一句话各表:
- 节点声明"我有哪些输入口、哪些输出口"。
- 边把节点串成执行链:
workflowStart跑完,激活到datasetSearch的边,轮到它跑。 - 引用让
datasetSearch的"检索词"输入去开始节点取userChatInput的值。 - 变量是一个虚拟节点(id 恒为
VARIABLE_NODE_ID),全局变量都挂在它的"输出"上,任何节点都能引用。
下面逐个拆开。
3. 节点类型:FlowNodeTypeEnum 枚举
每个节点有个 flowNodeType 字段,标明它是哪种节点。全部取值在一个枚举里
(packages/global/core/workflow/node/constant.ts:128 FlowNodeTypeEnum)。挑常用的按语义分组:
| 分组 | 枚举成员(值) | 干什么 |
|---|---|---|
| 系统/入口 | workflowStart('workflowStart')、systemConfig('userGuide')、pluginInput/pluginOutput | 工作流的起点、全局配置、插件出入口 |
| AI 能力 | chatNode、agent、toolCall(值为 'tools')、classifyQuestion、contentExtract、queryExtension(值为 'cfr') | 对话、规划 Agent、工具调用循环、问题分类、内容抽取 |
| 知识库 | datasetSearchNode、datasetConcatNode | RAG 检索与结果拼接 |
| 逻辑/控制 | ifElseNode、userSelect、formInput、stopTool | 条件分支、交互暂停、终止工具循环 |
| 数据处理 | answerNode、textEditor、code、httpRequest468(值为 'httpRequest468')、variableUpdate、readFiles | 回复、文本编辑、沙箱代码、HTTP 请求、改变量、读文件 |
| 嵌套容器 | loop、parallelRun、loopRun(及其系统子节点 loopStart/loopEnd/loopRunStart) | 循环 / 并行 / 批处理,容器内部还是一张子图 |
| 子应用 | appModule、pluginModule、runApp(值为 'app')、tool、toolSet | 把别的应用/插件/工具当一个节点嵌进来 |
几个容易踩的点(都有代码兜底):
- 枚举名和值经常不一样。
toolCall的值是'tools'、systemConfig的值是'userGuide'、queryExtension的值是'cfr'、nestedStart的值是'loopStart'。写代码比对时以值为准。 - 有三类"嵌套父容器"被单独收进一个集合
NESTED_PARENT_NODE_TYPES = {loop, parallelRun, loopRun},配了个isNestedParentNodeType()判定 (node/constant.ts:360、:366)。 - 交互类节点
userSelect/formInput收进INTERACTIVE_NODE_TYPES,规则是"parallelRun体内禁止用、loopRun允许" (node/constant.ts:370,注释在:369)。 loopStart/loopEnd/loopRunStart是"系统子节点",只能由容器自动创建,不许从模板面板手动添加 (NESTED_CHILD_SYSTEM_NODE_TYPES,node/constant.ts:379)。