跳到主要内容

长任务最容易出问题的地方不是规划,而是恢复

· 阅读需 5 分钟
一介布衣
全栈开发者 / 技术写作者

补档说明:本文属于「AI 工程落地周记」系列,计划发布时间为 2025-08-06 20:15。当前先保留为草稿,后续补充真实案例、代码片段和复盘细节后再发布。

现在大家做长任务系统,最容易把注意力放在“怎么规划得更聪明”上:

  • 怎么拆步骤
  • 怎么给模型更好的计划模板
  • 怎么让 Agent 看起来更会思考

这些当然重要,但我后来在线上遇到更多真实事故后,结论越来越清楚:长任务真正先出问题的,通常不是规划,而是恢复。

因为只要任务跨分钟、跨系统、跨多个工具执行,失败就不是偶发事件,而是常态。你不可能要求:

  • 外部接口永远不超时
  • 模型永远不空输出
  • 机器永远不断连
  • 队列永远不抖动

这时候系统能不能继续跑下去,关键就不在“第一次计划得多漂亮”,而在“中途挂掉之后能不能从正确的位置接着干”。

一个长任务最怕的不是失败,是“不知道失败到了哪”

我印象很深的一次,是一条内容生产链路,单次执行可能会持续十几分钟:

  • 收集输入资料
  • 生成草稿
  • 跑规则检查
  • 进入人工待审
  • 发布到站点
  • 触发索引更新和通知

有一次任务在“发布成功、索引更新未完成”这个状态挂掉了。
这时最麻烦的不是重新跑,而是团队根本说不清:

  • 页面到底有没有发出去
  • 索引是不是已经开始写了
  • 重跑会不会重复发通知
  • 审核状态要不要重新走

也就是说,系统失败了并不可怕,可怕的是恢复点不明确

为什么规划常被高估

规划之所以容易被高估,是因为它最容易展示:

  • 屏幕上能看到“思考过程”
  • 很容易给出分步计划
  • 人会天然相信“计划越完整,执行越稳”

但长任务真正让系统变脆的,往往是下面这些更底层的问题:

  • 状态是不是可持久化
  • 每一步是不是可重入
  • 副作用节点是不是幂等
  • 中途重启后是不是知道从哪恢复

这几件事不补,计划再漂亮也只是第一次跑看着顺。

我现在把长任务设计成 4 个恢复问题

只要任务执行时间一长,我就会逼自己先回答下面 4 个问题:

1. 当前进度怎么被机器读懂

不能只靠模型上下文记住“已经做到哪里了”。
必须有外部状态,例如:

{
"jobId": "publish_20250806_01",
"status": "running",
"currentStep": "update_search_index",
"completedSteps": [
"collect_context",
"generate_draft",
"review_passed",
"publish_site"
],
"attempt": 2
}

2. 哪些步骤可以安全重跑

不是所有步骤都该统一重试。
我现在会把节点分成三类:

  • 纯计算节点:可以直接重跑
  • 幂等副作用节点:可以带幂等键重跑
  • 非幂等副作用节点:必须人工确认或先查状态再继续

3. 恢复时需要的上下文是什么

很多系统喜欢把“恢复”理解成重新把整段历史上下文丢回模型。
这通常既贵又不稳。

我更倾向于恢复时只带:

  • 当前任务目标
  • 已完成步骤
  • 上一步关键结果
  • 需要继续执行的下一个节点

也就是把恢复上下文压成“机器可执行摘要”,而不是“聊天记录回放”。

4. 谁来判断任务真的恢复成功了

恢复不是把进程重新拉起来,而是确认系统重新回到一致状态。
例如:

  • 页面已发布
  • 索引已更新
  • 通知未重复发送
  • 审计日志补齐

没有这些验证,所谓恢复只是“程序又动了”。

一个更靠谱的恢复执行方式

后来我们把长任务执行器改成按 step checkpoint 驱动,而不是一口气跑到底:

async function resumeJob(jobId: string) {
const job = await jobStore.get(jobId)

switch (job.currentStep) {
case 'generate_draft':
return runGenerateDraft(job)
case 'update_search_index':
return runSearchReindex(job)
case 'send_notifications':
return runNotification(job)
default:
return markJobFailed(jobId, 'unknown_step')
}
}

这段代码的关键不在语法,而在思路:恢复从来不是重新开始,而是从明确状态继续。

长任务最容易漏掉的两个基础设施

心跳和租约

任务跑得久,就必须知道“它还活着吗”。
没有心跳,你根本分不清任务是慢、卡住了,还是执行器已经死了。

审计日志

恢复时最怕没有证据。
至少要能回答:

  • 哪一步开始了
  • 哪一步完成了
  • 用了哪个输入版本
  • 失败时错误是什么

这些信息比“完整思维链”更有用。

一个我现在很少再做的错误

以前我总想让模型自己重新规划,然后接着往下跑。
后来发现这会把恢复变成第二次不确定执行。

同一个任务第一次和第二次规划结果不一样时,你根本无法保证恢复后的一致性。
所以现在我更倾向于:

  • 任务图由系统定义
  • 模型只负责节点内推理
  • 恢复由状态机驱动

这样系统才知道自己在恢复什么。

总结

长任务最容易出问题的地方不是规划,而是恢复。因为规划解决的是“第一次怎么跑”,恢复解决的是“出事以后系统还能不能接住自己”。

只要任务足够长、工具链足够多、外部依赖足够复杂,失败就一定会发生。真正把系统从 demo 带进生产的,不是更会计划,而是更会从错误状态里回来。