运行时无关的AI工作流:兼顾生产持久性与快速评估
译文 AI 逐段翻译
InfoQ 首页 文章运行时无关的 AI 工作流:生产持久性与快速评估迭代的模式
运行时无关的 AI 工作流:生产持久性与快速评估迭代的模式
2026年8月6日 12分钟阅读
作者
审校
关注我们
收听本文 - 0:00
音频准备就绪
您的浏览器不支持音频元素。
0:00
0:00
关键要点
- AI 工作流有两个直接权衡的需求。在生产中可靠运行需要持久化并分发每一步,以便在崩溃、部署和重启后存活。但同样的机制使得运行变得过于沉重,无法满足快速、可丢弃的循环检查 LLM 输出质量的需求。带来持久性的特性正是扼杀迭代速度的特性。
- 你可以通过将工作流编写为纯业务逻辑,不关心它在何处运行,然后插入运行时来满足这两种需求,这样完全相同的逻辑可以在生产和评估中不变地运行。
- 当只有一份逻辑版本时,经过评估的版本保证与发布的版本一致。这种方法消除了由于逻辑版本随时间漂移而导致的整类错误。
- 保持逻辑与运行环境无关不能依赖开发者的自律。架构必须使正确的方式成为编写工作流最简单的方式。每当有人编写非无关代码时,构建必须失败。
- 解耦并非没有代价。编排层失去了直接访问各运行时原生特性的能力。每项新功能都必须通过无关层接入。这种设计只有在项目真正需要生产可靠性和快速评估时才值得。
AI 工作流是一系列步骤链接在一起以完成任务的流程,其中一个或多个步骤包含对 大型语言模型 (LLM) 的调用。组合这些步骤的逻辑(它们的顺序和分支)在此称为工作流的编排逻辑。
这些工作流承载着任何长时间运行的分布式系统十年来一直具备的生产需求:它们需要在部署和崩溃中存活、幂等重试、水平扩展。工作流引擎多年前就解决了这类问题。
AI 工作流的不同之处在于,LLM 步骤的输出质量可能随着每次提示调整或模型更改而漂移,因此必须通过 评估 来检查,即对工作流进行离线运行,针对标记数据集对其输出进行评分。
相关赞助商
这反过来又要求经典工作流引擎未设计的东西:一个足够便宜的快速评估循环,能够重复运行数百次。
这两个需求是相互矛盾的。生产持久性需要重量级、持久化、分布式的运行时。评估迭代需要轻量级、瞬态、进程内循环,可以在几秒钟内重新运行。大多数技术栈都是围绕其中一个运行时构建的。
持久性优先的运行时确实提供测试环境,但它们需要为评估循环建立沙箱、任务队列和测试服务器,而评估循环根本不需要这些。本文描述了我们在消除这种权衡时使用的模式。
该模式来自 Brex 的 AI 工作流平台。该平台使用 TypeScript 编写,由五人工程师团队维护。其 worker 在 Brex 的 Kubernetes 集群上运行,并连接到 Temporal Cloud,即托管的 Temporal 服务,用于执行长时间运行的智能体。
智能体通过 Vercel AI SDK 访问 LLM,该 SDK 路由到内部 LLM 网关,该网关集中处理速率限制和身份验证。评估在我们的内部平台上运行。
生产持久性与快速离线评估之间的权衡
让我们从工作流引擎的特性开始。持久执行要求在下一步运行之前持久化每一步的结果。如果进程崩溃、重新部署或重新调度到不同的 worker 上,引擎会重放历史并从上次中断的地方恢复。状态比任何单个进程存活更久。这正是你对于 深度研究智能体 所想要的:它在一个小时内跨数十次 LLM 调用和工具调用运行,不能因为 pod 被回收而损失四十分钟的工作。
现在看看评估迭代的特性。你在调整提示或分支决策。你想要更改一行,加载包含几百个示例的数据集,并查看聚合分数。循环是本地的、进程内的、瞬态的。不应该持久化或在集群上调度任何东西。没有任何东西应该在运行后存活。你想要模拟依赖项,以便它们的输出保持不变,将 LLM(你实际评估的部分)隔离为运行之间唯一变化的因素。循环保持足够廉价,每小时可以运行数百次。
通过工作流引擎运行评估是类别不匹配。你继承了持久性、任务队列、worker 和重放语义。这是与所需紧密循环对抗的开销。相反,通过评估工具运行生产无法提供你的一小时智能体所依赖的任何持久性保证。它们是解决不同问题的不同运行时。你的编排不应该被迫与任何一个绑定。然而在实践中,它几乎总是如此。
大多数技术栈迫使你选择一个
团队最终与运行时绑定的原因是主流工具通过设计将编排与运行时耦合。Agent 框架如
LangGraph 和 Mastra 直接在其 SDK 中表达编排。你的控制流变成图形节点和边,或框架的 DSL。编排逻辑和框架是同一个产物。要评估该逻辑,你运行框架;要为它服务,你运行框架。不存在独立于框架的编排。直接在其自身SDK中表达编排。你的控制流变成图节点和边,或框架的DSL。编排逻辑和框架是同一工件。要评估该逻辑,你运行框架;要服务它,你运行框架。不存在独立于框架的编排。
作为一个具体示例,这里是 classifyBusinessAgent(同一个我们稍后将要重写的)作为一个 Mastra 工作流:
import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";
const enrichWithWebData = createStep({
id: "enrich-with-web-data",
inputSchema: z.object({ businessName: z.string(), website: z.string() }),
outputSchema: z.object({ businessName: z.string(), webContext: z.string() }),
execute: async ({ inputData }) => {
// ...
},
});
const classify = createStep({
id: "classify",
inputSchema: z.object({ businessName: z.string(), webContext: z.string() }),
outputSchema: z.object({ category: z.string() }),
execute: async ({ inputData }) =>
// ...
});
// Ordering lives in Mastra's builder, not in plain control flow. The steps are
// Mastra objects, and the workflow only exists once committed to its engine.
export const classifyBusinessWorkflow = createWorkflow({
id: "classify_business",
inputSchema: z.object({ businessName: z.string(), website: z.string() }),
outputSchema: z.object({ category: z.string() }),
})
.then(enrichWithWebData)
.then(classify)
.commit();这里的一切都是 Mastra。在.commit()将图交给 Mastra 的引擎之前,什么都不运行。要评估这个逻辑,你需要启动同一个引擎。
诸如 Temporal 这样的工作流引擎则反其道而行之,允许你用通用语言编写编排,但限制你编写的方式。Temporal 工作流代码必须是确定性的,因此你不能在编排中直接调用Date.now()或执行 I/O。所有这些都必须被推入工作流步骤中。跨越工作流边界有负载大小限制。这些约束的存在是为了确保可重放性,但它们的出现要求编排必须依据引擎的规则来编写。
Brex 的入职代理在生产环境运行于 Temporal 之上,但它们需要持续调整其 LLM 驱动的决策。评估它们的天真方法是在单独的评估运行时中重新实现每个代理,但这种方法会留下同一逻辑的两个副本,这为评估-生产偏差打开了大门,因为副本可能会漂移。
评估-生产偏差是这种模式旨在杜绝的失败模式,它通过实现运行时无关的工作流编排来实现,这打破了许多框架认为运行时和编排是单一工件的假设。
运行时无关的编排
核心举措是停止为运行时编写编排,转而针对运行时满足的接口进行编写。为运行时编写编排,转而针对运行时满足的接口进行编写。
图1. 可移植核心及其适配器。来源:作者创建。
编排及其 Steps 接口在不同运行时之间是相同的;只有注入的插件和底层运行时发生变化。
该模式的完整可运行版本可在仓库中获取。其中包含一个名为ClassifyBusinessAgent的可运行代理,它连接了生产和评估运行时。其src/文件夹按三种方式拆分。agents/文件夹是代理作者唯一接触的文件夹,每个代理一个文件夹,包含编排和实现它的具体步骤。platform/文件夹持有定义代理所基于的原语,以及上图中的两个运行时适配器。最后,bin/文件夹持有入口点,如生产 worker 和评估循环。
这个顶层拆分正是该模式论证的依据。添加一个代理只需在agents/中编写代码。platform/不会改变,两个运行时也不会了解新代理。
具体来说,代理的编排是一个普通函数。它唯一的依赖是一个类型化的 Steps 接口,该接口命名了代理的有意义的操作。没有导入任何运行时特定的东西:没有 Temporal,没有评估框架,也没有 Node.js 内置模块。
// The contract the orchestration depends on. Nothing here is runtime-aware.
export interface ClassifyBusinessSteps {
enrichWithWebData(website: string): Promise<string>;
classify(businessName: string, webContext: string): Promise<string>;
}
// No workflow engine or eval framework imports.
export const classifyBusinessAgent = defineAgentHandle({
name: "classify_business",
description: "Classifies a business given its name and website.",
orchestration: async (
steps: ClassifyBusinessSteps,
input: { businessName: string; website: string },
) => {
const webContext = await steps.enrichWithWebData(input.website);
return steps.classify(input.businessName, webContext);
},
});编排读起来像业务逻辑:Enrich,然后 classify。它不关心enrichWithWebData是分派给 worker 的 Temporal 活动,还是返回固定数据的进程内调用。开发新代理的开发人员永远不会接触运行时。
副作用存在于具体的 Steps 实现中。这里是真正工作发生的地方。在这里,它通过注入接收依赖,例如 Web 抓取器和 LLM 客户端(参见ClassifyBusinessStepsImpl)。
在生产环境中,插件调用真实服务。在评估中,插件返回固定数据。编排无法区分,这正是我们想要的属性。
保持编排的可移植性
可移植性不是免费的;必须强制执行,否则一旦有人寻求便捷捷径,它就会侵蚀。两条规则最重要:
- 编排中不得有隐藏的非确定性 没有墙钟读取,没有随机值,没有直接 I/O。任何非确定性的事物都是Steps方法,因此它成为运行时接管控制的点。
- 编排中不得使用 Node.js 或运行时特定的 API 编排和Steps接口不得导入任何将它们绑定到进程模型的东西。仅限 Node 的模块(HTTP 客户端、文件/CSV 解析器)属于StepsImpl,绝不放进编排或接口中。
这些规则允许相同的编排在生产环境和评估中安全重放。我们使可移植形态成为阻力最小的路径:函数defineAgentHandle只给你steps和input来处理,因此编写代理的自然方式已经是正确的。
生产和评估适配器
将编排简化为接口上的函数,运行时适配器就是“提供Steps并调用函数”。两个适配器承担重任:Temporal 用于生产持久性,进程内适配器用于评估。
Temporal 适配器
Temporal 将代码分为两个世界:activities(工作流步骤),它们在普通的 Node.js 进程中运行,可以进行 I/O,以及workflow代码,它在确定性沙箱中运行,其影响外部世界的唯一方式是分派活动。我们的适配器干净地将模式映射到这种划分上:每个Steps方法变成一个活动,编排在沙箱内运行。
图2. Temporal 适配器序列图。来源:作者创建。
在沙箱中调用steps.foo(...)会被分派为 worker 上的持久活动。每个代理的 Proxy 重新添加agentName前缀,因此编排保持对运行时无感知。
worker 端在常规 Node 进程中运行。它实例化每个代理的具体 Steps,并注册每个方法为 Temporal 活动,扁平化到一个由代理前缀命名的字典中(例如classify_business_enrichWithWebData),这样两个代理定义相同的方法名就不会冲突。worker 的完整源代码在worker.ts。
编排端是在沙箱中运行的部分。它从不导入StepsImpl,因此这里没有东西传递性拉入 Node 模块。这就是前面提到的构建时强制。如果编排意外依赖了仅限 Node 的模块,这个 bundle 将无法构建。编排代码的完整源代码在workflows.ts。
Temporal 适配器为代理的编排函数提供 Temporal 活动作为 Steps 接口的实现。每次编排调用方法时,Proxy 拦截访问并前置代理名称,将普通方法映射到工作器注册的带前缀活动(因此 steps.enrichWithWebData 映射到 classify_business_enrichWithWebData)。
一旦客户端启动 agentWorkflow,编排内部的每个 steps.foo(...) 调用都会作为 Temporal 活动分发,并具有重试、超时和重新部署时的重放功能。编排代码完全不知道这一切的发生。
这一层加固了我们的长期运行代理。它们每天运行约一百次,每次耗时二十到六十分钟,跨越数十次 LLM 和工具调用。在旧设置下,运行过程中任何地方的 Pod 回收、部署或超时都会导致整个运行失败,近百分之四的运行从未完成。现在,当工作器在运行中途死亡时,Temporal 会重放历史并恢复到最后一个完成的步骤;在过去的几个月中,完成率保持在 99.9%。
评估适配器
评估适配器要小得多。没有沙箱、没有工作器、也没有任务队列。它在进程中运行 相同 的编排,使用 Steps 实例,其插件返回固定数据而不是访问真实服务。您可以在 run-eval.ts 中自行查看。
// Runs orchestration in-process with a real Steps instance, but with plugins
// that return fixture data. Same orchestration as production — byte-for-byte —
// only the runtime underneath differs.
export async function runEval<Input, Output, Steps>(
handle: AgentHandle<Input, Output, Steps>,
StepsClass: new (plugins: Plugins) => Steps,
input: Input,
fixtures: Record<string, string>,
llm: Llm,
): Promise<Output> {
const plugins: Plugins = {
webScraper: new MockWebScraper(fixtures),
llm, // LLM calls stay real — they are what we are evaluating.
};
return handle.orchestration(new StepsClass(plugins), input);
}因为 runEval 只是一个 (input) => Promise<output> 函数,任何评估平台都可以将其作为黑盒包装。参见 braintrust-eval.ts 了解使用 Braintrust 运行评估的示例。
Braintrust 只是一个示例,用于说明编排本身对评估框架或运行时一无所知。Laminar、内部工具或普通脚本可以以相同方式插入。这种方法让我们能够以低成本试验评估平台。
收益与成本
这种架构与软件中的任何其他架构一样,都有权衡取舍。
我们的收获
- 不存在评估与生产偏差,因为构建设计如此。您评估的编排就是您发布的编排,因此在评估中针对分支调整的内容不会与生产环境中的内容悄然不同。
- 运行时选择变得可逆。执行时使用 Temporal 或 Restate,评估时使用 Braintrust 或 Laminar 或 LangSmith。这些是适配器替换,而不是重写。我们不止一次更换评估平台,而代理未受影响。
- 运行时复杂性成为一次性平台成本。开发人员针对 Steps 接口编写普通 TypeScript。他们无需学习 Temporal 的确定性规则或评估 SDK 即可交付代理。新贡献者的入门是熟悉接口,而不是运行时。
- 可靠性和业务影响随之而来。通过吸收瞬态基础设施故障,Temporal 适配器将长期运行的成功率从约百分之九十六提高到 99.9%。在业务方面,基于此平台构建的代理现在为超过一半的入职申请生成自动决策。
我们的取舍
- 无法直接访问运行时原语。编排不能直接调用 Temporal 信号、查询或计时器,因为它们在评估运行时中不存在。任何运行时原生内容都必须通过接口建模,这有时会迫使采用不如原生 API 优雅的抽象。
- 每个运行时功能都必须通过间接层添加并传播。暴露新功能需要将其设计到接口中,并在所有适配器中实现。新功能是平台范围内的变更,而不是一行代码。
- 开箱即用的可视化工具。框架原生的图形可视化器和步骤调试器假设您按照其模型编写。当您的编排是接口上的普通函数时,除非您自己构建,否则您将放弃这些功能。
当您拥有多个工作流、真实的持久性需求和严肃的评估实践时,这种模式的价值才会体现。对于承载大量代理并面对监管决策的平台,将运行时视为接口背后的插件一直是设计决策,它让生产可靠性和评估速度不再相互制约。
关于作者
Mateus Moury
显示更多显示更少
此内容属于 AI、机器学习与数据工程 主题
相关主题:
- Stripe 使用图搜索和状态机自动化数据库修复
- Project Valhalla 首个预览版:JEP 401 重新定义 Java 对象的 ==
- Pinterest 如何通过集中式 Terraform 管道大规模保护 AWS 基础设施
- Canva 分享基于 S3 的架构,用于跨数亿会话的会话撤销
- Vercel Labs 发布 Zero:一种面向 Agent 编写的图优先语言
- 文化与方法趋势 2026:AI 工程中的人性化方面
InfoQ 通讯
每周二发送 InfoQ 上周内容摘要。加入超过 25 万资深开发者的社区。 查看示例