数据截至 (上游 commit 482e72519a4e)
工作流的数据模型:节点 / 边 / 引用 / 变量
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)。
4. 节点的输入口和输出口:input / output item
节点最核心的两个字段是 inputs 和 outputs——两个数组,每个元素描述一个"接线桩 + 它的配置"。
4.1 输入项 FlowNodeInputItemType
一个输入项声明"我这个口叫什么、在编辑器里长什么控件、值是什么类型"
(packages/global/core/workflow/type/io.ts:260 FlowNodeInputItemTypeSchema)。关键字段:
| 字段 | 含义 |
|---|---|
key | 输入的键名,如 'userChatInput'、'model'——节点内部靠它取值 |
renderTypeList | 这个口在编辑器里能用哪几种控件,数组,可切换 |
selectedTypeIndex | 当前选中 renderTypeList 里第几个控件 |
valueType | 值的数据类型(string/number/datasetQuote…),决定连线兼容性 |
value | 当前值。若是引用,这里存的就是 [nodeId, outputId] |
toolDescription | 非空时,说明这个输入可被 AI 当作"工具参数"填 |
renderTypeList 的取值来自另一个枚举 FlowNodeInputTypeEnum(node/constant.ts:3),常见几种:
reference(引用别的节点输出)、input(单行)、textarea(多行)、numberInput、switch、selectselectLLMModel(选模型)、selectDataset(选知识库)、settingDatasetQuotePrompt(知识库引用配置)JSONEditor、fileSelect、hidden(隐藏,仅存值不渲染)
一个口常常允许多种控件。比如"用户问题"这个输入,renderTypeList: [reference, textarea]——你既能直接打字,也能改成引用上游输出(template/input.ts:21 Input_Template_UserChatInput)。
4.2 输出项 FlowNodeOutputItemType
输出项声明"我吐出什么、叫什么、什么类型"
(type/io.ts:310 FlowNodeOutputItemTypeSchema)。关键字段:
| 字段 | 含义 |
|---|---|
id | 输出的唯一 id——引用就是靠它定位的 |
key | 输出键名(多数模板里 id === key) |
type | 输出的生成方式,取值见下 |
valueType | 输出值类型,供下游做兼容校验 |
type 来自 FlowNodeOutputTypeEnum(node/constant.ts:120):
static:固定输出(绝大多数)dynamic:运行时才确定的动态输出(如 HTTP 节点自定义字段)error:错误分支输出,配合节点的catchErrorsource:作为连线源桩hidden:不展示
4.3 值类型 valueType:连线的"电压等级"
valueType 是把节点接起来时最要紧的一个概念——它决定"这根线两端插得上插不上"。全部取值在
WorkflowIOValueTypeEnum(constants.ts:15):基础类型 string/number/boolean/object,数组类型
arrayString/arrayNumber/arrayObject/arrayAny,以及几个 FastGPT 专有类型:
chatHistory:对话历史数组{obj, value}[]datasetQuote:知识库检索结果{id, q, a, ...}[]selectDataset:选中的知识库列表any:任意,跟谁都兼容
每种类型的展示元信息(label 等)挂在 FlowValueTypeMap(node/constant.ts:177),拿元信息用
getFlowValueTypeMeta()——取不到时兜底成 any(node/constant.ts:248)。
5. 静态存储 vs 运行时:StoreNode / RuntimeNode,StoreEdge / RuntimeEdge
同一张图有两副面孔:存数据库时一套类型,跑起来时又转成另一套(多带了运行期状态)。这一层区分很重要。
5.1 节点:Store → Runtime
- 存的样子
StoreNodeItemType:就是编辑器里那份完整节点,带position(画布坐标)等 (type/node.ts:288,继承FlowNodeCommonTypeSchema:149)。 - 跑的样子
RuntimeNodeItemType:调度器只保留执行需要的字段,丢掉画布坐标一类的 UI 数据,另加isEntry(是不是入口节点)(runtime/type.ts:138)。
转换函数是 storeNodes2RuntimeNodes()——挑字段、按传入的 entryNodeIds 打上 isEntry 标记
(runtime/utils.ts:247)。谁是入口由 getWorkflowEntryNodeIds() 决定:默认是
systemConfig / workflowStart / pluginInput 这几类(runtime/utils.ts:221、入口清单在 :232)。
5.2 边:Store → Runtime,多了一个 status
- 存的样子
StoreEdgeItemType:只有四个字段——source、sourceHandle、target、targetHandle(type/edge.ts:3)。 - 跑的样子
RuntimeEdgeItemType:多一个status: 'waiting' | 'active' | 'skipped'(type/edge.ts:19)——调度器就是靠翻这个状态推进整张图。
转换函数 storeEdges2RuntimeEdges() 给每条边盖上初始 status: 'waiting'(runtime/utils.ts:207)。
(status 怎么流转、怎么驱动调度,是 03-workflow-engine 的活,这里只需知道字段存在。)
6. 连线传值的两套线:边(控制流) 与 引用(数据流)
回到本章的核心认知——边和引用是两套独立的线。这一节把它讲透。
6.1 边:接的是"接线桩 id"(handle id)
一条边记的是"源节点的某个桩 → 目标节点的某个桩"。桩的 id 不是随便起的,而是用
getHandleId() 拼出来的(utils.ts:55):
// 真实源码 utils.ts:55 getHandleId
export const getHandleId = (
nodeId: string,
type: 'source' | 'source_catch' | 'target',
key: string
) => {
return `${nodeId}-${type}-${key}`;
};
即 sourceHandle = "节点id-source-输出key",targetHandle = "节点id-target-输入key"。多出来的
source_catch 是错误分支的源桩——节点开了 catchError 时,错误从这个桩流出(对应
Output_Template_Error_Message 这个 type: error 的输出,template/output.ts 尾部)。
并非所有边都是控制流。 工具调用的连线(handle 为 selectedTools)不算普通执行边,调度前会被
filterWorkflowEdges() 滤掉(runtime/utils.ts:273):
// 真实源码 runtime/utils.ts:273 filterWorkflowEdges
return edges.filter(
(edge) =>
edge.sourceHandle !== NodeOutputKeyEnum.selectedTools &&
edge.targetHandle !== NodeOutputKeyEnum.selectedTools
);
6.2 引用:输入值写成 [nodeId, outputId]
数据流不走边,走引用。当一个输入的控件是 reference 时,它的 value 不是普通值,而是一个二元组
[来源节点id, 来源输出id](type/io.ts:373 ReferenceValueType——单个是 [string, string?],也可以是它的 数组,表示引一批)。
判断"某输入到底该不该按引用解析",用 nodeInputIsReference()(utils.ts:68)。这里有个坑:
控件是 settingDatasetQuotePrompt(知识库引用配置)时,renderType 虽不是 reference,但它的值仍是
[nodeId, outputId],也必须当引用解析——代码专门为它留了一条判断分支(utils.ts:71)。
6.3 把引用解析成真实值:getReferenceVariableValue
运行时靠 getReferenceVariableValue() 把 [nodeId, outputId] 换成真正的值(runtime/utils.ts:286)。
核心逻辑就一小段:
// 真实源码 runtime/utils.ts:299 resoleValue(getReferenceVariableValue 内部)
const sourceNodeId = value[0];
const outputId = value[1];
if (sourceNodeId === VARIABLE_NODE_ID) { // 引的是全局变量
if (!outputId) return undefined;
return variables[outputId];
}
const node = nodesMap instanceof Map ? nodesMap.get(sourceNodeId) : nodesMap[sourceNodeId];
if (!node) return value; // 找不到来源节点,原样返回
return node.outputs.find((output) => output.id === outputId)?.value; // 按输出 id 取值
三条规则读出来:
- 来源是
VARIABLE_NODE_ID→ 去全局变量表variables里按outputId取——这就是"变量"作为一个虚拟节点的实现方式(常量在constants.ts:493)。 - 来源是普通节点 → 在节点的
outputs里找output.id === outputId,返回它的value。注意匹配的是输出的id,不是key。 - 引用还能是数组
[nodeId, outputId][],会逐个解析再flat、过滤掉undefined(runtime/utils.ts:323)。
判定一个值是不是合法引用格式,用 isValidReferenceValueFormat():必须是长度为 2、首元素是字符串的数组;传了 nodesMap 还会校验来源节点确实存在(utils.ts:417)。
6.4 valueType 不匹配怎么办:valueTypeFormat 做兜底转换
引用取到的值类型未必和目标输入声明的 valueType 一致。valueTypeFormat() 负责"尽力转换"
(runtime/utils.ts:54):目标要 string 就 String()/JSON.stringify,要 number 就 Number(),要
object/array 就试着 json5.parse,any 则原样放行。这让"把一个 object 引到 string 输入"这种事不至于直接崩。
小结这一节: 执行顺序问谁——问边(source/target/handle);某个输入的值从哪来——问引用
(input.value = [nodeId, outputId]),再经 getReferenceVariableValue 解析、valueTypeFormat 兜底。两条线各走各的。
7. 节点模板长什么样:inputs/outputs 的声明
前面拆的 input/output item,在"节点模板"里成套出现。模板 = 一种节点的出厂定义(画布左侧面板拖出来的就是它的拷贝),类型是 FlowNodeTemplateType(type/node.ts:197)。全部系统模板在
packages/global/core/workflow/template/system/ 下。看两个例子就懂套路。
7.1 最简单的:workflowStart(开始节点)
template/system/workflowStart.ts:21 定义了开始节点:一个输入(用户问题)、一个输出(userChatInput,string 类型)。它还带两个特殊标记 forbidDelete: true(禁删)、unique: true(全图唯一):
// 真实源码 workflowStart.ts:34-43(节选)
inputs: [{ ...Input_Template_UserChatInput, toolDescription: i18nT('workflow:user_question') }],
outputs: [
{
id: NodeOutputKeyEnum.userChatInput, // 输出 id = 'userChatInput'
key: NodeOutputKeyEnum.userChatInput, // id 与 key 相同
label: ...,
type: FlowNodeOutputTypeEnum.static, // 静态输出
valueType: WorkflowIOValueTypeEnum.string // string 类型
}
]
下游任何节点想拿"用户原始问题",就把某输入的 value 设成 [开始节点id, 'userChatInput']——正好对上 6.3 的解析逻辑。
7.2 有料的:datasetSearch(知识库搜索)
template/system/datasetSearch.ts:22 展示一个"真实节点"的完整声明。看几个代表性输入:
- 选知识库:
renderTypeList: [selectDataset, reference],valueType: selectDataset,required: true(datasetSearch.ts:40)。 - 相 似度、topK、检索模式、rerank 等一大堆参数:
renderTypeList: [hidden]——不在画布上直接显示,而是走弹窗配置(datasetSearch.ts:48起)。 - 检索词:复用
Input_Template_UserChatInput但改了key/valueType,并带toolDescription——意味着它能被 Agent 当工具参数填(datasetSearch.ts:127、isTool: true在:34)。
输出侧:一个 quoteQA(valueType: datasetQuote)装检索结果,外加一个 Output_Template_Error_Message 错误输出(datasetSearch.ts:144)。
7.3 复用模板片段
注意上面反复出现的 Input_Template_* / Output_Template_*——这些是可复用的 IO 片段,集中在
template/input.ts 和 template/output.ts。比如 Input_Template_History(对话历史,chatHistory 类型,默认带 6 轮,input.ts:8)、Input_Template_SettingAiModel(选模型,input.ts:45)。toolCall 这类复杂节点
(template/system/toolCall.ts:24)几乎就是把这些片段拼起来,再补一堆 hidden 的高级参数。看模板时先认出这些复用片段,剩下的就好读了。
模板只是"出厂声明"。节点具体怎么执行、Agent 的工具循环怎么转,是 04-ai-nodes;知识库检索内部实现看 05-knowledge-base。
8. 边界与易错点(本章范围内)
诚实划一下这张"数据模型"的边界:
- 本章只讲静态结构和连线规则,不讲调度(边的
status怎么流转 → 03)、不讲节点内部实现(→ 04/05)。 - 枚举名 ≠ 枚举值:
toolCall='tools'、systemConfig='userGuide'、queryExtension='cfr'、nestedStart='loopStart'等,比对务必用值(node/constant.ts:128)。 - 引用按 output.id 匹配,不是 key(
runtime/utils.ts:314)。多数模板id===key,但别默认它们永远相等。 - 边不等于数据流:
selectedTools的连线是工具挂载关系,会被filterWorkflowEdges从执行边里剔除(runtime/utils.ts:273)。 - valueType 兼容靠"尽力转换"而非强校验:
valueTypeFormat会试图把类型掰过来,any一律放行(runtime/utils.ts:54)——所以运行期出现的类型问题,未必在连线时就被拦住。
9. 代码地图(导航索引)
| 主题 | 文件路径 | 关键符号 |
|---|---|---|
| 节点类型枚举 | packages/global/core/workflow/node/constant.ts:128 | FlowNodeTypeEnum |
| 输入控件类型枚举 | packages/global/core/workflow/node/constant.ts:3 | FlowNodeInputTypeEnum |
| 输出生成方式枚举 | packages/global/core/workflow/node/constant.ts:120 | FlowNodeOutputTypeEnum |
| 嵌套/交互节点集合 | packages/global/core/workflow/node/constant.ts:360 | NESTED_PARENT_NODE_TYPES、INTERACTIVE_NODE_TYPES、isNestedParentNodeType |
| 值类型枚举 | packages/global/core/workflow/constants.ts:15 | WorkflowIOValueTypeEnum |
| 值类型元信息 | packages/global/core/workflow/node/constant.ts:177 | FlowValueTypeMap、getFlowValueTypeMeta |
| 输入项结构 | packages/global/core/workflow/type/io.ts:260 | FlowNodeInputItemTypeSchema |
| 输出项结构 | packages/global/core/workflow/type/io.ts:310 | FlowNodeOutputItemTypeSchema |
| 引用值类型 | packages/global/core/workflow/type/io.ts:373 | ReferenceValueType |
| 边结构(存/运行) | packages/global/core/workflow/type/edge.ts:3 | StoreEdgeItemType、RuntimeEdgeItemType |
| 运行时节点 | packages/global/core/workflow/runtime/type.ts:138 | RuntimeNodeItemType |
| 存储节点 | packages/global/core/workflow/type/node.ts:288 | StoreNodeItemType、FlowNodeCommonTypeSchema |
| 接线桩 id 生成 | packages/global/core/workflow/utils.ts:55 | getHandleId |
| 判断输入是否引用 | packages/global/core/workflow/utils.ts:68 | nodeInputIsReference |
| 引用格式校验 | packages/global/core/workflow/utils.ts:417 | isValidReferenceValueFormat |
| 引用解析取值 | packages/global/core/workflow/runtime/utils.ts:286 | getReferenceVariableValue |
| 值类型兜底转换 | packages/global/core/workflow/runtime/utils.ts:54 | valueTypeFormat |
| 过滤工具边 | packages/global/core/workflow/runtime/utils.ts:273 | filterWorkflowEdges |
| Store→Runtime 转换 | packages/global/core/workflow/runtime/utils.ts:207 | storeEdges2RuntimeEdges、storeNodes2RuntimeNodes、getWorkflowEntryNodeIds |
| 全局变量节点 id | packages/global/core/workflow/constants.ts:493 | VARIABLE_NODE_ID |
| 节点模板类型 | packages/global/core/workflow/type/node.ts:197 | FlowNodeTemplateType |
| 模板示例·开始 | packages/global/core/workflow/template/system/workflowStart.ts:21 | WorkflowStart |
| 模板示例·知识库 | packages/global/core/workflow/template/system/datasetSearch.ts:22 | DatasetSearchModule |
| 模板示例·工具调用 | packages/global/core/workflow/template/system/toolCall.ts:24 | ToolCallNode |
| 可复用 IO 片段 | packages/global/core/workflow/template/input.ts:8 | Input_Template_History、Input_Template_UserChatInput、Input_Template_SettingAiModel |
同组其它章:02-chat-pipeline(一次对话端到端)· 03-workflow-engine(调度内核)· 04-ai-nodes(LLM 节点)· 05-knowledge-base(知识库 RAG)。