有一次线上任务卡在“处理中”,用户等了很久,页面一直转圈。我第一反应是加个定时任务:超过一定时间,把状态改成失败,不就行了?

后来发现,这个想法只对了三分之一。

卡住的不只是模型

任务链路比我想的长:接收请求、创建任务、调用模型、调工具、写结果、更新状态。任何一段都可能慢:模型排队、工具超时、网络抖动、下游服务限流。

最开始我只给模型调用加了超时,以为够了。结果工具调用卡住,任务还是挂着;工具超时了,但任务状态没变;任务状态变了,额度却没退。

“超时”不是一个点,是一条链。

我后来把状态机画了出来

任务不是“成功/失败”两个状态,而是一串:

  • 已提交
  • 排队中
  • 执行中
  • 等待人工确认
  • 成功
  • 失败
  • 已取消
  • 超时失败

每个状态都要回答三个问题:谁能进来、什么条件能出去、出去之后要不要补偿。

比如“执行中”超时,不能直接改“失败”就完事。要先确认这个任务是不是真的没人跑了;如果另一个 worker 还在跑,你把它标失败,它后面又把结果写回来,状态就乱了。

定时任务只是最后一道保险

真正有用的,是三层:

  1. 调用层超时:模型、工具、HTTP 请求都要有超时,不能让一个调用无限等;
  2. 任务层心跳:执行中的任务定期续约,长时间没心跳才认为是死任务;
  3. 兜底扫描:定时扫描超时任务,用条件更新抢占状态,抢到了才做补偿。

第三层不能少,但它不是第一层。只靠定时任务,等于等尸体凉了再报警。

补偿最怕“重复”

任务失败要退额度、退次数、释放资源。第一版没做幂等,结果定时任务跑两次,退了两次。

后来改成:

  • 状态更新用条件更新,只有从“执行中”改成“失败”成功的那一次,才触发补偿;
  • 补偿按业务单号查原记录,不重新发一份;
  • 补偿失败进重试队列,不吞掉;
  • 所有操作带 requestId,重复请求直接返回。

这套东西听起来像“分布式事务”,其实就是。Agent 任务一旦跨服务、跨进程,它就是一个分布式系统,跑不掉。

用户看到的是什么

技术上的状态再多,用户只看到三件事:还在跑、失败了、需要我做什么。

所以我在任务详情里加了:

  • 当前阶段;
  • 最后更新时间;
  • 失败原因(能说人话就说人话);
  • 能不能重试;
  • 需不需要人工确认。

这比在日志里写一堆堆栈有用。

最后一句

Agent 的可靠性,最后都会落到几个很“后端”的问题上:状态、超时、重试、幂等、补偿。

模型决定它能不能做,这些决定它能不能稳定地做完。