跳到主要内容

异步智能体工作流:长运行任务设计

· 阅读需 11 分钟
Tian Pan
Software Engineer

大多数 AI agent 演示都在单个 HTTP 请求中运行。用户发送消息,agent 推理几秒钟,然后返回响应。干净、简单、易于理解。接着,有人要求 agent 执行需要 8 分钟的操作——运行测试套件、从 20 个网页起草报告、处理一批文档——于是整个架构悄然崩溃。

30 秒壁垒是真实存在的。云函数会超时。负载均衡器会断开空闲连接。移动端客户端会进入休眠。标准的 agent 框架都没有记录当你的任务生命周期超过传输层时该怎么办。它们中的大多数都会静默失败。

本文介绍了弥合同步 HTTP 与长耗时自主工作之间差距所需的架构。这些模式并不新鲜——分布式系统工程师几十年来一直在解决这些问题——但在大多数 AI agent 技术栈中,它们系统性地缺失了。

根本性的不匹配

HTTP 是为同步的请求-响应交互而设计的。客户端连接、发送请求并等待。服务器处理请求并返回响应。连接关闭。服务器遗忘。

当工作在 1 秒内完成时,该模型运行良好。如果你愿意接受糟糕的用户体验(UX),它可以优雅地降级到大约 30 秒。除此之外,你就是在与协议作对。

Agent 工作流在几个方面打破了这一模型。执行多步研究任务的 agent 不仅仅是在等待单个 API 调用——它正在运行一个推理步骤循环,其中每一个步骤都可能触发耗时各异的工具调用。在工作流中间等待人工审批的 agent 可能会闲置数小时识别。处理批量文档的 agent 所做的工作与输入大小成正比,而不是单个查询的复杂度。

幼稚的解决方法是增加超时时间。将负载均衡器设置为 5 分钟并寄希望于一切顺利。这在某些时候管用,直到它失效——而且失败模式是静默的。客户端超时,agent 在后台继续运行但无法检索其输出,用户则收到错误。如果客户端重试,你现在就有两个 agent 在运行同一个任务。如果该任务具有副作用——发送电子邮件、写入记录、调用 API——那你就麻烦了。

异步任务模式

长耗时 agent 任务的正确架构与任何长耗时后台任务使用的模式相同:接受工作,立即返回一个句柄,让客户端在结果就绪时进行检索。

流程如下:

  1. 客户端提交任务。服务器验证请求并将工作排入队列。
  2. 服务器立即返回 task_id —— HTTP 响应是 202 Accepted,而不是 200 OK
  3. Agent 在后台异步运行。任务状态在外部持久化。
  4. 客户端轮询 GET /tasks/{task_id} 来检查状态,或者在任务完成时接收 Webhook 通知。

这将提交延迟与执行延迟解耦。客户端的连接只需要维持到接收到任务 ID 即可。之后的一切都是异步的。

任务状态机很简单:pending → working → completed | failed | cancelled。一旦任务达到终态,它就变得不可变。这对正确性至关重要——你绝不希望客户端在重试期间观察到状态从 completed 回退到 working

幂等键:为什么 Agent 比 API 更需要它们

标准的 API 设计建议为变更端点提供幂等键。对于 agent 来说,这是不可或缺的。

原因如下。当客户端提交一个 agent 任务,且网络在 202 响应到达之前中断时,客户端不知道任务是否被接受。正确的行为是重试。如果没有幂等键,该重试会派生出第二个运行相同任务的 agent。考虑到副作用,这是一场灾难。考虑到昂贵的 LLM 调用,这也是一种浪费。

模式如下:客户端在每次任务提交时附加一个 Idempotency-Key 请求头(由它们生成的 UUID)。服务器存储从该键到任务 ID 的映射。如果同一个键在 TTL 窗口内(通常为 24 小时)再次到达,服务器将返回现有的任务 ID,而不会创建新任务。然后客户端可以轮询该任务 ID,并发现工作已经完成。

对于 agent 特有的用例,还有一个额外的细节。如果你的 agent 本身调用了具有副作用的工具,那么每次工具调用都应该携带一个由任务 ID 和步骤编号派生出来的幂等键。这使得单个工具调用可以安全地重试而不会导致重复。一个通用的方案是:将 {task_id}:{step_index} 进行哈希和编码作为键。如果 agent 在执行过程中崩溃并从检查点恢复,那么在同一步骤重试的工具调用将产生相同的效果。

轮询 vs. Webhooks:实际决策框架

采用异步模式的团队通常希望立即实现 Webhook。Webhook 感觉很高效——服务器在工作完成时推送通知,而不是让客户端在轮询循环中消耗请求。它们确实高效,但并不可靠,而且失败模式很微妙。

Webhook 会被防火墙规则拦截。当接收端点暂时关闭时,它们会静默失败。重试时它们可能会乱序到达。它们要求客户端运行一个公共可访问的 HTTP 服务器,这在某些环境下是不可能的(移动端客户端、在开发机上运行的脚本、NAT 后面的工具)。

实际的建议是:将轮询作为单一事实来源,将 Webhook 视为一种优化。

轮询意味着客户端定期调用 GET /tasks/{task_id} 直到看到终态。这虽然枯燥但很可靠。缺点是轮询间隔期间的延迟和请求浪费。使用指数退避——从 1 秒开始,上限为 30 秒——你在 API 指标中几乎察觉不到它。

对于能够接收 Webhook 的客户端,它可以减少轮询开销。当任务完成时,服务器向客户端的回调 URL 发送一个经过签名的 POST 请求。如果 Webhook 成功了,那很好。如果失败了或者客户端没有注册,轮询兜底机制会捕获到结果。这两种机制可以正确组合:客户端持续轮询直到 Webhook 到达,然后停止轮询。

对你的 Webhook 进行签名。包含一个事件 ID 以便去重。从服务器端使用指数退避进行重试。提供一个重放端点,以便客户端可以重新获取错过的事件。

状态持久化与检查点-恢复模式

长时运行的智能体(Agent)工作流中最常见的失败模式并非 LLM 错误,而是基础设施中断。服务器重启、网络分区导致运行中的作业被孤立、云端竞价实例在任务执行中途被抢占。如果你的智能体状态仅存在于运行循环的进程内存中,任何这类事件都意味着必须从头开始。

解决方法是设置检查点(Checkpointing):在每个关键步骤之后将智能体状态保存到持久化存储中。“状态”指的是智能体在无需重复工作的情况下恢复所需的任何信息——步骤索引、到目前为止产生的中间输出、内存内容以及已经执行过的工具调用列表。

检查点-恢复工作流如下所示:在执行每个工具调用之前,智能体持久化一个检查点。如果进程重启,它会加载最近的检查点并从该步骤恢复。处于已记录检查点步骤的工具调用会被跳过(因为它们已经完成且输出已存储)。智能体从中断的地方继续执行。

这要求工具调用必须是幂等的,或者将其输出作为检查点的一部分进行存储。对于非幂等工具,你需要在再次调用之前检查该调用是否已经发生。模式如下:在任务记录中存储已完成工具调用的日志及其输出。在任何工具调用之前,先检查它是否已在日志中。

检查点间隔是一种权衡。更频繁的检查点意味着失败时损失的工作更少,但 I/O 开销更大。对于大多数智能体工作流,在每个工具调用边界设置检查点是合理的。

大多数智能体框架错在哪里

上述模式在分布式系统中已为人熟知。令人惊讶的是,它们在大多数智能体框架中的实现却非常糟糕。

主要的失败模式是将智能体循环与 HTTP 请求生命周期混为一谈。像 LangChain 和基础的 LlamaIndex 配置,都是在请求处理程序内部同步运行完整的智能体循环。这在演示(Demo)中可行,但在生产环境中,对于任何耗时超过 30 秒的任务都会崩溃。

第二种失败模式是在任务创建层缺少幂等性。不支持幂等键(Idempotency keys)的框架会让客户端无法安全地重试失败的提交。当团队开始看到由于网络级重试导致的重复任务执行时,才会发现这个问题。

第三种——也是最隐蔽的——失败模式是内存状态。如果智能体将工作状态作为 Python 对象存储在运行进程中,那么在崩溃时就没有恢复路径。当进程重启时,任务就消失了。客户端会永远持续轮询却得不到任何响应。没有错误,没有终端状态,只有沉默。

如果你正在使用的框架没有解决这些问题,务实的解决方案是在其上添加一个薄薄的协调层。Postgres 中一个简单的任务表(id, idempotency_key, status, checkpoint_data, result)就能处理这三种失败模式。框架代码在工作进程(Worker process)中运行,该进程从队列中加载任务,向该表记录检查点,并在完成或失败时更新状态。HTTP API 从同一张表中读取数据以响应轮询请求。

根据任务时长匹配基础设施

并非每个智能体任务都需要完整的异步处理。任务队列和轮询的开销并不是免费的。

一个有用的经验法则:10 秒以内的任务采用同步执行;1 小时以内的任务采用带轮询的异步执行;对于需要跨越数小时或数天且包含人机协同步骤的任务,则使用专门的工作流引擎(如 Temporal、Dagster 或类似工具)。

1 小时的界限很重要,因为当客户端保持会话并积极轮询时,基于轮询的异步工作效果良好。对于耗时数小时的任务,客户端经常会断开连接并重新连接。你需要跨会话的状态可见性——这意味着任务需要被无限期持久化,直到它们被明确确认,而不只是到轮询会话结束。

对于跨越数小时或数天的任务,持久化工作流引擎提供了临时任务队列所不具备的功能:持久化定时器、人工输入步骤、用于在失败时回滚已完成步骤的补偿逻辑以及审计日志。LLM 循环变成了更大工作流定义中的一个组件,而不是整个程序。

结语

在演示中运行的智能体与在生产环境中运行的智能体之间的差距,主要是一个基础设施问题。LLM 推理代码往往是简单的部分。难点在于当推理耗时超过连接保持时长、当进程在任务中途崩溃、当客户端重试一个已经通过的请求时会发生什么。

解决这些问题的模式——异步任务队列、幂等键、持久化检查点、带 Webhook 加速的轮询——并不新鲜。它们与支付处理、报告生成以及任何其他异步工作的系统中所使用的模式相同。将它们应用于智能体工作流,与其说是关于 AI,不如说是关于严肃对待分布式系统。

框架会跟上的。在它们赶上之前,由你自己构建这一层并不是过早优化。这是一个智能体在生产环境中可靠运行,还是仅能支撑到第一个真实工作负载到来之间的区别。

References:Let's stay in touch and Follow me for more thoughts and updates