
0. 前言一个绕不开的编排问题先抛一个我在对接企业级 AI 应用时经常被问到的问题项目里已经写了一堆 Chain后来为了处理并行分支又上了 Graph结果发现真到了业务落地阶段面临的场景还是“两个输入谁先到我就先跑谁”“这批子任务一起跑完再汇总”“跑完不满意就再重试一轮”用 Chain 和 Graph 都能硬凑但代码越来越丑排查越来越难。这个疑惑在 Eino 生态里尤其突出因为 Eino 的编排层同时提供了 Chain、Graph、Workflow 三种模型官方文档写得很简洁但真要选型时没人告诉你边界在哪。这篇文章就是围绕这个主题来写的Chain、Graph、Workflow 到底是什么各自解决什么阶段的问题以及为了一套真实业务场景为什么最终方案里我换成了 Workflow。整个系列面向正在做 AI 大模型应用落地的开发者不绕概念直接讲编排模型背后的执行机制和适用边界配上可跑的伪代码和踩坑记录希望能帮你省掉几个晚上的试错时间。先说结论Chain 的本质是“固定先后顺序的线性管道”Graph 的本质是“无环的并发分支与聚合”Workflow 的本质则是“带运行时决策的复杂流程编排”。三者不是替代关系而是你在不同复杂度阶段会依次遇到的三种工具。1. 内容整体设计与思路拆解1.1 Chain 和 Graph 各自解决了什么Chain 在 AI 应用里对应的是最朴素的“把一个大任务拆成依次执行的小步骤”。比如做一个文档摘要管线先读取文档然后做文本切分再把每个切片向量化最后调用大模型生成摘要。这个流程图是线性的前一步的输出当后一步的输入中间没有分支没有并发用 Chain 表达最自然。Chain 解决的核心问题是责任拆分。每个节点只关心一个职责不需要知道上下游的细节节点之间通过统一的参数对象传递数据。我在实际项目里最喜欢 Chain 的一点是它的调试链路非常清晰——日志按顺序打参数按顺序传一旦某一步出问题直接看那一步的输入输出就能定位不需要理解复杂的并发逻辑。Graph 解决的则是分支和并发问题。真实的大模型应用几乎不可能一路线性跑到底最常见的场景就是“并行调用多个不同模型然后让一个聚合节点把结果合并”。比如做信息抽取时我可以让一个节点抽实体一个节点抽关系一个节点抽情感倾向三个节点并行跑全部完成后统一交给结果整理节点。Graph 用有向无环图来建模这种关系每个节点可以声明依赖哪些上游节点执行器通过拓扑排序来决定先跑谁后跑谁没有依赖关系的节点天然并发。Graph 真正的价值在于它把“分支”纳入了编排模型而不是靠代码里的 if-else 硬写。每个节点从 Graph 的状态对象中读取它关心的字段处理完后再把结果写回状态对象。这种数据流和用于协调的 if-else 解耦的设计让并发分支的代码仍然可读、可维护。1.2 Workflow 出现前的真实痛点如果业务只需要“固定拓扑 并行分支”Chain Graph 已经能覆盖绝大多数需求。但我在落地知识库问答、Agent 任务流这类场景时慢慢发现几个 Graph 解决不了的问题。第一个痛点是“下一步去哪”这件事不应该由写代码的人提前决定。对话式 Agent 里用户的输入可能命中知识库也可能命中工具调用还有可能直接让模型自由回答。到底走哪条路需要在大模型返回结果之后根据结果内容来动态决定。Graph 虽然支持条件分支但它的条件判断本质上还是要提前把分支路径都定义清楚而且分支判断逻辑在图上表达有限复杂条件需要嵌套多个 Compile 期指令代码很快就变得不直观。第二个痛点是循环。Graph 的 DAG 结构天然不支持环而真实业务里“重试”“修正”“迭代生成”都是环状的。比如生成文案后让评分节点打分分数不够就回到生成节点再来一轮这种循环在 DAG 里是没法直接表达的通常只能靠外部包装一个循环驱动层把 Graph 包在里面反复调用。这样做不是不行但状态管理会变得分散Graph 只管一轮执行循环逻辑在外面中间状态的保存和传递都靠全局变量量大了之后很难维护。第三个痛点是需要表达“多实例并行”。同一个子流程要跑 10 遍每遍的输入参数来自不同的上游结果这种 map 结构在 Graph 里要写 10 个重复子图或者用代码循环方式把子图复制 10 次不仅编译慢每次修改都要改一堆节点 ID。这三个痛点集中出现的时候Workflow 的存在就非常合理了。2. 核心细节解析与实操要点2.1 Chain、Graph、Workflow 的执行模型对比要真正理解三者的差异光看概念不够要看执行引擎怎么处理节点。我整理了一个表格方便对照特性维度ChainGraphWorkflow拓扑结构线性链表有向无环图有向图支持环节点执行顺序严格按照声明顺序按依赖关系拓扑排序按依赖关系和运行时消息驱动是否支持并发否是无依赖节点并行执行是支持分支和并行条件分支不支持需外部代码支持编译期静态定义的预编译分支运行时基于节点输出动态选择循环不支持不支持支持通过 Edge 循环实现数据传递单一上下文结构体逐级传递Graph State 读写Workflow State 读写 分支入参映射多实例并行不支持需手动复制子图原生支持 Branch 与 Mapper调试难度最低中等较高依赖可视化工具适用场景固定顺序流水线稳定的并行分支聚合动态决策、循环、子任务并行Workflow 在 Eino 里的实现思路是把“图”这个概念拆分成了两套执行平面一套是声明平面负责定义节点、映射关系、分支规则另一套是执行平面负责按消息和条件动态决定到底走哪条路径。声明平面定义的是可能性执行平面决定的是实际路径两者分离之后图的表达能力立刻上了一个台阶。2.2 Workflow 的核心能力一Branch 条件分支Branch 是 Workflow 区别于 Graph 最重要的一块。Graph 也支持条件分支但它的分支目标是在 Compile 期由开发者的代码写死的比如“如果 A 为真则执行 B否则执行 C”这里的 A 在编译时已经确定运行时的动态性有限。Workflow 的 Branch 更接近“条件表达式”——你可以指定一个 Branch 的触发条件这个条件依赖于某个节点的实际输出由 Workflow 的 runtime 在每轮执行时动态求值再决定消息送入哪条后续 Edge。我实际用得比较多的地方是意图路由。用户输入进来后先用一个意图识别节点判断类型然后 Workflow 里定义三条 Edge一条指向工具调用子流程一条指向知识库检索子流程一条指向自由对话子流程。在 Graph 里这种路由需要写三个 if 分支在 Workflow 里就是给同一个节点挂三张带条件的边。这个能力在处理大模型输出不稳定的场景时尤其重要。模型返回的格式可能千奇百怪Branch 里可以写容错条件——比如 JSON 解析失败时自动走“重试”路径解析成功才走“正常”路径。这种纠错逻辑放在 Workflow 层的条件边里比放在节点代码里干净得多。2.3 Workflow 的核心能力二Loop 循环与 MapperLoop 是另一个让我觉得“少了它不行”的能力。在实现“由浅入深的多轮追问”时我需要一个节点反复调用大模型直到模型自己认为“我已经拿到了评估所需的所有信息”为止。这种循环在 Graph 里只能通过外部 while 包一层实现但循环内部的上下文维护、结果更新、终止条件判断都要自己写代码很容易变成一坨。Workflow 的 Edge 支持指回上游节点配合一个超时或条件判断节点做终止整个循环就变成图内部的语义了。Eino 的实现里Loop 的终止条件通常不是空转而是依赖某个节点的输出字段变化比如“评分 8 则跳出否则回到生成节点”这在 Workflow 里就是一条带 Condition 的回边。Mapper 解决的是多实例并行问题。它的定位相当于一个“动态子图实例化器”定义一个子流程输入是某个集合型字段Workflow 会在运行时为集合里的每一个元素创建一个独立的子流程实例这些实例并行跑完成后把结果聚合成集合写回状态。我拿这个功能处理过批量文档预处理的场景每篇文档走一个独立的“切分-向量化-入库”子流程如果用 Graph 写我这个子流程要复制 50 份用 Mapper 的话一份定义加上一个 List 字段就搞定了。2.4 三个模型在工程上的选型建议选型不是越复杂越好。根据我的经验可以用两个问题来过滤你的流程里是否存在“运行时才能确定的分支”是否存在循环或多实例如果两个答案都是否用 Chain 就够了别贪复杂度如果只有动态分支不需要循环Graph 也能胜任用 Workflow 反而会增加配置量只有当流程涉及动态路由 循环 并行子任务中的两项以上时Workflow 才是那个“最不坏”的选择。我在多个项目里养成的习惯是先用文字把业务流程画一遍在图上标出“谁先谁后”“谁可能和谁并发”“哪个分支是运行时才决定的”。标注完如果图里出现了闭环或者星型发散结构基本就能判断必须上 Workflow 了。3. 实操过程与核心环节实现3.1 用伪代码演示一个真实业务场景我不直接贴某个框架的完整代码因为 Eino 的 API 版本迭代比较快贴代码很容易过期而且这段内容的目的不是教大家抄 API而是理解编排的思考过程。下面用一个“智能客服工单处理”的场景把 Chain、Graph、Workflow 三种写法各演示一遍你会直观看到差异。业务需求是这样的用户提交工单系统读取历史订单信息。并行做两件事一是抽取工单里的关键实体订单号、商品名、诉求类型二是判断用户情绪是否激烈。根据抽取结果决定走“常规处理”还是“紧急处理”分支。分支完成后合并结果调用大模型生成回复内容。如果用户情绪仍然激烈回到步骤 2 再处理一轮最多重试 3 次。如果只用 Chain这个需求基本写不出来因为步骤 2、3、5 都不是线性的。只能用一堆 if 在外部驱动代码逻辑会散落在各个调用点状态传递全靠全局变量极其脆弱。如果只用 Graph可以硬写。步骤 2 的两个并行节点画出来很自然步骤 3 用 Graph 的条件分支也算能实现但步骤 5 的循环就完全没辙了——DAG 里不允许存在环。最终的实现只能是外层套一个 Go 的 for 循环每轮重新调用一次 Graph 实例中间状态保存到外部代码勉强能跑但可读性已经大打折扣。用 Workflow 的话流程图几乎是业务需求的一比一映射// 伪代码用于展示 Workflow 编排结构不是某个框架的具体 API workflow : NewWorkflow() workflow.AddNode(load_order, LoadOrderByID) workflow.AddNode(extract_entity, ExtractEntity) workflow.AddNode(emotion_analysis, AnalyzeEmotion) workflow.AddNode(regular_handle, RegularHandle) workflow.AddNode(urgent_handle, UrgentHandle) workflow.AddNode(merge_result, MergeResult) workflow.AddNode(generate_reply, GenerateReply) workflow.AddNode(final_check, FinalCheck) // 并行执行 extract_entity 和 emotion_analysis workflow.AddEdge(load_order, extract_entity) workflow.AddEdge(load_order, emotion_analysis) // 条件分支根据 extract_entity 的输出决定走哪条处理路径 workflow.AddConditionalEdge(extract_entity, func(output EntityOutput) string { if output.UrgencyLevel 0.8 { return urgent } return regular }, map[string]string{ urgent: urgent_handle, regular: regular_handle, }) workflow.AddEdge(emotion_analysis, merge_result) workflow.AddEdge(urgent_handle, merge_result) workflow.AddEdge(regular_handle, merge_result) workflow.AddEdge(merge_result, generate_reply) workflow.AddEdge(generate_reply, final_check) // 循环final_check 结果不达标时回到 emotion_analysis 重新分析 workflow.AddLoopEdge(final_check, emotion_analysis, func(check CheckOutput) bool { return check.EmotionIntensity 0.7 check.RetryCount 3 }) result : workflow.Execute(ctx, map[string]any{ order_id: SO-2024-001, })这个伪代码展示了一个核心思路Workflow 把执行路径的控制权从“代码调用堆栈”转移到了“图结构”本身。你在代码里不需要写任何 if、for 来调度执行顺序只需要把这些条件和循环声明成 Edge 的属性执行引擎会托管全局。3.2 Eino 内部如何实现 Workflow 的执行语义Eino 的 Workflow 底层实现和 Graph 的一个关键区别在于它对“节点输入”的管理更精细。Graph 里所有节点共享一个 State 对象各节点自己决定从 State 里取什么字段。而 Workflow 在 State 之上又加了一层“输入映射”的抽象每条 Edge 在传递消息时可以指定把上游节点的某个输出字段映射为下游节点的某个输入字段。这个精细化管理的价值在于节点之间得以解耦。比如 extract_entity 节点输出的是 EntityResult 结构体urgent_handle 节点只关心它的 UrgencyLevel 字段通过 Edge 的字段映射urgent_handle 拿到的输入就是一个明确声明过的字段而不是一个庞大的 State。这不仅让数据流更清晰也让节点天然可复用——同一个节点放在不同 Workflow 里只需要调整映射关系就行不需要改节点内部代码。工作流引擎在调度时会维护一个“就绪队列”。初始时入队所有没有上游依赖的节点每处理完一个节点就扫描它的下游边检查这条边的条件是否满足、上游数据是否就绪满足的入队不满足的继续等待。循环边的存在并不影响整个引擎的无环判断因为引擎在编码时会把循环记录为额外的重试次数不会把它当成无限期的死循环。3.3 同场景下 Chain 和 Graph 的代码对比为了让你直观看到差异我再用精简的伪代码展示同一个“工单处理”场景在 Graph 里的样子// 伪代码Graph 写法的问题 graph : NewGraph() graph.AddNode(load_order, LoadOrderByID) graph.AddNode(extract_entity, ExtractEntity) graph.AddNode(emotion_analysis, AnalyzeEmotion) graph.AddNode(route_branch, RouteBranch) // 用代码做条件判断 graph.AddNode(regular_handle, RegularHandle) graph.AddNode(urgent_handle, UrgentHandle) graph.AddNode(merge_result, MergeResult) graph.AddNode(generate_reply, GenerateReply) graph.AddNode(final_check, FinalCheck) graph.AddEdge(load_order, extract_entity) graph.AddEdge(load_order, emotion_analysis) graph.AddEdge(extract_entity, route_branch) graph.AddEdge(emotion_analysis, route_branch) graph.AddEdge(route_branch, regular_handle) graph.AddEdge(route_branch, urgent_handle) graph.AddEdge(regular_handle, merge_result) graph.AddEdge(urgent_handle, merge_result) graph.AddEdge(merge_result, generate_reply) graph.AddEdge(generate_reply, final_check) // 循环怎么处理 答案是处理不了必须在外面用一个 for 循环反复 Execute。看到问题没有route_branch 节点替代了 Workflow 里 AddConditionalEdge 的职责它内部必然有一坨 if-else。更重要的是final_check 和 emotion_analysis 之间的循环在 Graph 里没有表达法只能在外部包循环每轮 Execute 时还需要把状态导出再导入。这个方案代码能跑但其实你用不用 Graph 已经无所谓了——复杂的控制流已经回到了大家的业务代码里图只是个执行容器而已。3.4 Workflow 的代价状态可观测性与调试门槛Workflow 并不是银弹。它最大的代价是调试复杂度明显上升。Chain 只按顺序跑日志天然有序Graph 的日志虽然有并发交叉但可以用节点 ID 过滤Workflow 因为包含了循环和动态条件分支同一轮执行里同一个节点可能被触发多次日志里同一节点 ID 会反复出现如果不做 Trace ID 级别的追踪排查问题会非常痛苦。我在实际项目里养成的习惯是在 Workflow 里给每个节点注入一个共用的日志字段“request_id”代表一次完整的用户请求。每执行一个节点就打印这个 request_id、节点名、输入关键字段、输出关键字段并用一个缩进级别表示当前是第几次循环。这样做的好处是即使节点被触发 10 次也能通过 request_id 循环序号快速还原每次调用的上下文。如果你用的是官方或社区的可视化方案另外一个很方便的能力是导出执行计划。Workflow 在执行前会先生成一棵执行计划树展示每个节点的触发路径和边条件把它导出成 JSON 存到日志里线上排障时能精确看到这轮请求实际走了哪些分支走了几次循环。这一步在 Graph 里做不了这么精细因为在 Graph 的执行模型里节点触发路径比较扁平Workflow 就不同了动态分支加循环很容易让执行路径变得多而杂这个能力几乎是必须的。4. 常见问题与排查技巧实录4.1 节点状态混乱是不是不该用 Workflow我遇到的第一个 Workflow 翻车案例是真事。当时把一批 Graph 换成 Workflow 后发现并行节点的状态经常串。两个分支节点同时往 State 写入同名 key 的字段后写的把先写的覆盖了导致聚合节点拿到的数据错乱。排查了半天根因不是 Workflow 有 bug而是我没做好数据隔离。Workflow 的并行节点在 Eino 这类框架里共享 State但因为执行引擎会为每个分支维护独立的局部状态同一个 State 字段被不同分支写入时需要格外小心。解决思路很简单并行节点的输出字段命名从一开始就要带分支前缀比如 extract_result、emotion_result不能都叫 result。这个教训也提醒我在编排层做字段命名规范比写注释重要得多。4.2 循环退不出来死循环问题排查Loop 功能刚上手时最常踩的坑是死循环。有一次我写了一个“检查生成结果是否通过”的循环边结果检查节点在连续几次运行时评分结果都是 0.8始终小于我设定的阈值 0.85整个 Workflow 就无限循环下去。因为我没有设置最大循环次数和整体超时时间。排查方法有两条。第一看执行计划日志里同一个节点被调用的次数如果超过某个阈值还在涨基本可以断定是条件边判断不收敛。第二检查条件判断所用的字段是否真的是上游节点新写入的还是读了一个全局不变的字段——这是最隐蔽的坑条件边永远判断的是同一个旧值自然会一直重试。我的经验是在设计循环边时一定要把“最大轮次”作为 Workflow 的全局参数配好而且超时时间要低于下游依赖服务的超时时间这样即使逻辑有问题也不会把整个调用链路拖垮。4.3 从 Graph 迁移到 Workflow 时的注意点已有 Graph 迁移过来时最容易踩的坑是不改动 State 结构直接把 Graph 的节点搬进 Workflow然后发现有大量节点不再被触发了。原因是 Graph 里节点的触发条件是“是否有上游节点完成”Workflow 里则是“是否有上游节点的输出映射到该节点的输入”。如果原来 Graph 里两个节点没有显式的数据依赖只是靠执行顺序让某个节点读取 State 里的字段迁移到 Workflow 后这个隐式依赖就断了——Workflow 的节点只有在收到输入映射时才会被唤醒。解决方法是显式补一条“纯控制流”边上游节点输出一个空结构体或者只传递触发信号下游节点靠这条边被唤醒却不依赖它的数据。这个设计初看很绕但想通了就会觉得正是这种显式声明才让 Workflow 比 Graph 更可控——每个节点的触发原因都摆在那里不会默默依赖执行顺序。4.4 与模型调用相关的超时和重试配置如果 Workflow 中的节点是 LLM 调用超时配置最好独立于 Workflow 全局超时。我习惯给 LLM 节点配置两档超时单次调用 30 秒Workflow 整体 120 秒。这样当 LLM 服务出现慢响应时节点能早点失败并触发条件边的“降级”分支而不是把整个 Workflow 拖到超时。在 Workflow 的循环边场景里重试逻辑要小心区分“节点内部的重试”和“Workflow 层的循环重试”。节点内部重试用于处理瞬时错误网络抖动、限流Workflow 层的循环用于处理结果不达标内容质量、情绪强度。两种重试混在一起很容易导致重复计费——一次循环内节点内部重试了 3 次整体循环又重试了 3 次同一个模型的调用次数会变成 9 次。我在一个生产项目里就因为这个问题被账单教育过。4.5 性能优化的两个小技巧如果 Workflow 里的并行分支较多节点启动的开销不可忽视。每个节点的启动都涉及上下文复制、状态读取、映射注入数量大了之后即使节点本身很快整体调度也可能在毫秒级别上叠加延迟。我的优化手段是让每个节点尽量保持轻量避免在节点内部做重量级初始化比如每次都新建立一个 HTTP 客户端能复用的连接池尽量放到 Workflow 外部节点内部只做纯计算。另一个技巧是在大流量场景下预编译 Workflow。Workflow 的执行计划可以在编译期生成并缓存不要把编译和执行放在同一个请求里。我在长耗时业务中通常会在系统初始化阶段把 Workflow 实例构建好请求进来直接执行能省掉编译耗时。如果你做的系统 QPS 本身不高这个优化感受不明显但一旦压测就能看到几十毫秒级别的差距。5. 结尾我个人在实际操作中的体会是Chain、Graph、Workflow 这三者不是竞争关系而是 AI 应用开发者成长路径上三个不同阶段的标配。Chain 帮你建立“流程拆解”的直觉Graph 帮你建立“并行与汇聚”的脑回路Workflow 才让你真正进入“流程设计即业务表达”的境界。如果你现在正好卡在“Graph 能跑但总觉得代码很别扭”的节点上大概率说明你该尝试引入 Workflow 了。最后再分享一个选型上的参考不要因为 Workflow 功能强大就把所有场景都改成 Workflow。用一个固定顺序、不含分支、不需要循环的小任务去上 Workflow收益非常有限反而是给自己增加调试成本。按我目前的经验链路里包含两个以上运行时条件分支或即便只有一个循环Workflow 的价值就体现出来了。沿着这个标准去对照自己的项目你会得到一个比较靠谱的结论。