定时 Agent 中途失败后会自己接着跑吗:断点续跑实测

先说结论:定时作业中途被强杀,调度器不会重做被中断的那一次,它只在下一个时间点重新起一个进程。所以”失败后会不会自己接着跑”不是调度器的能力,而是作业自己有没有把进度落到磁盘上。同一环境下的对照实测:没有状态落盘的实现,重启后重复处理了 33/72 个工作单元;有断点续跑的实现,在同样的中断时刻重启后重复 0、遗漏 0,只补了没做完的部分。真正稀缺的不是”能跑通”,是”能接着跑”。

问题从哪里来

翻看 B 站 AI Agent 相关内容的公开评论区后,企业侧从业者的反馈集中在六个方向:

  • 模型输出不稳定、事实边界模糊,结果不敢直接交给业务方使用
  • 业务流程拆不成可执行的工作单元,拆完发现步骤之间的状态传不下去
  • 内网私有化部署在算力受限的条件下达不到预期效果
  • 逐条调用外部接口的成本失控,一次简单问答的消耗也可能超出预期
  • 多步骤任务跑到一半失败,没有人知道究竟做到哪一步了
  • 任务挂掉之后,第二天没人敢确认产出结果是完整的

最后两条其实是同一件事的两面:可观测性和可恢复性。它不像”模型不够聪明”那样显眼,却决定了一套自动化能不能被交出去、能不能无人值守。选它作为验证对象,是因为它可以本地复现、并且可以用数据判定成败。

验证口径与原型设计

把问题收敛成一句可测的话:一个定时作业在运行中途进程消失后,下一次触发时能不能只做没做完的部分。

工作负载

  • 72 个工作单元 = 12 个真实 URL × 6 项检查
  • 每个单元 = 一次真实网络请求 + 一次结果落盘(与企业里逐条调用接口、逐台连接主机的形态同构)
  • 单元实测平均耗时 239 毫秒,最慢 585 毫秒;纯真实 IO 的单轮总时长 17.8 秒

一处如实说明:为了让单轮时长接近企业批量作业的量级,主实验在每个单元后加入 0.25 秒固定间隔,单轮由 17.8 秒拉长到 36.2 秒。这个间隔只影响时长,不影响重复与遗漏的判读——纯真实 IO 的对照组同样完整跑通。

两种实现

  • 朴素实现:一次跑完全部单元,不写状态;任何一个单元出错,整批直接中断
  • 续跑实现:每完成一个单元就把进度原子落盘;重启时读取状态跳过已完成单元;单元出错记录后继续,不拖垮整批

故障注入方式

  • 强杀:进程跑到一半被 taskkill /F 强制结束(对应内存不足被系统回收、部署重启、超时被回收)
  • 外部调用超时:把第 30 个单元的目标替换为不可达地址,触发真实 socket 超时

实测结果

被强杀后的行为对比

  • 朴素实现:18.1 秒时被强杀,已完成 33/72 个单元。随后重跑一次,结果文件累计 105 行,去重后仍是 72 个单元——33 个单元被重复处理,重复率 45.8%
  • 续跑实现:同样时刻被强杀,已完成 30 个,状态文件记录 30 个。重跑只补了剩下的 42 个,耗时 22.4 秒,结果文件 72 行、重复 0;再跑第三次,0.7 秒结束,全部跳过

单元级失败的扩散范围

  • 朴素实现:第 30 个单元外部调用超时,整批中断,最终只完成 29/72,完成率停在 40.3%,后面 43 个单元一个都没做
  • 续跑实现:同一次超时,该单元被记账后跳过,批次继续,最终完成 71/72,留下 1 条失败记录,作业正常收尾

真实调度器上的表现

把续跑实现挂到真实定时调度上,间隔 2 分钟,然后在运行中途强杀:

  • 16:10:52 第一次触发。运行 14 秒后被强杀,调度器记为失败,消耗掉一次运行配额,下一次触发时间推进到 16:13:08
  • 16:13:34 下一次触发自动开始。日志记录”跳过 32 个已完成单元,只处理剩下的 40 个”,18.5 秒结束,结果文件 72 行、重复 0

结论很明确:被中断的那一次,调度器不补偿;但下一个时间点触发时,作业自己把没做完的做完了。

核心踩坑记录

强杀不触发任何清理逻辑

被 taskkill /F 结束是立即消失:没有退出钩子、没有缓冲区刷新、没有”运行结束”日志。运行日志里最后一条 run_start 之后直接断开,下一次的 run_start 里带着”已跳过多少单元”的计数——这是判断上一次死在哪个位置的唯一线索。因此日志必须写成逐单元的追加式记录,只在结束时汇总写一次是不够的。

外部超时的报错原文

单元级失败的报错长这样:URLError: <urlopen error timed out>,超时阈值 8 秒。朴素实现拿到这个异常后判定整批失败;续跑实现把它收进失败清单继续往下走。同一个异常,两种收场。

调度器的语义是”至多一次”

看实现才明白:调度器在开始执行之前就把下一次触发时间往前推了,也就是先记账、再干活。这样设计是为了防止任务重叠和重复触发,代价是已经记过账的那一次,进程死了也不会补做。另外,停机期间错过的多个运行周期会被折叠成一次补跑,而不是把欠下的次数全部放出来。

状态落盘必须是原子写

状态文件如果边写边被杀,会留下一个被截断的 JSON,下次启动直接解析失败——那比没有状态更糟。落地时的必要动作是:先写临时文件,再用重命名替换正式文件。

能力边界:这套方案做不到什么

  • 不补偿被中断的那一次:调度只保证”至多执行一次”,不做补偿执行
  • 失败单元不会自动重试:本次实现是”记录 + 继续”,退避重试需要另外加,而且必须配幂等键
  • 状态文件是单点:它落在本地磁盘上,目录被清、磁盘写满、机器更换,断点信息一并丢失
  • 对不可幂等的外部副作用无效:会重复扣费、重复通知的场景必须先设计幂等键,否则”续跑”会变成”重复伤害”
  • 配额会被失败消耗:带运行次数上限的任务,失败也算一次(实测一次中断就消耗掉一次配额)

适配与排斥场景

适配

  • 逐条处理、结果可被重复覆盖的任务:站点检查、日报汇总、批量巡检、内容批处理
  • 单轮时长超过调度间隔、容易和下一次触发重叠的长任务
  • 需要交给别人运行、要求无人值守的场景

排斥

  • 带不可逆外部副作用的动作:支付、发货、对外通知、写入第三方系统
  • 要求”要么全成、要么全不成”的流程——那需要的是事务,不是续跑
  • 单元之间强依赖且没有唯一标识的任务:没有幂等键就切不出断点

落地交付建议

  • 先定义幂等键:每个工作单元必须有稳定唯一标识,续跑才成立
  • 进度原子落盘:临时文件加重命名替换,不要原地覆盖
  • 失败隔离而非整批中断:单条失败记账后继续,让批次正常收尾
  • 日志写成逐单元追加:判断”死在哪儿”靠的是最后一条记录,不是事后汇总
  • 区分”调度失败”与”单元失败”:前者决定要不要人工介入,后者决定要不要重试
  • 交付前做一次强杀演练——这是唯一能证明”能接着跑”的方式

常见问题

定时任务被强制中断后,下次会自己接着跑吗?
取决于作业自身,不取决于调度器。调度器不会补偿被中断的那一次,只在下一个时间点重新启动作业:作业有断点状态就从断点继续,没有就从头重来。同一环境的对照实测中,无状态实现在重启后重复处理了 45.8% 的单元,有状态实现重复为 0、遗漏为 0。数据来源:2026-09-11 本机真实运行记录。
停机期间错过的多个运行周期会全部补跑吗?
不会。过期作业累积错过的运行会被折叠成一次补跑,下一次触发时间直接推进到未来,避免调度恢复的瞬间集中爆发。这一点在调度实现里是明确写死的语义,不是偶然行为。数据来源:2026-09-11 调度器实现核对与实测。
一个工作单元出错会让整批任务失败吗?
取决于有没有做失败隔离。不做隔离时,实测第 30 个单元的外部调用超时直接中断整批,完成率停在 40.3%,后面 43 个单元一个都没做;做了隔离之后,同一次超时只留下 1 条失败记录,批次完成 71/72 并正常收尾。数据来源:2026-09-11 本机真实运行记录。
断点续跑会不会重复处理数据?
实现正确时不会。前提有两项:每个工作单元有稳定唯一的标识,进度落盘使用原子替换。实测的续跑实现在三轮运行之后,结果文件 72 行,重复 0、遗漏 0;第三次运行时全部跳过,用时 0.7 秒。数据来源:2026-09-11 本机真实运行记录。

下一步

这一轮验证的是”能不能接着跑”,下一轮该验证”接着跑之后产出是否可信”:单元级失败被记录之后,要不要自动重试、重试几次、什么条件下放弃,以及失败清单如何进入人工复核流程。这部分直接关系到这套机制能不能在不看日志的情况下被信任。

本文的全部数字都来自本机真实运行的原始记录:72 个工作单元的逐条明细、调度器两次真实触发的日志、以及被强杀那一刻的状态文件,均留档可查。这套方法可以整体搬到任何”逐条处理 + 定时触发”的场景里。如果你在自己的环境里遇到同类问题,欢迎到 https://www.chenaigeo.com/about/ 页找到我的联系方式交流。