08Workflow 实战用字段映射表达业务流程《Eino 实战用 Go 构建 AI 应用与智能体》· 第 08 篇关键词compose.Workflow、DAG、MapFields、字段级依赖、并行前置条件已完成 07Graph 实战。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同时表达了两件事执行依赖——merge必须等classify和keywords都完成后再执行。数据映射——把上游输出的某个字段映射到本节点输入的某个字段。因此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(contextfmtlogstringsgithub.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.Linein.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 日Workflow 编排框架Chain/Graph 编排介绍Eino 主仓库本文示例按Eino v0.9.21编写实机运行正文输出为真实结果。文中“字段冲突”报错的原文也来自实际运行。