系列「企业级 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 包定义了一个编译回调接口:

// compose/introspect.go
type GraphCompileCallback interface {
    OnFinish(ctx context.Context, info *GraphInfo)
}

OnFinish 在每次图编译成功后被调用。GraphInfo 包含编译时能拿到的一切:

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 可视化复杂的原因。

注册回调:

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 的边,控制流和数据流几乎总是重合,用最简单的箭头:

START

prompt
(ChatTemplate)

model
(ChatModel)

END

Workflow 专门把三种语义用箭头样式区分开:

// 控制 + 数据(最常见)
start_node -- control+data --> b1

// 只有控制(AddDependency 产生,节点要等前驱完成,但不接收数据)
b1 == control-only ==> announcer

// 只有数据(控制流不走这条边,但数据会从这里过来)
start_node -. data-only .-> b2

自动检测逻辑:如果 DataEdgesEdges 完全一致,说明是 Graph/Chain,用简单箭头;否则用带标签的 Workflow 样式。

分支条件用菱形表示:

渲染错误: Mermaid 渲染失败: No diagram type detected matching given configuration for text: b1_branch_0{"branch"} b1 ==> b1_branch_0 b1_branch_0 ==> end_node b1_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 全串在一起,生成图里会出现:

  • 分支菱形节点(b1b2 两个出口)
  • 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 CLI
npm install -g @mermaid-js/mermaid-cli
# 手动渲染
mmdc -i topology.mmd -o topology.png

没有 mmdc 就自动退化到 headless Chrome(chromedp),用浏览器渲染 SVG 再截图。


七、不想生成文件?用 mermaid.live

最快的方式:把 .md 文件里的 mermaid 代码块内容粘贴到 https://mermaid.live,实时渲染,支持导出 PNG/SVG,不需要装任何工具。

开发调试阶段常用的工作流:

1. 编译时生成 .md 文件
2. 打开 mermaid.live
3. 粘贴 mermaid 代码块内容
4. 立刻看到拓扑图
5. 调整节点顺序 / 边关系,重新 Compile
6. 刷新 mermaid.live

CI 里要生成可存档的图片才需要 mmdc。


八、自己实现一个 GraphCompileCallback

MermaidGenerator 能做的事,其他工具也能做——只要实现 GraphCompileCallback

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 代码时就能看到拓扑,不用再脑补。

下篇继续。


代码来源:cloudwego/eino-examples · cloudwego/eino

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐