如何让定时任务长期可靠地执行
我们的产品里有不少需要每天执行的任务。例如,审核系统内部任务的进展,跟踪组织知识库的每日变化。这些功能一旦开始使用,用户就会逐渐形成预期:该关注的问题会被提醒,重要的变化不会被遗漏。

最近读到 Lance Martin 和 CJ Avilla 写的 Building effective agent automations,很有共鸣。文章用自动生成工作简报的例子,讨论了保存读取进度、识别来源故障、发送前核验、确认交付、区分偏好与运行状态,以及限制权限和开销等问题。这些原则也适用于我们自己的定时任务。
我想结合上面两个场景,梳理一套设计这类功能时可以反复使用的方法。它们既涉及传统后台任务,也涉及让 AI 参与阅读、判断和总结的自动化流程。
先约定怎样才算做完
假设每天早上九点审核任务进展。程序启动了,查了一批数据,模型生成了总结,整个进程正常退出。这样就算成功了吗?
还需要回答几个问题:应该审核的任务是否都看到了?结论是否有依据?需要提醒的人是否能看到结果?如果一切正常、无需通知,是否留下了本次检查完成的记录?
我会先为每一种定时任务写清楚四件事:检查范围、完成时限、交付结果、异常处理方式。
对于任务审核,检查范围可以包括未完成任务、近期状态变化,以及已经到期的事项;交付结果可以是一份带原始任务链接的待关注清单。对于知识库跟踪,结果则可以是重要内容的变化及影响范围。具体时限和提醒条件由业务决定。
“每天九点执行”还需要约定时区,以及九点触发后最晚什么时候完成。发生延迟时,本次审核属于哪一天,也应有确定的规则。
这些约定决定了系统应该保存什么状态、监控什么指标,以及失败后从哪里恢复。
区分发生了变化和需要继续关注
知识库跟踪很适合增量处理:记录已经处理到哪个位置,下次从那里继续。这样,即使中间停了一天,也有机会补齐积压的变化。
但“位置”必须对应来源能够提供的读取保证。我会优先使用来源提供的变更序号或游标;只有更新时间时,要检查排序、分页以及相同时间戳的处理方式。仅记住本次见到的最大时间戳,不能证明中间所有记录都处理完了。
例如,一次查询有五页数据,第三页读取失败。前两页可以先保存下来,但不能把整批进度直接推进到查询结束时间,否则第三页及后续内容可能再也不会被读取。更稳妥的做法是保留已确认连续完成的位置,再补齐缺失部分。
删除也需要单独考虑。一篇文档被删除后,可能不会再出现在“最近更新的文档”列表里。要捕捉这类变化,需要来源提供删除事件,或者在权限范围稳定的前提下,定期对照完整清单。访问不到文档时,还要区分删除与权限变化。
任务审核则多了一层要求:一个任务没有任何更新,也可能在今天变得值得关注。
假设一项任务周一创建,截止时间是周五。它从周二开始就没有人更新。只扫描“昨天发生变化的任务”,到周六也可能发现不了它已经逾期。
所以我会把任务审核拆成两部分:用增量读取发现状态变化,同时定期重新检查仍未完成的任务。时间流逝本身,也是触发判断的条件。
这也意味着,已经通知过用户的事项,不能简单地从检查范围中删除。是否继续检查,与是否再次通知,是两个独立决定。
让同一次工作在重试时仍有稳定身份
定时调度需要面对重复触发和漏触发。Kubernetes 的 CronJob 文档就明确说明,某些情况下可能创建两个 Job,也可能没有创建,因此任务本身应该具备幂等性,也就是重复执行时不会重复产生同一个业务效果。来源:Kubernetes CronJob
对于每日审核,我会给“某个组织、某种任务、某个业务日期的一次审核”分配稳定标识。失败后重试,仍然处理这一项工作;每一次尝试,再有自己的执行记录。这样才能区分今天的第二次尝试和明天的新任务。
两个执行实例同时开始时,还需要通过数据库唯一约束和原子领取等机制协调。单纯在执行前查询“有没有做过”,会留下两个实例都查到“没有”的机会。
更棘手的是通知。假设消息已经送达,但返回结果时网络断开了。系统只知道请求超时,无法据此认定发送失败。
如果目标接口支持幂等键,就可以在接口规定的有效期和参数约束内,用同一个键重试同一份请求。Stripe 的接口就是这种机制的一个例子。目标接口没有类似能力时,则需要查询回执或核对已有结果;无法确认的请求应保留为待核对状态。来源:Stripe 幂等请求
对我们自己的产品,一个可以考虑的设计是:把审核结果和“待发送通知”一起写入同一个数据库事务,再由独立步骤负责发送。这样,审核完成后即使进程退出,发送工作也不会只存在于内存里。这是事务性发件箱模式的基本思路。来源:AWS 事务性发件箱
这里仍然需要保留两个状态:结果已保存,以及通知已确认发送。数据库事务无法替外部通知平台确认收件,发件箱也不能独自保证消息绝不重复。
如果结果和待发送记录已经可靠落库,读取进度可以在同一个事务中推进,由发送步骤继续履行交付责任。否则,就应等到交付得到确认后再推进。选择哪种方式,关键在于后续责任是否已经被可靠保存。
让判断依据和通知条件可以被检查
AI 可以帮助解释任务讨论、识别阻塞原因、总结文档变化。但最终结果应该能回到具体事实。
例如,在任务审核中,“连续三天没有更新”是一项观察,“项目即将延期”则是一项判断。后者还需要结合截止时间、依赖关系或负责人的说明。缺少这些依据时,可以提醒用户核实进展,不能把推测直接写成已经发生的事实。
我会让每条候选结果关联对象标识、来源版本或更新时间、原始链接,以及触发它的规则。这样,无论是否使用模型,出现错误时都能追溯判断从哪里来。
对知识库也一样。“昨天编辑了三十篇文档”提供的信息有限。如果能进一步说明某份操作规范改变了审批步骤,以及哪些团队需要了解,通知才更有用。这里需要比较内容差异,而不是把编辑次数直接当作重要程度。
通知条件也应该独立于检查条件。一个仍未完成的任务可以每天被检查,但只有风险等级变化、超过新的时间阈值,或者达到用户约定的提醒间隔时,才再次通知。具体规则需要让用户能够理解和调整。
对于准备发出的提醒,还要考虑检查与交付之间的时间差。如果一个任务刚刚完成,先前生成的催办就应该被撤销或更新。进入发送队列很久的结果,也需要在发送前重新判断是否仍然有效。
给规则和历史记录安排不同的位置
长期运行以后,系统会积累许多东西:用户关注哪些项目、某项任务上次何时被提醒、哪些文档已经处理过,以及模型总结出的经验。
我会把业务规则作为独立配置管理,并记录每次运行使用的规则版本。用户调整逾期阈值后,新的审核应使用新规则;回看上周的结论时,也应该知道当时依据的是哪一版规则。
运行历史则用于解释已经发生的事情:读取覆盖到哪里,某条结果为什么被保留或忽略,通知最后是什么状态。模型生成的笔记可以辅助后续判断,但不应自动变成新的业务规则。
对于组织知识库,还需要把权限带入整个过程。能读取某篇文档,不代表能把内容摘要发给所有人。我的设计会要求发送目标与内容的授权范围匹配;历史缓存也要随权限变化重新检查。
任务审核同样需要明确动作边界。识别出一个可能延期的任务,通常足以生成提醒,并不自然意味着系统获得了修改截止时间或重新指派负责人的权限。这些动作应当有各自明确的授权。
让没有消息也有据可查
假设今天没有收到知识库日报。可能是没有重要变化,也可能是来源不可用、处理尚未完成,或者通知还在等待发送。
如果这几种情况在系统里都表现为“没有消息”,用户就无法知道自动化是否还在正常工作。
我会为每次运行分别记录来源覆盖情况、判断结果和交付状态。部分来源失败时,成功读取的部分可以按业务约定继续处理,但结果必须说明覆盖范围;依赖缺失数据的结论则应暂停。例如,任务列表没读全时,就不能宣布所有任务都在正常推进。
监控也应沿着这些状态展开:距离上次完整检查过去了多久,各来源积压到什么位置,有没有长期等待的通知,以及哪些请求还处于无法确认的状态。
Google 的 SRE 文档强调,监控既要观察系统内部,也要检查用户实际看到的行为。应用到这里,我会同时检查后台运行记录和最终结果是否按约定可用。来源:Google SRE 监控分布式系统
监控最好能独立发现“本次运行没有发生”。如果告警只能由定时任务自己发出,那么任务根本没有启动时,也就没人报告问题。
对于正常检查后确实无需通知的情况,保存完成记录就够了。业务提醒可以保持安静,系统健康状态仍然应当可查。
执行时间、重试次数和模型开销也需要上限。触及上限时,要留下停止原因和可恢复的位置,并进入后续处理流程。对于积压很多天的知识库变化,可以分批补齐,避免一次运行无限膨胀。
上线前先演练几种坏情况
对于这些功能,我会在正常流程跑通以后,主动安排几次故障演练。先验证系统能否解释和恢复,再逐步扩大处理范围。
| 演练场景 | 希望观察到的行为 |
|---|---|
| 同一天的审核同时触发两次 | 两次尝试归属同一项工作,协调处理,不重复生成同一份通知 |
| 读取知识库时中间一页失败 | 保留缺口,恢复后能继续补齐,不把未读部分标成完成 |
| 通知送达,但响应丢失 | 进入核对或幂等重试流程,不直接认定失败后再发一份 |
| 任务没有更新,但已经超过截止时间 | 持续检查能发现逾期,按规则决定是否提醒 |
| 今天没有重要变化 | 有完整检查记录,不为了证明运行过而制造通知 |
| 调度停了一天 | 独立监控能发现延迟,恢复后按约定补查积压内容 |
最初可以只选择一个项目、一部分知识库,先把结果保存为内部预览,人工检查遗漏、误报和通知频率。等这些规则稳定,再开放自动通知。
后续新增类似功能时,我会先把检查范围、进度、判断依据、交付状态和恢复方式写清楚,再选择调度框架或模型。用户愿意依赖这些自动化,前提是需要关注的事情能够及时出现,出现的问题也能够被解释和处理。