系列「企业级 AI Agent 实现拆解」E26 篇。上一篇讲了 MCP 工具集成:外部工具变 Eino Tool。这篇讲流程可视化------Graph、Chain、Workflow 编好之后,怎么自动生成一张能看懂拓扑结构的 Mermaid 图。
读完这篇你会知道
GraphCompileCallback接口:编译时回调拿到完整图结构GraphInfo里有什么:节点、控制边、数据边、分支MermaidGenerator怎么把这些信息变成 Mermaid 语法- Graph/Chain 和 Workflow 的边为什么要区别对待
- 三种箭头的语义:
-->、== control-only ==>、-. data-only .->- 输出文件:
.md+.png,mmdc 没装就 chromedp 降级- 一行接入,两行渲染到 mermaid.live
一、问题:编排代码越来越难读
用 Eino 写复杂 Agent 的人都遇到过这个问题:图构建代码超过 50 行之后,光靠读代码已经很难快速理解节点之间的执行顺序。
Graph 还好,Chain 有了条件分支和并行节点之后,Workflow 又引入了控制流和数据流分离------大脑要在"代码顺序"和"执行顺序"之间反复切换。
解决方案直接:编译时生成一张图。
二、入口:GraphCompileCallback 接口
Eino 的 compose 包定义了一个编译回调接口:
go
// compose/introspect.go
type GraphCompileCallback interface {
OnFinish(ctx context.Context, info *GraphInfo)
}
OnFinish 在每次图编译成功后被调用。GraphInfo 包含编译时能拿到的一切:
go
type GraphInfo struct {
Name string
Nodes map[string]GraphNodeInfo // key → 节点信息(组件类型、是否嵌套图等)
Edges map[string][]string // 控制边:起点 → 终点列表
DataEdges map[string][]string // 数据边:起点 → 终点列表
Branches map[string][]GraphBranch // 条件分支:起点 → 分支列表
// ...
}
关键区别:Edges 是控制边(决定执行顺序),DataEdges 是数据边(决定数据流向)。Graph/Chain 里两者通常重合,Workflow 里它们可以不同------这也是 Workflow 可视化复杂的原因。
注册回调:
go
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 里的实现分四步:
markdown
收集所有节点(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 的边,控制流和数据流几乎总是重合,用最简单的箭头:
Workflow 专门把三种语义用箭头样式区分开:
go
// 控制 + 数据(最常见)
start_node -- control+data --> b1
// 只有控制(AddDependency 产生,节点要等前驱完成,但不接收数据)
b1 == control-only ==> announcer
// 只有数据(控制流不走这条边,但数据会从这里过来)
start_node -. data-only .-> b2
自动检测逻辑:如果 DataEdges 和 Edges 完全一致,说明是 Graph/Chain,用简单箭头;否则用带标签的 Workflow 样式。
分支条件用菱形表示:
五、三种编排的实际效果
Graph(compose/graph/simple)
go
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"))
生成:
scss
START → prompt(ChatTemplate) → model(ChatModel) → END
Chain(compose/chain)
Chain 把 Lambda、Branch、Passthrough、Parallel、嵌套 Graph 全串在一起,生成图里会出现:
- 分支菱形节点(
b1和b2两个出口) - Parallel 展开为多个并行节点
- 嵌套的
rolePlayerChain展开为subgraph
go
chain.Compile(ctx, compose.WithGraphCompileCallbacks(
visualize.NewMermaidGenerator("compose/chain"),
), compose.WithGraphName("chain"))
Workflow(compose/workflow/4_control_only_branch)
这个例子故意展示控制流和数据流分离:
go
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):
bash
# 安装 mermaid CLI
npm 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,不需要装任何工具。
开发调试阶段常用的工作流:
markdown
1. 编译时生成 .md 文件
2. 打开 mermaid.live
3. 粘贴 mermaid 代码块内容
4. 立刻看到拓扑图
5. 调整节点顺序 / 边关系,重新 Compile
6. 刷新 mermaid.live
CI 里要生成可存档的图片才需要 mmdc。
八、自己实现一个 GraphCompileCallback
MermaidGenerator 能做的事,其他工具也能做------只要实现 GraphCompileCallback:
go
type myLogger struct{}
func (l *myLogger) OnFinish(_ context.Context, info *compose.GraphInfo) {
fmt.Printf("图名: %s\n", info.Name)
fmt.Printf("节点数: %d\n", len(info.Nodes))
for start, ends := range info.Edges {
for _, end := range ends {
fmt.Printf(" %s → %s\n", 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 代码时就能看到拓扑,不用再脑补。
下篇继续。