跳到主要内容

第 1 章 · 画布 JSON 怎样变成可执行的 WorkflowSchema

本章讲什么: 前端画布序列化出的 JSON(vo.Canvas)长什么样,后端 CanvasToWorkflowSchema 怎样把它规整成引擎能编译的 WorkflowSchema。这一步是「翻译」,还没到执行。

1.1 前端:画布是怎么来的

前端工作流编辑器在 frontend/packages/workflow/,画布本身基于字节开源的 flowgram 自由布局编辑器(依赖 @flowgram-adapter/free-layout-editor,见 frontend/packages/workflow/playground/package.json)。

它的职责分工大致是:

干什么
playground画布容器、各种 service(运行、保存、校验、连线、拖拽)
nodes每种节点的数据模型、表单、校验器
variable变量系统(哪个字段能引用哪些上游字段)
base公共 store / API / 类型

用户每拖一个节点、连一条线、填一个字段映射,flowgram 都更新内存里的图;保存或试运行时,前端把图序列化成一份 JSON,结构就是后端的 vo.Canvas:一个 Nodes[] + 一个 Edges[]

1.2 契约:vo.Canvas 长什么样

后端拿到的是一个 JSON 字符串,反序列化进 vo.Canvas。核心就两样东西:

  • 节点 vo.Node:有 IDType(数字字符串,如 "3" 表示 LLM)、Data(标题、输入参数、输出定义、各节点专属配置),复合节点还有 Blocks(内层子节点)和 Edges(内层连线)。
  • 连线 vo.EdgeSourceNodeID / TargetNodeID,以及可选的 SourcePortID(用于分支,如选择器的 true / false 端口)。

注意一个细节:节点 Type 在画布里是数字 ID 字符串"1"=开始、"2"=结束、"3"=LLM……),后端用 entity.IDStrToNodeType 把它映射回内部的 NodeType 字符串常量(node_meta.go:35:259NodeTypeMetas 是这张映射表的单一真相源)。

1.3 主流程:CanvasToWorkflowSchema

入口是 to_schema.go:66CanvasToWorkflowSchema。它做四件事,依次进行:

画布 JSON (vo.Canvas)

├─① PruneIsolatedNodes ── 剪掉没连到任何边的孤立节点

├─② 逐节点 NodeToNodeSchema ── 每个节点 → 一个或多个 NodeSchema
│ (复合节点展开成 父NodeSchema + 内层NodeSchema[] + 层级表)

├─③ normalizePorts ── 把前端端口名(true/false/true_2)归一成 branch_N/default

└─④ BuildBranches ── 从带端口的连线里构建分支表


WorkflowSchema(Nodes + Connections + Hierarchy + Branches)

① 剪掉孤立节点

PruneIsolatedNodesto_schema.go:396)给每个节点算「入度」:开始/结束节点预设为 1(永远保留),其余节点看有没有边指向它。入度为 0 的节点和它发出的边一并删掉。这样用户在画布上随手拖了又没连的节点不会进入执行。

② 每个节点 → NodeSchema

NodeToNodeSchemato_schema.go:216)是关键。它先把数字 Type 转成 NodeType,然后查节点适配器注册表

// to_schema.go:230 —— 按节点类型取出对应的 NodeAdaptor,由它把 vo.Node 转成 NodeSchema
na, ok := nodes.GetNodeAdaptor(et)
if ok {
ns, err := na.Adapt(ctx, n, nodes.WithCanvas(c))
...
}

每种节点都注册了一个 NodeAdaptor,集中在 to_schema.go:594RegisterAllNodeAdaptors——这是「有哪些节点」的总清单(开始/结束/选择器/LLM/插件/代码/循环/批处理/数据库/知识库/会话……约 50 个)。想加新节点,就在这里注册一个适配器。

巧妙之处:注册表 + 适配器接口。 引擎核心完全不认识「LLM 节点」「数据库节点」这些具体类型,它只认 NodeAdaptor(转换)和 NodeBuilder(实例化)两个接口(internal/nodes/node.go:152)。加节点 = 实现这俩接口 + 注册一行,不动引擎。

如果节点有 Blocks(是循环/批处理这类复合节点),会递归把内层子节点也转成 NodeSchema,并记录一张层级表 hierarchy(子节点 key → 父节点 key),to_schema.go:241-260

③ 端口归一化

前端的分支端口名很口语化:选择器是 true / false / true_2normalizePortsto_schema.go:157)把它们翻译成引擎统一的格式(branch_schema.go:29):

前端端口归一化后含义
truebranch_0第 1 个条件命中
true_2branch_2第 3 个条件命中
falsedefault都不命中走「否则」
branch_errorbranch_error节点出错走异常分支

循环/批处理那些内部接线端口(loop-output 等)则被直接清掉端口名,当成普通连线(to_schema.go:171)。

④ 构建分支表

BuildBranchesbranch_schema.go:44)扫描所有「带端口」的连线,按源节点聚合成 BranchSchema:哪个端口(branch_N / default / branch_error)连向哪些目标节点。运行时选择器算出选了第几个分支,就查这张表决定往哪走(细节见 02 章 和分支条件 GetFullBranchbranch_schema.go:125)。

1.4 精华:批处理「展开」成父+内层

这是本章最巧的一处。用户在画布上对某个普通节点(比如 LLM)打开「批处理」开关,意思是「对一个数组的每个元素都跑一遍这个 LLM」。后端不为「批处理」单独写一种执行逻辑,而是把它改写成一个复合结构

用户视角: 后端改写后(parseBatchMode, to_schema.go:447):
┌───────────┐ ┌─────────── Batch 父节点 ───────────┐
│ LLM │ │ 输入:要遍历的数组 + 批大小/并发数 │
│ [批处理✓] │ ───► │ ┌──────────────────────────────┐ │
│ │ │ │ LLM_inner(原节点,单次逻辑) │ │
└───────────┘ │ └──────────────────────────────┘ │
│ 输出:把每次结果聚合成一个数组列表 │
└────────────────────────────────────┘

parseBatchModeto_schema.go:447)干的事:

  • 把原节点降级成内层节点 <id>_inner,只保留「单次」逻辑(to_schema.go:553)。
  • 造一个 NodeTypeBatch 父节点,配上批大小 BatchSize / 并发数 ConcurrentSize,并把内层输出包装成一个「列表」字段映射(to_schema.go:503)。
  • 用两条特殊内部连线(batch-function-inline-output / batch-function-inline-input)把父子接起来(to_schema.go:575)。

这样「批处理」就复用了和「循环」完全相同的**复合节点(CompositeNode)**机制——一个父节点 + 一个内层子工作流,统一交给编译器处理。

1.5 收尾:Init 预计算

规整完,WorkflowSchema.Init()workflow_schema.go:70)做一次性预计算并用 sync.Once 锁住:建节点索引 nodeMap、找出所有复合节点、递归 Init 子工作流、判定「是否需要检查点」requireCheckPoint(有问答/循环这类会中断的节点就需要)、判定「是否需要流式」requireStreaming。这些结论会指导下一步编译。

下一章: schema 已就绪,02-compile-to-eino-graph.md 讲它怎样编译成 eino 数据流图——本库真正的核心。

代码地图

主题文件符号
转换主流程internal/canvas/adaptor/to_schema.goCanvasToWorkflowSchema
单节点转换internal/canvas/adaptor/to_schema.goNodeToNodeSchema
节点注册总清单internal/canvas/adaptor/to_schema.goRegisterAllNodeAdaptors
剪枝孤立节点internal/canvas/adaptor/to_schema.goPruneIsolatedNodes
端口归一化internal/canvas/adaptor/to_schema.gonormalizePorts
批处理展开internal/canvas/adaptor/to_schema.goparseBatchMode
分支表构建internal/schema/branch_schema.goBuildBranches
适配器/构建器接口internal/nodes/node.goNodeAdaptorGetNodeAdaptor
节点类型清单entity/node_meta.goNodeTypeMetasIDStrToNodeType
schema 预计算internal/schema/workflow_schema.goWorkflowSchema.Init