流程可视化:把 Eino 编排图转换为 Mermaid 图表
读完这篇你会知道
一、问题:编排代码越来越难读
用 Eino 写复杂 Agent 的人都遇到过这个问题:图构建代码超过 50 行之后,光靠读代码已经很难快速理解节点之间的执行顺序。
Graph 还好,Chain 有了条件分支和并行节点之后,Workflow 又引入了控制流和数据流分离——大脑要在"代码顺序"和"执行顺序"之间反复切换。
解决方案直接:编译时生成一张图。
二、入口:GraphCompileCallback 接口
Eino 的 compose 包定义了一个编译回调接口:
// compose/introspect.gotype GraphCompileCallback interface {OnFinish(ctx context.Context, info *GraphInfo)}
OnFinish 在每次图编译成功后被调用。GraphInfo 包含编译时能拿到的一切:
type GraphInfo struct {NamestringNodes map[string]GraphNodeInfo // key → 节点信息(组件类型、是否嵌套图等)Edges map[string][]string// 控制边:起点 → 终点列表DataEdges map[string][]string// 数据边:起点 → 终点列表Branchesmap[string][]GraphBranch // 条件分支:起点 → 分支列表// ...}
关键区别:Edges 是控制边(决定执行顺序),DataEdges 是数据边(决定数据流向)。Graph/Chain 里两者通常重合,Workflow 里它们可以不同——这也是 Workflow 可视化复杂的原因。
注册回调:
gen := visualize.NewMermaidGenerator("output/dir")runner, err := g.Compile(ctx,compose.WithGraphCompileCallbacks(gen),compose.WithGraphName("MyGraph"),)
Compile 内部调用 gen.OnFinish(ctx, info),图的完整信息就传进来了。
三、MermaidGenerator 的核心逻辑
devops/visualize/mermaid.go 里的实现分四步:
收集所有节点(Nodes + Edges + Branches 里出现的)↓渲染节点(普通节点 / Lambda / 嵌套子图)↓渲染边(控制边 + 数据边,按 Workflow 模式决定是否加标签)↓渲染分支(菱形决策节点 + 出边)
节点形状规则:
| 节点类型 | Mermaid 形状 | 示例 |
|---|---|---|
| 普通节点 | 方形 [...] | model["model<br/>(ChatModel)"] |
| Lambda 节点 | 圆角 (...) | step("step<br/>(Lambda)") |
| 嵌套 Graph/Chain/Workflow | subgraph | 递归渲染 |
| START / END | 椭圆 ([...]) | start_node([START]) |
START 和 END 被重命名为 start_node / end_node——因为 end 是 Mermaid 关键字,直接用会破坏语法。
四、三种边的语义
Graph 和 Chain 的边,控制流和数据流几乎总是重合,用最简单的箭头:
graph TDstart_node([START])prompt["prompt<br/>(ChatTemplate)"]model["model<br/>(ChatModel)"]end_node([END])start_node --> promptprompt --> modelmodel --> end_node
Workflow 专门把三种语义用箭头样式区分开:
// 控制 + 数据(最常见)start_node -- control+data --> b1// 只有控制(AddDependency 产生,节点要等前驱完成,但不接收数据)b1 == control-only ==> announcer// 只有数据(控制流不走这条边,但数据会从这里过来)start_node -. data-only .-> b2
自动检测逻辑:如果 DataEdges 和 Edges 完全一致,说明是 Graph/Chain,用简单箭头;否则用带标签的 Workflow 样式。
分支条件用菱形表示:
b1_branch_0{"branch"}b1 ==> b1_branch_0b1_branch_0 ==> end_nodeb1_branch_0 ==> b2
五、三种编排的实际效果
Graph(compose/graph/simple)
g := compose.NewGraph[map[string]any, *schema.Message]()g.AddChatTemplateNode("prompt", pt)g.AddChatModelNode("model", cm)g.AddEdge(compose.START, "prompt")g.AddEdge("prompt", "model")g.AddEdge("model", compose.END)gen := visualize.NewMermaidGenerator("compose/graph/simple")g.Compile(ctx, compose.WithGraphCompileCallbacks(gen), compose.WithGraphName("SimpleGraph"))
生成:
START → prompt(ChatTemplate) → model(ChatModel) → END
Chain(compose/chain)
Chain 把 Lambda、Branch、Passthrough、Parallel、嵌套 Graph 全串在一起,生成图里会出现:
- 分支菱形节点(
b1和b2两个出口) - Parallel 展开为多个并行节点
- 嵌套的
rolePlayerChain展开为subgraph
chain.Compile(ctx, compose.WithGraphCompileCallbacks(visualize.NewMermaidGenerator("compose/chain"),), compose.WithGraphName("chain"))
Workflow(compose/workflow/4_control_only_branch)
这个例子故意展示控制流和数据流分离:
wf.AddLambdaNode("b1", ...).AddInput(compose.START)// announcer 只依赖 b1 完成,不接收 b1 的输出wf.AddLambdaNode("announcer", ...).AddDependency("b1")// b2 的数据来自 START,但控制流由 b1 的分支决定wf.AddLambdaNode("b2", ...).AddInputWithOptions(compose.START, nil, compose.WithNoDirectDependency())gen := visualize.NewMermaidGenerator("compose/workflow/4_control_only_branch")wf.Compile(ctx,compose.WithGraphCompileCallbacks(gen),compose.WithGraphName("Workflow-Control-Only-Branch"),)
生成图里,b1 → announcer 是粗箭头(== control-only ==>),START → b2 是虚线箭头(-. data-only .->),一眼就能看出哪条边只传控制、哪条边只传数据。
六、输出文件和渲染
NewMermaidGenerator(dir) 创建后,Compile 触发时自动写两个文件:
| 文件 | 内容 |
|---|---|
<GraphName>.md | ```mermaid 代码块,可以直接粘贴到 GitHub/Obsidian |
<GraphName>.png | 渲染好的图片 |
PNG 渲染优先找 mmdc(官方 CLI):
# 安装 mermaid CLInpm install -g @mermaid-js/mermaid-cli# 手动渲染mmdc -i topology.mmd -o topology.png
没有 mmdc 就自动退化到 headless Chrome(chromedp),用浏览器渲染 SVG 再截图。
七、不想生成文件?用 mermaid.live
最快的方式:把 .md 文件里的 mermaid 代码块内容粘贴到 mermaid.live,实时渲染,支持导出 PNG/SVG,不需要装任何工具。
开发调试阶段常用的工作流:
1. 编译时生成 .md 文件2. 打开 mermaid.live3. 粘贴 mermaid 代码块内容4. 立刻看到拓扑图5. 调整节点顺序 / 边关系,重新 Compile6. 刷新 mermaid.live
CI 里要生成可存档的图片才需要 mmdc。
八、自己实现一个 GraphCompileCallback
MermaidGenerator 能做的事,其他工具也能做——只要实现 GraphCompileCallback:
type myLogger struct{}func (l *myLogger) OnFinish(_ context.Context, info *compose.GraphInfo) {fmt.Printf("图名: %sn", info.Name)fmt.Printf("节点数: %dn", len(info.Nodes))for start, ends := range info.Edges {for _, end := range ends {fmt.Printf("%s → %sn", start, end)}}}// 接入g.Compile(ctx, compose.WithGraphCompileCallbacks(&myLogger{}))
想往 CI artifact 里写 JSON、或者把图结构上报到监控系统——都是这个接口。
小结
GraphCompileCallback 是 Eino 编译阶段的唯一扩展点,GraphInfo 里包含节点、控制边、数据边、分支的完整结构。
MermaidGenerator 把它变成 Mermaid 语法:普通节点方形,Lambda 圆角,嵌套图展开为 subgraph,START/END 用椭圆并重命名避开关键字冲突。Graph/Chain 用简单箭头,Workflow 自动区分三种边语义(control+data、control-only、data-only)。
接入只需两行:NewMermaidGenerator(dir) + WithGraphCompileCallbacks(gen)。看图用 mermaid.live,存档用 mmdc。
开发期把图粘进 PR 描述,其他人 review 代码时就能看到拓扑,不用再脑补。
下篇继续。
代码来源:cloudwego/eino-examples · cloudwego/eino
-
07.03
梦幻西游华光玉之伤任务怎么完成-华光玉之伤任务的领取条件
-
07.03
梦幻西游炼兽笼与烧双速度对比-不同等级宝宝选择建议
-
07.03
《梦幻西游》坐骑怎么快速升级-快速提升坐骑等级的技巧
-
07.03
梦幻西游花草种子怎么获取-花草种子的种植方法及获取途径
-
07.03
《梦幻西游》如何快速找到各门派入口-各门派起点坐标和传送方法
-
07.03
DNF究极能量誓约套装数据总览
-
-
下载
- 《神剑伏魔录》(神剑风云)游戏音乐合集
- 其他游戏|7.73 MB
- 一款非常好玩的武侠闯关游戏
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- 《生化危机:浣熊市行动》免安装中文硬盘版下载
- 单机|6310 MB
- 一款以动作射击为主题的游戏
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏