乐于分享
好东西不私藏

AI Agent工作流:稳定评估兼得模式

AI Agent工作流:稳定评估兼得模式

做 AI 工作流的工程师,几乎都遇到过同一个噩梦。

生产环境的代理需要持久化、重试、水平扩展;评估迭代需要轻量、快速、可重放,每秒钟能跑几百次。这两个需求天然矛盾,传统的做法是各搭一套,导致评估环境与生产环境长期漂移,一个调好的代理部署上线后表现完全不一样。

Brex 这个五人工程团队给出了一个解法:把编排逻辑写成与运行时无关的纯函数,再用适配器模式分别对接生产环境的 Temporal 和评估环境的进程内 Mock。同一个编排函数,在两个完全不同的运行时里跑出完全一致的结果。

这不是理论推演,这是一套已经在生产环境里跑了 18 个月、每天运行约 100 次、成功率从 96% 提升到 99.9% 的工程方案。

一、传统方案的根本矛盾:生产与评估不可兼得

先说清楚问题到底卡在哪里。

AI 工作流是一系列步骤的串联,其中一步或多步涉及大语言模型调用。这种工作流对生产环境的要求,与过去十年间任何长期运行的分布式系统一致:需要能经受住部署和崩溃,需要幂等地重试,需要水平扩展。这些事情,工作流引擎(Temporal、Airflow、Restate)早在多年前就已经解决。

但 AI 工作流有一个独特之处:LLM 步骤的输出质量会随着提示词微调或模型变更而漂移。因此必须通过评估来验证,在标注数据集上离线运行工作流并对输出评分。这要求工作流引擎具备其设计时未曾考虑的能力:成本足够低,能重复运行数百次的快速评估循环。

这两项要求是相互矛盾的。生产环境的持久化需要一个重量级、分布式的运行时;评估迭代需要一个轻量级、进程内的短暂循环,几秒钟内重新跑完。大多数技术栈都是围绕其中一种运行时构建的。

虽然持久性优先的运行时确实提供了测试环境,但它们需要为一个根本不需要这些组件的评估循环搭建沙箱、任务队列和测试服务器。把评估代码塞进生产引擎会导致类别不匹配,继承一堆完全不需要的开销;反过来把生产代码塞进评估框架,则根本无法提供长时运行所需的持久性。

二、LangGraph / Mastra / Temporal 都没解决这个矛盾

主流代理框架都把编排和运行时绑定在一起。LangGraph 和 Mastra 之类的代理框架,会在其自身的 SDK 中直接表达编排逻辑,控制流转化为图中的节点和边,或者转化为框架提供的领域特定语言(DSL)。编排逻辑与框架本身是同一个组件。要评估该逻辑,就需要运行框架;要部署该逻辑,同样需要运行框架。不存在独立于框架而存在的编排。

像 Temporal 这样的工作流引擎则采取了相反的做法:它允许你使用通用语言编写编排代码,但会对编写方式施加限制。Temporal 的工作流代码必须是确定性的,因此无法在编排代码内部直接调用 Date.now() 或执行 I/O 操作。所有这些操作都必须推入工作流步骤中。这些约束的存在是为了确保可重放性,但它们的存在也要求编排代码必须遵循引擎的规则。

Brex 的开户代理运行在 Temporal 上的生产环境中,但其基于 LLM 的决策需要持续调整。对于这些代理的评估,有一种简单粗暴的方法是在单独的评估运行时中重新实现每个代理,但这种方法会导致同一个逻辑存在两份副本,从而可能引发评估环境与生产环境之间的偏差。

评估与生产环境偏差正是这种重写方案希望杜绝却最终无法杜绝的故障模式。

三、解耦的核心:让编排变成纯函数

Brex 给出的解法核心是:不再为某个运行时编写编排代码,而是开始针对该运行时所遵循的接口规范来编写编排代码。

具体来说,代理的编排是一个普通的函数。其唯一的依赖是类型化的 Steps 接口,其中列出了代理所有有意义的操作。它不会导入任何与运行时相关的内容:既不导入 Temporal,也不导入 eval 框架,更不导入 Node.js 内置函数。

例如一个叫 classifyBusinessAgent 的代理,它的编排代码读起来就像业务逻辑:先丰富数据,再进行分类。它并不关心 enrichWithWebData 是分派给工作进程的 Temporal 活动,还是返回测试数据的进程内调用。编写新代理的开发人员永远不会接触运行时。

副作用存在于具体的 Steps 实现中。这里才是实际执行操作的地方,通过依赖注入接收诸如 Web 爬虫和 LLM 客户端等依赖项。在生产环境中,插件调用真实的服务;而在评估环境中,插件返回测试数据。编排机制不区分这两种情况。

四、可移植性的强制约束:构建时检查

可移植性并非理所当然,必须强制执行,否则一旦有人为了图方便走捷径,这种特性就会被侵蚀。Brex 给出了两条最关键的规则。

第一条:编排中不得存在隐蔽的非确定性。不得读取墙钟时间,不得使用随机值,不得进行直接 I/O 操作。任何非确定性的内容都应该作为 Steps 方法来处理,这样它就成为运行时接管控制的切入点。

第二条:编排中不得使用 Node.js 或运行时特有的 API。编排和 Steps 接口绝不能导入任何会将其与特定进程模型绑定的内容。仅限 Node.js 使用的模块(HTTP 客户端、文件/CSV 解析器)应位于 StepsImpl 中,绝不能出现在编排或接口中。

这些规则确保了同一个编排方案可以在生产环境和评估环境中安全地重放。Brex 将可移植的结构设计为阻力最小的路径:defineAgentHandle 函数仅提供步骤和供处理的输入,因此编写代理的方式本身就可以保证其正确性。

五、双适配器:同一编排,两个运行时

随着编排被简化为实现接口的一个函数,运行时适配器的作用仅仅是"提供 Steps 实现并调用该函数"。Brex 实现了两个适配器:用于生产环境持久化的 Temporal 适配器,以及用于评估的进程内适配器。

Temporal 适配器的核心工作是把每个 Steps 方法都映射成一个 Temporal 活动,把协调逻辑运行在确定性沙箱里。编排层调用 steps.foo(...) 会被 Proxy 拦截,在方法名前添加代理名称,映射到工作进程已注册的带前缀的活动。客户端启动 agentWorkflow 后,编排层内部的每次调用都会作为 Temporal 活动分发,支持重试、超时以及重新部署时重放。编排代码完全察觉不到这些操作的发生。

eval 适配器要小得多。它没有沙箱、没有工作进程、也没有任务队列。它通过一个 Steps 实例在进程内运行相同的编排,插件返回的是测试数据。由于 runEval 只是一个 (input) => Promise 函数,所以任何 eval 平台都可以将其作为黑盒进行封装。Braintrust、Laminar、LangSmith,集成方式完全相同。

六、实战数据:从 96% 到 99.9% 的可靠性跃升

Brex 把这套架构用在了真实的开户代理生产环境中。这些代理每天运行约一百次,每次耗时 20 到 60 分钟,涉及数十次 LLM 和工具调用。

在旧的架构下,运行过程中任何环节的 Pod 回收、重新部署或超时都会导致整个流程被清零,近 4% 的任务永远无法完成。现在,当工作节点在运行中途宕机时,Temporal 会回放历史记录并从最后完成的步骤继续执行,过去几个月里任务完成率一直保持在 99.9%。

业务层面,基于该平台构建的代理现在能为超过一半的开户申请生成自动化决策。这是一个非常具体的数字,意味着这套架构不只是工程上的优雅,在商业上同样能落地。

七、收获与代价

这套架构最大的收获是:从设计上讲,评估版与生产版之间不存在偏差。经过分支调优的评估版不可能在不知不觉中与生产环境中的版本产生差异。运行时选择变得可逆,生产可以选 Temporal 或 Restate,评估可以选 Braintrust、Laminar 或 LangSmith,这些都是适配器的替换而非重写。Brex 曾多次更换评估平台,代理对此毫无察觉。

代价同样明显。编排层无法直接调用 Temporal 的信号、查询或定时器,因为这些在 eval 运行时中并不存在。每个运行时功能都必须通过间接方式添加并进行传播,要暴露一项新功能,需要将其设计到接口中,并在所有适配器中实现。新功能是全平台范围的变更,而非一行代码就能完成。开箱即用的可视化工具同样缺失,框架原生的图可视化工具和步骤调试器会假设你是根据其模型编写的代码,当编排层仅是实现了接口的普通函数时,除非自行构建,否则将无法使用这些工具。

我的评价

读完这套架构,我最大的感受是:Brex 给出的不是新算法,是新纪律。

它的核心思想其实很朴素:把编排和运行时分开,通过接口契约保证二者之间的边界。这与软件工程里"业务逻辑与技术细节分离"的原则如出一辙,只是 AI 时代这种分离的难度被严重低估了。

当前 AI 领域的工程实践,大多数团队还在重复 Brex 18 个月前的弯路:写一套代理代码,塞进 LangGraph,跑评估时发现和线上不一样,只能再写一份重新实现。结果两份代码长期漂移,每次模型更新都变成一场跨代码库的同步噩梦。Brex 的解法是让这个问题在架构层就不存在。

这套架构的真正价值在于它把"运行时选择"变成了一种可逆的工程决策,而不是一开始就押注的不归路。这意味着创业团队可以先用最简单的运行时起步,等业务规模到了再切到更复杂的运行时,中间不需要重写核心代码。

更深一层看,这套架构揭示了一个被很多人忽视的事实:AI 工作流的工程难度,90% 都不在 LLM 调用本身,而在持久化、重试、评估、可观测这些"周边系统"。Brex 把周边系统做对了,业务自然就顺了。