流程可视化:把 Eino 编排图转为 Mermaid 图表
Eino复杂Agent的图构建代码超过50行后,执行顺序变得难以直观理解。本文介绍利用GraphCompileCallback接口在编译阶段生成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 |
| Lambda 节点 | 圆角 (...) | step("step |
| 嵌套 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
(ChatTemplate)"]model["model
(ChatModel)"]end_node([END])start_node --> promptprompt --> modelmodel --> end_nodeWorkflow则专门用箭头样式区分三种语义:
// 控制 + 数据(最常见)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) → ENDChain(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触发时会自动写入两个文件:
| 文件 | 内容 |
|---|---|
.md | ```mermaid代码块,可直接粘贴到GitHub/Obsidian |
.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代码时就能看到拓扑,免去脑补。