长任务Agent需要独立于HTTP连接的生命周期
文章指出,多轮模型调用、搜索和工具执行会把Agent任务拉长至数分钟,使任务生命周期与单次HTTP连接绑定成为架构问题。示例展示了通过AbortSignal组合客户端断连与服务端超时,并向上游请求传递取消信号。
AI 深度解读
用户关掉标签页,AI 就该停止工作吗?普通聊天往往应该停,但如果它正在研究 20 份文档、生成报告,关页面通常不等于取消任务。作者的核心主张是:需要保留结果的 Agent,应该拥有独立于 HTTP 连接的生命周期。
这篇文章值得读的地方,是把「流式返回」和「任务执行」拆开了。前端仍然可以实时显示进度,Node.js 服务也仍然接收请求,但连接不再决定任务的生死。真正需要补上的,是状态存储、明确的取消语义,以及失败后的恢复机制。
关掉页面,为什么不该顺手杀掉 Agent
最初的 AI 接口通常很直接:收到 POST /api/chat,调用模型,返回结果。作者用下面这段代码展示这种常见写法:
app.post('/api/chat', async (c) => {
const result = await runAgent(await c.req.json());
return c.json(result);
});
关键在 await runAgent(...):整个 Agent 的执行被包在一次请求处理里,接口要等它完成才返回。
当 Agent 只调用一次 LLM,这个安排很自然。但它加入搜索、数据库查询、工具调用和文件生成后,原来 5 秒的请求可能延长到 30 秒、2 分钟,甚至 10 分钟。这些时间是作者说明问题的示例,并非性能测试结果。
对于普通流式聊天,浏览器离开后停止生成通常合理。服务端可以用 AbortSignal.any([timeoutSignal, c.req.raw.signal]) 合并超时和客户端断开信号,再通过 await fetch(LLM_URL, { signal }) 传给上游请求。
Agent 的问题是:它做过的事情未必能随连接一起丢弃。
如果搜索已经完成、数据库已经查过、文件已经生成,用户关掉页面,并不代表他放弃了报告。作者因此区分两类操作:一种为了当前连接生成回答;另一种接受用户委托,完成一个有结果、有副作用的任务。
是否需要任务化,先问关闭页面后,这份工作是否还应该存在。
给工作一个 taskId,前端才能放心重连
作者提出,让 POST /api/agents 创建任务并返回 {"taskId":"task_01J..."},随后结束 HTTP 请求。Agent 在独立的执行流程中继续运行。
这里的 taskId 不只是方便查日志的编号。它让工作有了稳定身份,数据库也由此成为判断任务状态的依据。原文给出的模型很精简:
type AgentTask = {
id: string;
status: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
createdAt: Date;
startedAt?: Date;
completedAt?: Date;
error?: string;
};
要关注 status 这一行:任务从 queued、running 进入 completed、failed 或 cancelled,这些状态应由执行情况决定,不能由浏览器连接是否存在决定。
前端对应使用三个入口:
POST /api/agents创建任务,拿到task_123。GET /api/agents/task_123查询当前状态。GET /api/agents/task_123/events订阅实时进度。
作者举了一个直观场景:用户在 12:00 启动任务,浏览器在 12:01 崩溃,12:05 重新打开应用。前端查询同一个任务,可能直接得到 {"id":"task_123","status":"completed"}。原来的 TCP 连接无需一直存活。
流式输出也没有被放弃,只是换了位置。LLM 的输出先进入任务事件流,再由 SSE,也就是服务端发送事件,传给订阅者。事件包括 task.started、tool.started、tool.completed、llm.started、llm.delta、llm.completed 和 task.completed。
浏览器订阅的是进度。一个页面断开,另一个页面仍可继续查看。API 则负责校验输入、创建任务、交给 worker,实际执行由 worker 承担。
但原文没有详细说明事件如何持久化、重连后怎样补齐漏掉的事件。能重新查询任务状态,并不自动等于能完整恢复此前的流式内容。
拆出后台任务后,取消和恢复才真正开始
任务独立以后,断开连接和主动取消终于可以分开。浏览器关闭,只结束订阅;用户明确要求停止,则调用 POST /api/agents/task_123/cancel。
难点在于「停止」到底停到了哪里。
作者的演示会返回 step_cancelled 或 side_effect_committed,用于区分步骤被取消,还是副作用已经提交。如果工具已经写入文件、更新数据库,取消 LLM 流并不会让这些操作自动撤销。工具也需要检查任务的取消信号,并考虑重复执行的安全性。
同样,客户端断开、服务端停止工作、供应商停止推理,是三件需要分别确认的事。一个 AbortSignal 无法证明整个链路都已停止。
作者提供了可运行演示 aiengineering-book/agent-task-demo:让同一个模拟 Agent 分别绑定请求和作为独立任务运行,在中途断开连接、不同阶段取消,以及订阅者停止读取时,检查哪些工作还能保留。这是模拟场景验证,不是生产系统可靠性报告。
真正上线还得处理几类老问题。
首先是慢订阅者。心跳有助于避免健康连接被代理或负载均衡器按空闲连接关闭,却不能证明客户端还活着。单向流没有客户端回执;当套接字停止排空、写入形成背压时,服务端可以等待一个较短的排空超时,再移除该订阅。作者的演示覆盖了这个处理,任务仍继续运行。
其次是 worker 崩溃。数据库里的 running 不会自己变成失败,必须有机制发现执行者消失,再决定重试、恢复或标记失败。
最后是步骤记录与幂等性。Agent 执行到第四步崩溃,盲目重跑可能重复发邮件、调用 API。即使记录完成状态,也可能出现「邮件已发送,完成记录还没写入就崩溃」的窗口,因此有副作用的步骤需要幂等键等配套设计。
返回 taskId 只是入口改造,持久执行还需要单独设计。
我的判断
作者最有价值的提醒,是把接口设计的判断标准从「耗时多久」移到了「用户是否还需要结果」。耗时和复杂度会暴露问题,但真正决定连接能否拥有工作的,是这份工作的业务语义。
普通聊天继续采用请求内调用、流式返回、断开取消,依然合理。作者引用的前一篇测试中,DeepSeek 在某一天的流式聊天请求里,分别在两个时间点中止,费用随中止位置变化,并且明显低于跑满 token 上限的成本。这个结果只覆盖一个供应商和特定测试条件,不能扩展成所有模型都会及时停算、及时停费的承诺。
相比之下,独立任务适合需要保留产物、产生副作用的 Agent,但会引入状态管理、后台执行和故障处理成本。现有后台任务系统也可以承接这些工作,原文没有给出与具体方案的对照数据,不能据此判断哪种实现更快、更便宜或更可靠。
还有一个容易误读的地方:数据库里存了任务,不代表任务就能恢复。原文把持久步骤、幂等性和崩溃检测列为必须面对的问题,并没有交付完整方案。输入材料中提到的「四种事件」和「三种模式」也缺少具体条目,不宜替作者补成一套确定分类。
能拿来做什么
- 检查现有 AI 接口的退出语义。普通聊天可以随连接取消;文档研究、报告生成等需要保留结果的工作,应考虑返回任务 ID,并提供独立的状态查询。
- 在前端保存任务 ID,让刷新、崩溃后重开能够重新查询和订阅。明确区分「离开页面」与「取消任务」,避免把组件卸载直接当成业务取消。
- 在 Node.js 执行链路中传递任务 ID,让浏览器、API、worker 和供应商调用的日志能够对应起来,同时明确取消接口返回的是停止请求、步骤已取消,还是副作用已提交。
- 给会写文件、更新数据库、发送邮件的步骤设计完成记录和幂等策略,并验证 worker 中途退出后的行为。验收不能只看正常完成,也要看断线、取消和重复执行时结果是否符合预期。