数据截至 (上游 commit fefb05ff27be)
第 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:有ID、Type(数字字符串,如"3"表示 LLM)、Data(标题、输入参数、输出定义、各节点专属配置),复合节点还有Blocks(内层子节点)和Edges(内层连线)。 - 连线
vo.Edge:SourceNodeID/TargetNodeID,以及可选的SourcePortID(用于分支,如选择器的true/false端口)。
注意一个细节:节点 Type 在画布里是数字 ID 字符串("1"=开始、"2"=结束、"3"=LLM……),后端用 entity.IDStrToNodeType 把它映射回内部的 NodeType 字符串常量(node_meta.go:35、:259 的 NodeTypeMetas 是这张映射表的单一真相源)。
1.3 主流程:CanvasToWorkflowSchema
入口是 to_schema.go:66 的 CanvasToWorkflowSchema。它做四件事,依次进行:
画布 JSON (vo.Canvas)
│
├─① PruneIsolatedNodes ── 剪掉没连到任何边的孤立节点
│
├─② 逐节点 NodeToNodeSchema ── 每个节点 → 一个或多个 NodeSchema
│ (复合节点展开成 父NodeSchema + 内层NodeSchema[] + 层级表)
│
├─③ normalizePorts ── 把前端端口名(true/false/true_2)归一成 branch_N/default
│
└─④ BuildBranches ── 从带端口的连线里构建分支表
│
▼
WorkflowSchema(Nodes + Connections + Hierarchy + Branches)
① 剪掉孤立节点
PruneIsolatedNodes(to_schema.go:396)给每个节点算「入度」:开始/结束节点预设为 1(永远保留),其余节点看有没有边指向它。入度为 0 的节点和它发出的边一并删掉。这样用户在画布上随手拖了又没连的节点不会进入执行。
② 每个节点 → NodeSchema
NodeToNodeSchema(to_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:594 的 RegisterAllNodeAdaptors——这是「有哪些节点」的总清单(开始/结束/选择器/LLM/插件/代码/循环/批处理/数据库/知识库/会话……约 50 个)。想加新节点,就在这里注册一个适配器。
巧妙之处:注册表 + 适配器接口。 引擎核心完全不认识「LLM 节点」「数据库节点」这些具体类型,它只认
NodeAdaptor(转换)和NodeBuilder(实例化)两个接口(internal/nodes/node.go:152)。加节点 = 实现这俩接口 + 注册一行,不动引擎。
如果节点有 Blocks(是循环/批处理这类复合节点),会递归把内层子节点也转成 NodeSchema,并记录一张层级表 hierarchy(子节点 key → 父节点 key),to_schema.go:241-260。
③ 端口归一化
前端的分支端口名很口语化:选择器是 true / false / true_2。normalizePorts(to_schema.go:157)把它们翻译成引擎统一的格式(branch_schema.go:29):
| 前端端口 | 归一化后 | 含义 |
|---|---|---|
true | branch_0 | 第 1 个条件命中 |
true_2 | branch_2 | 第 3 个条件命中 |
false | default | 都不命中走「否则」 |
branch_error | branch_error | 节点出错走异常分支 |
循环/批处理那些内部接线端口(loop-output 等)则被直接清掉端口名,当成普通连线(to_schema.go:171)。
④ 构建分支表
BuildBranches(branch_schema.go:44)扫描所有「带端口」的连线,按源节点聚合成 BranchSchema:哪个端口(branch_N / default / branch_error)连向哪些目标节点。运行时选择器算出选了第几个分支,就查这张表决定往哪走(细节见 02 章 和分支条件 GetFullBranch,branch_schema.go:125)。
1.4 精华:批处理「展开」成父+内层
这是本章最巧的一处。用户在画布上对某个普通节点(比如 LLM)打开「批处理」开关,意思是「对一个数组的每个元素都跑一遍这个 LLM」。后端不为「批处理」单独写一种执行逻辑,而是把它改写成一个复合结构:
用户视角: 后端改写后(parseBatchMode, to_schema.go:447):
┌───────────┐ ┌─────────── Batch 父节点 ───────────┐
│ LLM │ │ 输入:要遍历的数组 + 批大小/并发数 │
│ [批处理✓] │ ───► │ ┌──────────────────────────────┐ │
│ │ │ │ LLM_inner(原节点, 单次逻辑) │ │
└───────────┘ │ └──────────────────────────────┘ │
│ 输出:把每次结果聚合成一个数组列表 │
└────────────────────────────────────┘
parseBatchMode(to_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.go | CanvasToWorkflowSchema |
| 单节点转换 | internal/canvas/adaptor/to_schema.go | NodeToNodeSchema |
| 节点注册总清单 | internal/canvas/adaptor/to_schema.go | RegisterAllNodeAdaptors |
| 剪枝孤立节点 | internal/canvas/adaptor/to_schema.go | PruneIsolatedNodes |
| 端口归一化 | internal/canvas/adaptor/to_schema.go | normalizePorts |
| 批处理展开 | internal/canvas/adaptor/to_schema.go | parseBatchMode |
| 分支表构建 | internal/schema/branch_schema.go | BuildBranches |
| 适配器/构建器接口 | internal/nodes/node.go | NodeAdaptor、GetNodeAdaptor |
| 节点类型清 单 | entity/node_meta.go | NodeTypeMetas、IDStrToNodeType |
| schema 预计算 | internal/schema/workflow_schema.go | WorkflowSchema.Init |