site logo

Marico's space

Agent 工作流:一个早已解决的问题,却被行业反复重新发明

算法解析 2026-04-30 17:45:34 8

译者前言:Microsoft Agent Framework 几天前刚发布 1.0,连同 Google ADK、LangGraph、CrewAI,几乎所有主流 Agent 框架都在做同一件事:把「工作流引擎」和「Agent 运行时」打包在一起卖。但有意思的是,Anthropic 和 OpenAI 这两家模型厂,却偏偏不这么干。这篇文章就是想聊聊这背后的逻辑,以及为什么我认为某些做法其实是在重蹈覆辙。

图数据流到底是什么

打开 Microsoft Agent Framework 的 Workflows 包源码,或者 LangGraph 的 Pregel 模块,你会看到同一个东西:一个运行在 BSP(Bulk Synchronous Parallel)模型上的图引擎。BSP 最初是 Google 在 Pregel 论文里提出的,最初是为了大规模图处理用的。

BSP supersteps: executors run in parallel, sync at a barrier, repeat

每个 superstep 里,所有有待处理消息的执行器并发运行;全部完成后,消息沿边传递,下一个 superstep 启动。如此反复,直到没有待处理消息为止。

如果你用过 Apache Beam、Akka Streams 或 Flink,应该会觉得眼熟——流处理系统的设计思路是一样的。Pregel 这类系统之所以存在,是因为当有很多小计算需要通过带类型的消息协调、每轮之间还要 checkpoint 时,BSP 提供了一个干净的执行模型,内置 fan-out、fan-in 和同步屏障。

它的适用场景真实存在,但比较窄:

  • 带背压的流式管道(token 级输出经过一串 transformer)。
  • 声明式创作工具,非工程师在 UI 里拖拽节点(n8n、Zapier 本质上就是这个)。
  • 带类型 join 的 fan-out 密集型工作(把同一个查询发给十个 Agent,聚合结果)。

对于典型的 Agent 应用,以上三点你大部分时候都不需要。

图数据流假设的拓扑是这样的,有 fan-out、join 和 superstep:

Graph dataflow topology: A→B→D and B→C→E→D

大多数 Agent 应用的真实结构其实是这样的:一个 Agent 循环加工具调用,偶尔有一次 handoff:

Agent loop: agent calls a tool, observes the result, loops

所谓 handoff,无非是一个 Agent 调用了一个把控制权移交给另一个 Agent 的工具。这就是 while 循环里的一个函数调用,根本不需要 superstep。

头部模型厂站在哪边

不出意料,Anthropic 和 OpenAI——这两家所有框架都在对接的模型厂——非常默契地没有自带工作流引擎。

Anthropic / OpenAI Microsoft / Google / LangChain
Workflow engine Don't ship one Ship one bundled with the agent runtime
Composition Bring your own (Temporal, FSM, bus, code) Use ours (graph DSL, declarative YAML)
Examples Claude Agent SDK, OpenAI Agents SDK MAF Workflows, Google ADK, LangGraph, CrewAI
Bet Models need less scaffolding over time Enterprises need built-in compliance + audit

Anthropic 2024 年底发布的《Building Effective Agents》立场非常明确。他们在 workflows(预定义的代码路径)和 agents(自我导向行为的系统)之间画了一条线,然后明确警告不要过度依赖框架。核心论点是:框架增加的抽象层会遮蔽底层的 prompt 和响应,让人在本可以用更简单方案时忍不住引入更多复杂度。文章直接点名了 LangGraph、Bedrock Agents、Rivet 和 Vellum。

Claude Agent SDK 体现了这个思路。核心是这些:Agent 循环、工具调用、内存钩子、MCP 集成。没有工作流图,没有节点-边 DSL。想做多步逻辑?写代码。

OpenAI 的 Agents SDK 形态一致。四个核心概念:agents、handoffs、guardrails、sessions。Handoff 不是图拓扑,就是 LLM 调用的一个普通函数工具(transfer_to_refund_agent)。多 Agent 编排就是一个 Agent 调用工具把控制权移交给另一个。当用户需要持久化时,推荐做法是把 SDK 包在 Temporal 里:每个 Agent 调用变成一个 Temporal activity,工作流引擎负责持久化和故障恢复。

所以模型厂自己的意思是:做出色的基础组件,让人们用已有的编排工具自由组合。

编排问题的真实光谱

「工作流」这一个词被用来指代至少四个完全不同的问题。它们不能混用。

Category What it solves Example libraries Production-grade since
In-process FSM Valid state transitions, single process Stateless, Automatonymous ~2010
Distributed saga Cross-service coordination + compensations MassTransit, NServiceBus, Brighter ~2009
Durable execution Replay-based recovery through crashes Temporal, DurableTask, DBOS, Restate ~2017
Graph dataflow Typed parallel message-passing + streaming LangGraph, MAF Workflows, Beam ~2024 (for agents)

这个分类里,在 Agent 框架讨论中最被忽视、但其实最重要的是持久化执行。它提供了 FSM 和消息总线都给不了的东西:确定性回放。

   Run 1 (crashes after step 3):              Run 2 (replay):
   -----------------------------              ---------------

   step 1 --> [LLM call: $0.50]   logged      step 1 --> [logged]     skip
   step 2 --> [tool call]         logged      step 2 --> [logged]     skip
   step 3 --> [LLM call: $5.00]   logged      step 3 --> [logged]     skip
   step 4 --> [tool call]         CRASH       step 4 --> [tool call]  runs
                                              step 5 --> continues normally

工作原理:你的代码运行,引擎把每个外部调用作为事件记录下来。如果进程中途挂掉,它不会从一个保存的 saga 状态重启,而是从顶部重放你的代码,跳过已有结果的调用,直到追赶到实时状态。从你的角度看,你写的是直线型异步代码;从系统角度看,每一步都是可恢复的。

对于需要长时间运行的 Agent 工作(一轮 LLM 调用 30 秒,接着一个工具调用 2 分钟,再来一轮 LLM 调用),这正是合适的原语。你肯定不想因为容器重启就重做一次 5 美元的 LLM 调用。

有意思的是,Microsoft 其实已经内置了 DurableTask,从 2017 年就有了。Agent Framework 的 Workflows 包甚至有 DurableTask 集成。这引出了一个合理的问题:如果我需要持久化,直接用 DurableTask 就好了;如果需要消息总线,直接用 MassTransit 就好了;如果需要状态验证,直接用 Stateless 就好了。那这个图运行时到底额外带来了什么?

诚实的答案是:流式输出,和图拓扑的声明式编排。对于大多数 Agent 应用,这两者都不重要。

新范式的真实成本

学习一套新的编排模型有真实的成本,框架的市场宣传往往会低估它。

Adopting a graph dataflow runtime Composing existing primitives
Executor lifecycle async Task (already known)
Edge types: direct, fan-in, fan-out, switch Stateless config (afternoon to learn)
Superstep concurrency rules MassTransit sagas (well-documented)
Port system + checkpointing DurableTask (years of Azure Functions docs)
All framework-specific Composes; transfers everywhere

框架的卖点是「编排的事我们帮你搞定」。现实往往是:你在你写的代码和真正运行的东西之间多加了一层间接寻址。调试一个行为异常的 Agent 流程,现在意味着同时理解模型的推理运行时的 superstep 调度。当某个环节卡住了:是 LLM 的问题,还是边路由器的问题?

这正是 Anthropic 对框架批评最有力的一点。他们不是说框架是错的,而是说框架遮蔽了 prompt 和响应。对于 Agent 工作,这种遮蔽代价高昂。调试时最有用的一步就是看清楚模型实际被问了什么、实际答了什么。任何在这层循环里加间接层的做法,在出问题时会让你付出更多。

当然也有合理的反驳。声明式图工作流让合规、审计和可视化更容易。如果你向受监管行业的企业卖产品,「这份 YAML 明确定义了 Agent 的行为」确实有价值——这显然是 Microsoft 和 Google 在押的注。我不是说这个赌注错了。只是它和模型厂自己的做法(做出色的原语,相信开发者会组合)走的是不同的路。

模型现在能自己做什么

我对重型工作流引擎持怀疑态度的另一个原因是:模型本身在处理长周期工作时越来越不需要外部编排的脚手架了。

Agent loop using the filesystem as ephemeral memory

Anthropic 最近关于 context engineering 的文章描述了 Claude Code 的模式:一个长时间运行的 Agent,用 globgrep 这类原语即时探索环境,CLAUDE.md 文件提供高层指令,文件系统本身充当临时内存。它不是图,不是状态机,就是一个有趁手工具和 workspace 的模型。

正在浮现的模式——而且我认为这是真正有趣的趋势——是用文件系统作为长时间任务的临时内存。Agent 工作时读写和更新文件;如果它崩溃了,把同一个 workspace 交给它继续就行。工作流的「状态」就是文件的状态,而这是每个开发者都理解的抽象。

这基本就是 Claude Code、Cursor 和同类编程 Agent 今天的工作方式。编排很轻,因为模型自己在做编排。框架的职责是给模型好的工具、一个运行它们的沙盒,以及故障时的恢复方式。框架的职责不是把 Agent 的推理建模成一张图。

如果模型在长周期一致性上持续进步(过去 18 个月的轨迹表明会的),那为 2024 年 Agent 能力设计重型编排框架迟早会显得杀鸡用牛刀,就像在大多数现代 Web 应用里 XML BPM 引擎显得多余一样。

我们遗忘了有界上下文

先退一步,忘掉框架之争,看看大家实际上在做什么。

实践中最常见的 Multi-Agent 架构(Claude Agent SDK 鼓励的、Anthropic 研究 Agent 用的、生产部署最终收敛到的)是 orchestrator + subagents:

Orchestrator + subagents: one orchestrator spawns N subagents

orchestrator 决定需要做什么,生成 subagents 来执行,可能再生成更多,最后聚合结果。这对单一连贯任务没问题。问题出在团队把这种模式扩展到多个业务域时,最终得到一个 orchestrator,它的 subagents 可以触及整个系统:订单、库存、支付、物流、客服,全部。

这就是一个单体式的 Agent 系统。我们在搭建 2010 年代的企业单体架构,只是把盒子叫成「Agent」。

Anti-pattern: one orchestrator reaching into every domain

正确的架构:每个有界上下文有其专属 Agent,通过消息通信:

Bounded contexts: each domain has its own agent

领域驱动设计十五年前就解决了这个问题。有界上下文拥有自己的模型、语言、规则、数据。如果一个上下文内部需要触发另一个上下文的副作用,不要跨边界伸手,而是发一条消息。接收方决定如何解释。这就是每一个真正能扩展的微服务架构的基本形态。

Agent 也应该遵循同样的规则。订单 Agent 操作订单,不直接修改库存、不直接扣款、不直接触发物流。当一个订单需要预留库存时,它发布 OrderPlaced(或发送 ReserveInventory),由库存上下文的 Agent 在自己的边界内处理。

这不只是架构卫生。有界上下文给了你一个正经 Agent 系统想要的一切:

  • 爆炸半径。一个行为异常的库存 Agent 最多破坏库存,动不了订单和物流。
  • 权限边界。每个 Agent 只有自己上下文内的工具和权限。最小权限变得可执行,而不是「理想状态」。
  • 独立演进。团队可以独立修改库存 Agent 的 prompt、模型或工具,不需要和五个其他域协调。
  • 可审计性。「Agent 做了什么?」每个 BC 有有限答案,而不是一个巨型 orchestrator 的分布式追踪。

关键的是,这还能让你回到成熟的编排工具上。跨 BC 的消息正是消息总线的用武之地。MassTransit、NServiceBus、Kafka、RabbitMQ 都是为这个设计的。如果你的 Agent 架构是有界上下文通过事件通信,你不需要图工作流引擎来编排它们。你只需要一条总线。

自带的「工作流引擎」做法实际上在反着来。它鼓励你把全部 Agent 放进一个工作流图里,在一个进程里,共享一个执行上下文。这就是单体的拓扑。把盒子叫成「Agent」并不改变这一点。

如果要做任何超出玩具 Demo 的东西,问题不是「我应该用什么图运行时来编排 Agent」,而是「我的有界上下文是什么,每个上下文里放哪些 Agent」。

所以你应该用什么

If you need... Reach for
In-process state validation Stateless (or equivalent FSM)
Cross-service coordination with compensations MassTransit or NServiceBus
Replay through crashes, long-running tasks Temporal or DurableTask
Streaming fan-out + typed joins LangGraph or MAF Workflows
Multi-agent orchestration per bounded context Message bus + your language's async primitives

如果你需要的是数据流并行(token 级流式输出、跨节点 fan-out、streaming join)——这是图数据流唯一真正有价值的地方——那么 LangGraph 和 MAF Workflows 的 BSP 模型是合理的。这不是一条坏路,只是条窄路。

但对于大多数 Agent 应用,我要说的核心观点是:不要从框架开始,从问题开始。先搞清楚你要解决的是哪类编排问题,再看现有工具里哪个最适合。不要因为某个框架把「Agent」和「Workflow」打包在一起就默认你需要两者——模型厂自己都在告诉你:给他们好的原语,你用自己已有的工具组合。

参考文献和链接如有需要可参考原文。