跳到主要内容

你的定时 Agent 有四个时钟,而你信任的是错误的那一个

· 阅读需 14 分钟
Tian Pan
Software Engineer

一个每日站会总结被安排在 UTC 时间 09:00。定时任务(Cron)准时触发。两秒钟后,一个工作节点容器组(Worker pod)启动。LLM 调用又耗费了四十秒的往返时间。模型在撰写总结时认为现在是去年的 2 月,因为那是其训练数据最后确信的时间点。工具层在 UTC 时间 09:00:42 根据挂钟时间(Wall clock)发送了 Slack 消息,模型从未提及具体日期,因为没人要求它这样做。消息进入了正确的频道,昨天的站会笔记被总结成了“今天的”,而且整整三周都没有人察觉。

这并不是任何单一组件的 bug。这是一种在四个不同的时钟之间、谁也没有写下来的契约,而这四个时钟都认为自己知道“现在”是什么时候。

定时任务有它自己的时刻 —— 触发时间,这是系统中唯一在定义上准时的东西。工作节点有一个挂钟时间,可能比调度器晚几百毫秒,如果容器组是冷启动的,延迟会更长。模型有一个基于“感觉”的“现在”,它源自其上下文出现的任何时间戳,加上对其训练截止日期的模糊记忆。工具 API 有它自己的挂钟时间,这可能完全是另一台机器,也是副作用真正发生的时刻。在一个健康的系统中,由于间隔很小且动作粒度较粗,这四个时刻会坍缩为一个。而在一个不健康的系统中 —— 也就是说,在大多数生产环境的 Agent 系统中 —— 间隔成了实际的行为逻辑,团队交付的是一个语义取决于每一轮竞态中哪个时钟获胜的调度器。

四个时钟,一条 Slack 消息

缓慢地梳理从定时任务到 Agent 再到工具的链路,分歧就会显现出来。

调度器的时钟(Scheduler's clock) 是预期的触发时刻。如果你的定时任务设置为 0 9 * * *,契约就是“在 09:00 触发”。大多数调度器在正常负载下能在一秒内履行这一契约。在队列背压或控制平面降级的情况下,触发可能会推迟 —— 原定于 09:00 的任务可能直到 09:04 才真正调用工作节点。这种推迟很少会反馈给 Agent。

工作节点的时钟(Worker's clock) 是运行 Agent 循环的机器上的挂钟时间。在正常情况下,NTP 会将其与 UTC 的误差保持在几毫秒内,但没有明确设置 TZ=UTC 的容器可能会陷入本地时间解析的偏差中,而冷启动的容器组在启动的第一秒内可能会有 100 毫秒或更多的偏移。分布式系统文献几十年来一直在记录节点间的时钟偏移(Clock skew)是一个持续存在的背景问题;Agent 系统继承了其中的每一个痛点。

模型的心理“现在”(Model's mental "now") 是这四个时钟中最难以捉摸的一个。模型无法访问挂钟。它根据你在提示词(Prompt)中放入的内容及其训练截止日期来推理时间。截至 2026 年年中,截止日期散布在过去的 12 到 18 个月之间 —— 不同的供应商、不同的模型、不同的训练运行都锚定在不同的“知识视界”上。当提示词对日期保持沉默时,模型会填入训练期间认为最具代表性的日期。有时是截止月份;有时是更早的月份;有时则在相对时间推理(“两小时后发送”)中出现离谱的错误。

工具 API 的时钟(Tool API's clock) 是副作用真正触发时的挂钟时间。这通常与工作节点不在同一台机器上 —— 你的工作节点调用 Slack 的 API,Slack 用 Slack 的时钟为消息盖戳。如果你的工具层没有携带明确的时间负载,“这件事发生的时间”就是一个除了接收系统的日志之外,在任何地方都不存在的事实。

如果团队没有将这四个时钟命名为独立的实体,那么该系统的时间语义就会演变为:在任何给定步骤中,恰好拥有权威地位的组件说了算。那不是契约,那是等待复合的意外。

看起来不像时间 Bug 的失效模式

这类 bug 保持隐形的隐匿原因是其症状很少看起来像时间 bug。它们看起来更像内容 bug、重试 bug 或“模型幻觉了一个日期”的 bug。以下是几个值得识别的模式:

模型总结了昨天却称其为今天。 定时任务触发了每日站会。提示词没有指定日期。模型写道:“这是团队今天正在做的工作”,然后列出了看起来合理的任务项,因为它们总是看起来很合理,并锚定在模型隐性相信的任何日期上。总结被发布了,粗看没问题,但悄无声息地引用了过期的工单。评估套件(Eval suite)没有模型心理日期与实际日期偏离数月的合成用例,因此评估永远捕捉不到这一点。

“明天的提醒”永远不会触发,或者触发在过去。 模型被要求安排一个后续任务。它根据其内部日期(假设是 2025-08-15)计算出“明天”,并输出 2025-08-16T09:00:00Z 作为计划时间。调度器忠实地尝试触发一个过去的任务,这要么导致静默错误,要么作为补录(Backfill)立即触发。用户明天不会收到提醒;他们现在就会收到,或者永远收不到。模型的行为与其信念是一致的;系统在没有检查这些信念的情况下,给予了它们操作上的权重。

重试跨越了日期边界并落在错误的一天。 定时任务在 23:59:30 触发。Agent 对模型的第一次调用因供应商临时故障而失败。重试策略退避并在次日 00:00:15 触发第二次调用。工具现在发送了一个动作 —— “发布当日总结” —— 该动作落在了第二天的频道中,总结的却是昨天的数据。在文献定义的意义上,重试是幂等的:相同的 Key,相同的调用。但相对于预期的触发时间,它并不是幂等的,因为没有哪一层将该时间作为事实标准(Ground truth)来传递。

夏令时(DST)切换导致重复或漏掉运行。 在时钟拨快的那天,一个安排在“当地时间 2:30”的任务并不存在;它会被静默跳过。在时钟拨回的那天,同一个任务会存在两次并运行两次,这取决于调度器的夏令时策略。工业界已有记录,像 Kubernetes CronJobs 这样的分布式调度器在夏令时边界触发了数千次重复调用,导致了五位数的云成本事故。Agent 在已有的所有时钟问题之上,又继承了这一点。

定时任务所指的“上午 9 点”不是提示词所指的“上午 9 点”。 定时任务表达式是 America/New_York 时区的 0 9 * * *。系统提示词写着“每日上午 9 点的站会”。模型在没有时区上下文的情况下,将“上午 9 点”推理为用户的本地时间,并发送了一条对旧金山凌晨 6 点的读者说“早上好”的消息。两种正确的时区解释产生了冲突,而团队在没有做出选择的情况下交付了两者。

时间即状态。像对待状态一样传递它。

弥合这些差距的模式都有一个共同点:停止将时间视为环境上下文,开始将其视为传递的状态,并将 cron 的意图作为权威来源。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates