没人叫它、它自己就跑的智能体。
定时智能体没有人可问,这会把你的默认值翻过来:任何含糊之处都必须落到"什么都别做,并且说明白",而你真正会出货的失效模式是沉默,不是报错。本文最难的不是那条 cron 表达式或那次重试——而是在每一次触发时判断,刚发生的事值不值得占用一个人的注意力,因为一个每次运行都要推送一下的智能体,一周之内就会被静音,而那之后它其实已经不在运行了。
把用户拿走,三条默认值就会翻转。
交互式智能体在每个难关都有一个便宜的逃生口:问。把用户拿走——把智能体放到一条 cron、一个 webhook、一个队列或一个文件监听后面——这个逃生口就关上了,而其他每一项性质都没变。三个在交互循环里正确的习惯,在这里变成了错的。
- "问"变成"弃权"。当智能体无法消解一处含糊,交互式的答案是提一个澄清问题,无人值守的答案是停下、什么都不做,并且准确记下它没能决定的是什么。一个会猜的无人值守智能体,就是一个在凌晨三点、没人在读的时候做错事的智能体。
- "重试"变成"对账"。交互场景下,一个失败的动作被重试,双发了用户会注意到。无人值守时,同样的重试跑在一个没人看着的世界里,重复要等到后来由会计发现。每一次写入都需要一个键和一次回读——见幂等与重试——而且这里的键必须跨触发稳定,而不只是在一次触发内的多次尝试之间稳定。
- "汇报"变成"配给"。交互场景下输出多是好事。无人值守时,输出是对某个人注意力的一笔支取,而这份预算很小,且不会续。
还有那个定义性的性质:定时智能体默认是静默失败的。一个坏掉的交互式智能体,几分钟内就会产出一位恼火的用户。一个自周二起就一直报错的夜间智能体,什么都不产出——而"什么都没有"恰恰也是一个健康的安静夜晚产出的东西。如果你只从本文实现一件事,就实现"区分这两种什么都没有"。
触发语义是一项设计决策,不是一个配置字段。
你继承来的那个调度器——cron、工作流引擎、webhook、队列消费者——替你做了四个决定,而这四个你都应该自己刻意去做。
- 重叠。第 n+1 次触发到了而第 n 次还在跑,会怎样?对智能体来说答案几乎总是跳过而不是排队:智能体的运行时长是长尾的,排队会把一次慢运行变成一场惊群,而两个实例对着同一个收件箱推理,两个都会动手。取一个具名租约,TTL 长于你的任务时长视野,让第二次触发立刻退出,并且退得足够响亮以便被计数。
- 错过的窗口。故障恢复之后,智能体要不要补跑?默认不要。把跳过的十一小时补回来,正是一个分诊智能体发出十一条重复评论的方式。确实需要补跑的地方,就用一次触发配一个加宽的时间窗,而不是用原窗口跑 n 次。
- 投递保证。事件驱动的触发通常是至少一次,所以糟糕的日子里同一个 webhook 会来两遍。这没问题,当且仅当这次运行端到端幂等;而对智能体来说,幂等指的是副作用带键,不是推理有确定性——推理从来没有。
- 抖动与对齐。所有排在整点的任务,会在整点一起砸向你的模型服务商。把触发时刻打散,你就把一起限流事故变成了寻常流量;见限流与服务商容量。
只对推不了的东西去轮询。对一个支持 webhook 的邮箱做五分钟一次的轮询,每天要花掉 288 次模型调用去发现"什么都没有",而成本还是其中较小的那一半——那 288 次触发里的每一次,都是一次做出多余动作的机会。非轮询不可的地方,就在智能体前面放一个廉价的、不含模型的判据,让昂贵的循环只在真有变化时才启动。
两次触发之间,世界动了,而你的笔记过期了。
交互式会话攥着自己的上下文。定时智能体醒来时进入的是一个在它离开期间已经变过的世界,手里却拿着一份它为先前状态写下的摘要。那份摘要是系统里最危险的输入,因为它流畅、可信、出自本人之手,而且已经陈旧。
- 永远读新的。每一次触发的第一个动作,都是从权威系统加载当前状态,而不是从智能体自己上次的输出加载。把上次运行的笔记当作"该去哪里看"的提示,绝不当作"什么是真的"的事实。
- 传递水位线,不要传递叙事。两次触发之间正确的交接是小而结构化的:最后处理的 ID、最后见到的时间戳、一组已经动过手的键。一段"我上次做了什么"的散文,等于邀请模型从自己的虚构里推理。紧凑的结构化状态也正是让一次触发可恢复的东西——见持久状态与可恢复性。
- 预期环境已经漂移。工具会被改名,权限会被撤销,schema 会多出一个字段。交互式智能体会把这些变成一句用户抱怨;定时的会把它们吸收掉。给每次触发盖上工具目录的哈希,让变化可见——就是第三方工具漂移里的那套纪律。
- 给累积设界。任何被智能体跨触发追加的状态——一个记忆文件、一份滚动摘要、一张任务清单——在没有用户修剪的情况下只会长,而且它会悄悄吃掉上下文预算,直到质量莫名其妙地下降。按大小封顶,按时间过期,装不下时告警。
"要不要通知"这个决定才是产品。
这是团队投入不足、而用户据以评判的那一部分。定时智能体与人之间的全部界面,就是它选择发出的那条消息,而注意力是一份不会自动续上的预算。连着两周多发一条通知,这个频道就会被静音;从那一刻起,智能体在运行却不再起作用,而你的监控不会有任何一处这么说。
把"发不发"做成一个显式的、可测试的步骤,而不是提示词里涌现出来的性质:
- 每次触发以一个判定结束,而不是以一份报告结束。三个取值就够:
acted(动了)、no-op(无事)、blocked(卡住)。判定是外壳记录下来的结构化字段;散文是可选的,附在它后面。 - 按状态转移通知,而不是按状态通知。"仍然健康"不是新闻。"连坏三夜之后恢复健康"是。与上一次判定做差分,变了就发——这顺带白送你一条自然的恢复通知。
- 把连续的无事合并。一串安静的触发是一行,不是四十行。在日志里保留全部保真度,在通知里剔除它们。
- 凡是
blocked一律通知。因为决定不了而停下的智能体,背后正等着一个人类决定,而它恰恰是最常在摘要里被淹没的那一种。 - 把读者会据以行动的东西放在最前。对多数收件人来说,第一句就是整条消息;把发现放在那里,而不是放"任务跑过了"这件事。
- 给频道本身限流,独立于智能体自己的判断。每天最多 k 条通知、溢出的合并成摘要,这条控制能在智能体出 bug 时活下来——因为你要防的失效,恰恰就是一个认定"什么都很紧急"的智能体。
这件事在设计一侧的镜像是异步智能体的交互设计:同一个问题,换成收件人的体验来陈述。
没人看着计价表时的爆炸半径。
无人值守的执行,拿走了那个会注意到"这东西已经跑了一小时"的人。有两样成本会在黑暗里失控,而两样都需要由运行时来强制限制,而不是在提示词里请求。
支出。按次触发的步数预算与令牌上限,外加同一条排程所有触发的滚动日上限。只有按次上限是不够的:经典事故是某个触发器开始以远超预期的频率发火,而每一次单独运行都在预算之内。两条限制都属于外壳——道理见循环内的成本控制,再加上这里的一点:没有人可以按停止键。
效果。把每次触发允许的对外写动作数量,封顶在一个你刻意选定的数字上,并且把超限做成 blocked 判定,而不是截断。一个开了三张工单的智能体在干活;一个开了三百张的智能体本来是要开三千张的。这条限制能抓住那些再小心的提示词也防不住的循环病态。
然后确保一个人真的能把它停下来。定时智能体的紧急开关必须在每一次触发的开头去查一个服务端取值——如果下一次触发要靠一次部署才看得见,那么部署配置里的一个标志位就不是紧急开关。而且"停掉排程"和"停掉智能体"是两回事:团队经常暂停了 cron 条目,webhook 触发却还在源源不断地到。
盯"没有发生",而不只是盯报错。
标准监控对坏事件告警。这里的特征性失效是事件的缺席,所以监控必须反过来做。
- 对缺失的触发告警。为每条排程配一个"守尸开关":若在 1.5 个周期内没有记录到任何一次运行,就呼叫。这一条告警就能抓住"调度器、凭据或容器悄悄停了"这一整类失败,而几乎没人在第一天就配上它。
- 每一次触发都要有追踪和判定,包括无事的那些。带追踪的无事是健康的证据;没有记录的无事,与"根本没跑"无法区分。见追踪与可观测性。
- 把无事比例当作质量指标来追踪。一条只在 2% 的触发上动手的排程,多半在烧钱,也许该换个更便宜的触发方式;一条 100% 都动手的,很可能范围划得太小,正在做一些迟早会有人来争的事。盯这个比例的变化,而不是它的绝对值。
- 维护一份带负责人与到期日的排程登记册。定时智能体会堆积、会活得比它的作者更久,还会一直花钱。每条排程都要有一位具名负责人和一个复审日期,无主的排程就停掉而不是被继承——这正是智能体清册与登记里的论点,只是被"这些东西没人调用也会跑"这一事实磨得更锋利。
- 改动之前先回放。因为触发是无人值守的,一次提示词或模型改动会在没有用户能接住的情况下落到生产。拿新配置重跑最近一百次触发,把判定做差分;一个把十二次无事变成十二次动手的改动,会在你的用户之前先告诉你一些事。
四条控制,按这个顺序。今天就给每条排程装上守尸开关——你看不见的那种失败,正是已经在发生的那种。让每次触发以一个结构化判定 acted / no-op / blocked 结束,按它们之间的转移来通知,并且独立于智能体自身判断给频道限流。给外壳设一条"每次触发的对外写动作数"硬上限,以及一条跨触发的滚动日支出上限,两者都在提示词之外强制。并且把"弃权"定义成一切含糊之处的既定行为:一个停下来并说明原因的无人值守智能体是在正确工作,而一个去猜的不是——不管那一猜后来有多准。
相关:并发与扩缩谈许多排程同时开火会怎样,智能体的事故响应谈那次在夜里跑歪了的运行,后台编码智能体是这一模式发展得最成熟的实例,以及人在环中谈当房间里没有人时,那个人该被放在哪里。