ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

08|Workflow 实战:用字段映射表达业务流程 - eino框架

08|Workflow 实战:用字段映射表达业务流程 - eino框架

08|Workflow 实战:用字段映射表达业务流程

《Eino 实战:用 Go 构建 AI 应用与智能体》· 第 08 篇
关键词:compose.Workflow、DAG、MapFields、字段级依赖、并行
前置条件:已完成 07|Graph 实战。

Graph 用“节点 + 边”描述流程,直观且灵活。但它有个副作用:当节点变多、数据来源变复杂时,整块传递会变得含糊——下游到底需要上游的哪部分数据?只能翻代码才知道。

Workflow 换了个角度:不描述“谁连谁”,而描述数据依赖。它面向有向无环图(DAG),并支持在结构体字段级别做映射。


一、核心差异:声明依赖,而不是连线

对比两种写法:

// Graph:显式连线_=g.AddEdge("classify","merge")_=g.AddEdge("keywords","merge")// Workflow:声明依赖关系,并指定字段映射node.AddInput("classify",compose.MapFields("Category","Category")).AddInput("keywords",compose.MapFields("Keywords","Keywords"))

AddInput同时表达了两件事:

  1. 执行依赖——merge必须等classify和keywords都完成后再执行。
  2. 数据映射——把上游输出的某个字段,映射到本节点输入的某个字段。

因此,Workflow 里的并行是自动的:只要两个节点之间没有依赖声明,它们就可以同时执行;一旦都被某个下游AddInput,它们会在那里汇聚。


二、MapFields:字段级映射

compose.MapFields(from,to)
  • from是上游输出结构体中的字段名。
  • to是本节点输入结构体中的字段名。

例如:

AddInput("classify",compose.MapFields("Category","Category"))

含义是“把 classify 节点输出的Category字段,填入本节点的Category字段”。

这个设计的好处是:依赖关系写在字段上,代码读起来就是数据流。想知道某个字段从哪来,搜索MapFields的目标字段即可。

需要注意一条约束:同一个输入字段不能被多个上游同时映射。如果两个MapFields的第二个参数都写成"Line",编译会直接报错:

two terminal field paths conflict for node merge: [Line], [Line]

这是实际踩到的错误(见下方示例)。含义很明确:一个字段只能有一个来源,需要合并多个上游的数据时,应分别映射到不同字段,再在节点内部组合。


三、完整示例

下面这段程序不访问网络,可直接运行。它把“分类”和“提取关键词”两个互不依赖的节点并行执行,再汇聚到一个节点:

packagemainimport("context""fmt""log""strings""github.com/cloudwego/eino/compose")// TicketInput 是工作流的整体输入。typeTicketInputstruct{Rawstring}// ClassifyOutput 只提供分类字段。typeClassifyOutputstruct{Categorystring}// ExtractOutput 只提供关键词字段。typeExtractOutputstruct{Keywordsstring}// SummaryOutput 是最终汇总结果。typeSummaryOutputstruct{CategorystringKeywordsstringLinestring}funcmain(){iferr:=run();err!=nil{log.Fatal(err)}}funcrun()error{ctx:=context.Background()wf:=compose.NewWorkflow[TicketInput,SummaryOutput]()// 分类节点:读取输入的 Raw 字段。wf.AddLambdaNode("classify",compose.InvokableLambda(func(_context.Context,in TicketInput)(ClassifyOutput,error){ifstrings.Contains(in.Raw,"打印")||strings.Contains(in.Raw,"离线"){returnClassifyOutput{Category:"hardware"},nil}returnClassifyOutput{Category:"other"},nil},)).AddInput(compose.START)// 关键词节点:同样读取 Raw 字段,可与分类节点并行。wf.AddLambdaNode("keywords",compose.InvokableLambda(func(_context.Context,in TicketInput)(ExtractOutput,error){returnExtractOutput{Keywords:"打印机,离线"},nil},)).AddInput(compose.START)// 汇总节点:用字段映射分别从上游取字段,组装成自己的输入。wf.AddLambdaNode("merge",compose.InvokableLambda(func(_context.Context,in SummaryOutput)(SummaryOutput,error){in.Line=in.Category+" | "+in.Keywordsreturnin,nil},)).AddInput("classify",compose.MapFields("Category","Category")).AddInput("keywords",compose.MapFields("Keywords","Keywords"))wf.End().AddInput("merge")runnable,err:=wf.Compile(ctx)iferr!=nil{returnfmt.Errorf("编译 Workflow 失败: %w",err)}out,err:=runnable.Invoke(ctx,TicketInput{Raw:"三楼打印机离线,无法出纸"})iferr!=nil{returnfmt.Errorf("执行 Workflow 失败: %w",err)}fmt.Printf("Workflow 输出: %+v\n",out)returnnil}

实机输出:

Workflow 输出: {Category:hardware Keywords:打印机,离线 Line:hardware | 打印机,离线}

可以看到,merge节点的输入同时拿到了来自classify的Category和来自keywords的Keywords,节点内部再把它们组合成Line。


四、几个关键约定

1. 数据依赖即执行依赖。
没有AddInput声明的节点不会被执行,也不会自动连到流程里。每个节点都要能找到从START出发的依赖路径。

2. 输入输出必须能映射。
MapFields的字段名基于结构体字段,写错会在编译期报错或映射不到值。这比运行时才发现问题好得多。

3. 一个字段一个来源。
同一个输入字段被多次映射会直接冲突报错,需重新设计结构体字段。

4. Workflow 无环。
它是 DAG,不支持循环。需要回退重试时用 Graph(第 07 篇)。

5. 并行是隐式的。
没有互相依赖的节点会被并行调度,不必显式声明。这既是优点也有代价:并行度不直观,需要通过依赖关系推断。


五、Workflow、Graph、Chain 怎么选

场景推荐理由
顺序处理,步骤少Chain最简洁
需要循环、复杂动态分支Graph支持环,控制力最强
多节点、依赖关系明确、字段来源多样Workflow字段级映射让数据流可读
需要向模型/工具传整块消息列表Graph 或 ChainWorkflow 偏向结构化字段

还有一点要再次强调,因为概念名称容易混:compose.Workflow是组件编排 API,而 ADK 里的SequentialAgent、ParallelAgent、LoopAgent是智能体组合方式(第四、五篇展开)。它们不是同一个东西,也不是彼此的别名。


六、常见问题

编译报字段路径冲突
同一个输入字段被多个上游映射。改用不同目标字段,或在节点内部合并。

节点没有执行
缺少AddInput依赖声明,节点没有被纳入执行链。

字段映射后取到零值
MapFields的字段名与本节点结构体字段名不匹配,或上游没有填充该字段。

想要执行顺序,但不想传数据
可以用AddDependency之类的方式只表达依赖。若确实只是“先后执行”,用 Graph 的AddEdge反而更直观。

上游需要整块结构,而不是某几个字段
AddInput不传MapFields时表达的是整块依赖。具体行为以官方文档为准,涉及整块传递时建议先用小示例验证。


七、小结

  • Workflow 面向 DAG,用AddInput声明执行依赖与字段映射。
  • MapFields(from, to)描述字段级数据流,让依赖关系可读。
  • 无依赖关系的节点自动并行,无需显式声明。
  • 一个输入字段只能有一个来源,冲突会在编译期报错。
  • Workflow 不支持环;需要重试回退时用 Graph。

下一篇《流式编程:从模型分片到完整业务结果》,我们处理 Eino 中最现实也最容易出错的一环:流。


参考资料与版本说明

资料核对日期为2026 年 10 月 9 日:

  1. Workflow 编排框架
  2. Chain/Graph 编排介绍
  3. Eino 主仓库

本文示例按Eino v0.9.21编写,实机运行,正文输出为真实结果。文中“字段冲突”报错的原文也来自实际运行。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进