跳到主要内容

工作流的数据模型:节点 / 边 / 引用 / 变量

30 秒导读: FastGPT 让你在画布上拖节点、连线,搭出一条 AI 工作流。但在代码里,这张图不过是两个数组——一个装节点、一个装边。这一章只干一件事:把「节点、边、引用、变量」这四个词讲成你后面看任何工作流代码都能用的词汇表。不讲怎么跑(留给 03-workflow-engine),只讲这张图长什么样

本章是全组最浅的一章。读完你应该能回答:一张 Flow 存进数据库时是什么结构?两个节点之间"连一根线"到底连的是什么?一个节点怎么知道自己的输入该从别人哪个输出取值?


1. 这是什么:一张 Flow 就是「节点数组 + 边数组」

先建立最粗的心智模型。你在 FastGPT 画布上看到的东西,落到数据层只有两类:

  • 节点(Node):画布上的一个个方块——"知识库搜索""AI 对话""判断器"。每个节点自带一组输入和一组输出
  • 边(Edge):连接方块的那根线,记录"从哪个节点的哪个桩,连到哪个节点的哪个桩"。

一整张工作流存进数据库,就是 { nodes: [...], edges: [...] } 两个数组。没有别的魔法。

一句话直觉: 把节点想成"函数",边想成"调用顺序的箭头",引用想成"函数参数从哪个变量取值"。画布只是这堆数据的可视化外壳。

这一章要拆的四个词,各管一层:

在数据里是什么管什么
节点 Nodeinputs/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 能力chatNodeagenttoolCall(值为 'tools')、classifyQuestioncontentExtractqueryExtension(值为 'cfr'对话、规划 Agent、工具调用循环、问题分类、内容抽取
知识库datasetSearchNodedatasetConcatNodeRAG 检索与结果拼接
逻辑/控制ifElseNodeuserSelectformInputstopTool条件分支、交互暂停、终止工具循环
数据处理answerNodetextEditorcodehttpRequest468(值为 'httpRequest468')、variableUpdatereadFiles回复、文本编辑、沙箱代码、HTTP 请求、改变量、读文件
嵌套容器loopparallelRunloopRun(及其系统子节点 loopStart/loopEnd/loopRunStart循环 / 并行 / 批处理,容器内部还是一张子图
子应用appModulepluginModulerunApp(值为 'app')、tooltoolSet把别的应用/插件/工具当一个节点嵌进来

几个容易踩的点(都有代码兜底):

  • 枚举名和值经常不一样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_TYPESnode/constant.ts:379)。

4. 节点的输入口和输出口:input / output item

节点最核心的两个字段是 inputsoutputs——两个数组,每个元素描述一个"接线桩 + 它的配置"。

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 的取值来自另一个枚举 FlowNodeInputTypeEnumnode/constant.ts:3),常见几种:

  • reference(引用别的节点输出)、input(单行)、textarea(多行)、numberInputswitchselect
  • selectLLMModel(选模型)、selectDataset(选知识库)、settingDatasetQuotePrompt(知识库引用配置)
  • JSONEditorfileSelecthidden(隐藏,仅存值不渲染)

一个口常常允许多种控件。比如"用户问题"这个输入,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 来自 FlowNodeOutputTypeEnumnode/constant.ts:120):

  • static:固定输出(绝大多数)
  • dynamic:运行时才确定的动态输出(如 HTTP 节点自定义字段)
  • error:错误分支输出,配合节点的 catchError
  • source:作为连线源桩
  • hidden:不展示

4.3 值类型 valueType:连线的"电压等级"

valueType 是把节点接起来时最要紧的一个概念——它决定"这根线两端插得上插不上"。全部取值在 WorkflowIOValueTypeEnumconstants.ts:15):基础类型 string/number/boolean/object,数组类型 arrayString/arrayNumber/arrayObject/arrayAny,以及几个 FastGPT 专有类型:

  • chatHistory:对话历史数组 {obj, value}[]
  • datasetQuote:知识库检索结果 {id, q, a, ...}[]
  • selectDataset:选中的知识库列表
  • any:任意,跟谁都兼容

每种类型的展示元信息(label 等)挂在 FlowValueTypeMapnode/constant.ts:177),拿元信息用 getFlowValueTypeMeta()——取不到时兜底成 anynode/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:只有四个字段——sourcesourceHandletargettargetHandletype/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 取值

三条规则读出来:

  1. 来源是 VARIABLE_NODE_ID → 去全局变量表 variables 里按 outputId 取——这就是"变量"作为一个虚拟节点的实现方式(常量在 constants.ts:493)。
  2. 来源是普通节点 → 在节点的 outputs 里找 output.id === outputId,返回它的 value。注意匹配的是输出的 id,不是 key
  3. 引用还能是数组 [nodeId, outputId][],会逐个解析再 flat、过滤掉 undefinedruntime/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.parseany 则原样放行。这让"把一个 object 引到 string 输入"这种事不至于直接崩。

小结这一节: 执行顺序问谁——问source/target/handle);某个输入的值从哪来——问引用input.value = [nodeId, outputId]),再经 getReferenceVariableValue 解析、valueTypeFormat 兜底。两条线各走各的。


7. 节点模板长什么样:inputs/outputs 的声明

前面拆的 input/output item,在"节点模板"里成套出现。模板 = 一种节点的出厂定义(画布左侧面板拖出来的就是它的拷贝),类型是 FlowNodeTemplateTypetype/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: selectDatasetrequired: truedatasetSearch.ts:40)。
  • 相似度、topK、检索模式、rerank 等一大堆参数:renderTypeList: [hidden]——不在画布上直接显示,而是走弹窗配置(datasetSearch.ts:48 起)。
  • 检索词:复用 Input_Template_UserChatInput 但改了 key/valueType,并带 toolDescription——意味着它能被 Agent 当工具参数填(datasetSearch.ts:127isTool: true:34)。

输出侧:一个 quoteQAvalueType: datasetQuote)装检索结果,外加一个 Output_Template_Error_Message 错误输出(datasetSearch.ts:144)。

7.3 复用模板片段

注意上面反复出现的 Input_Template_* / Output_Template_*——这些是可复用的 IO 片段,集中在 template/input.tstemplate/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 匹配,不是 keyruntime/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:128FlowNodeTypeEnum
输入控件类型枚举packages/global/core/workflow/node/constant.ts:3FlowNodeInputTypeEnum
输出生成方式枚举packages/global/core/workflow/node/constant.ts:120FlowNodeOutputTypeEnum
嵌套/交互节点集合packages/global/core/workflow/node/constant.ts:360NESTED_PARENT_NODE_TYPESINTERACTIVE_NODE_TYPESisNestedParentNodeType
值类型枚举packages/global/core/workflow/constants.ts:15WorkflowIOValueTypeEnum
值类型元信息packages/global/core/workflow/node/constant.ts:177FlowValueTypeMapgetFlowValueTypeMeta
输入项结构packages/global/core/workflow/type/io.ts:260FlowNodeInputItemTypeSchema
输出项结构packages/global/core/workflow/type/io.ts:310FlowNodeOutputItemTypeSchema
引用值类型packages/global/core/workflow/type/io.ts:373ReferenceValueType
边结构(存/运行)packages/global/core/workflow/type/edge.ts:3StoreEdgeItemTypeRuntimeEdgeItemType
运行时节点packages/global/core/workflow/runtime/type.ts:138RuntimeNodeItemType
存储节点packages/global/core/workflow/type/node.ts:288StoreNodeItemTypeFlowNodeCommonTypeSchema
接线桩 id 生成packages/global/core/workflow/utils.ts:55getHandleId
判断输入是否引用packages/global/core/workflow/utils.ts:68nodeInputIsReference
引用格式校验packages/global/core/workflow/utils.ts:417isValidReferenceValueFormat
引用解析取值packages/global/core/workflow/runtime/utils.ts:286getReferenceVariableValue
值类型兜底转换packages/global/core/workflow/runtime/utils.ts:54valueTypeFormat
过滤工具边packages/global/core/workflow/runtime/utils.ts:273filterWorkflowEdges
Store→Runtime 转换packages/global/core/workflow/runtime/utils.ts:207storeEdges2RuntimeEdgesstoreNodes2RuntimeNodesgetWorkflowEntryNodeIds
全局变量节点 idpackages/global/core/workflow/constants.ts:493VARIABLE_NODE_ID
节点模板类型packages/global/core/workflow/type/node.ts:197FlowNodeTemplateType
模板示例·开始packages/global/core/workflow/template/system/workflowStart.ts:21WorkflowStart
模板示例·知识库packages/global/core/workflow/template/system/datasetSearch.ts:22DatasetSearchModule
模板示例·工具调用packages/global/core/workflow/template/system/toolCall.ts:24ToolCallNode
可复用 IO 片段packages/global/core/workflow/template/input.ts:8Input_Template_HistoryInput_Template_UserChatInputInput_Template_SettingAiModel

同组其它章:02-chat-pipeline(一次对话端到端)· 03-workflow-engine(调度内核)· 04-ai-nodes(LLM 节点)· 05-knowledge-base(知识库 RAG)。