定时跟进
智能体如何把工作排到之后再做,并把结果送回它来的那个渠道——持久化任务、原子领取与故障恢复。
有些事在一轮对话里答不完:一次要跑几分钟的核查、一个下周二的提醒、一场六周之后才该开启的续约谈话。智能体可以把这类工作登记下来,等结果就绪了再送达。
机制
对话进行中,智能体针对本轮内完不成的工作调用 schedule_followup:要跑几分钟的核查、下周二的提醒、六周后的续约谈话。
任务是 Postgres 里的一行,不是消息队列里的一条消息。待执行的任务是已提交的数据,重新部署或进程崩溃都不会把它弄丢——持久性直接来自本就在用的存储,不额外花钱。
轮询器每隔几秒醒一次,找出到期的任务。代价是精度:任务在下一次轮询时触发,而不是精确到毫秒。对于以分钟到周为单位的跟进,这点损失没有意义。
到期任务用 FOR UPDATE SKIP LOCKED 领取。单个轮询器本来用不着这么写——现在就写对,意味着日后跑第二个实例也不会重复发送,而一条跟进发两遍,赔进去的是客户对发件方的信任。
生成结果。如果领走任务的进程随后挂掉,会有一趟恢复流程把它退回待执行状态,而不是让它永远卡在执行中。
送达时回到请求发起的那个渠道——网页挂件里的一个气泡,或者 Telegram、WhatsApp 里的一条消息。从 WhatsApp 问的,就在 WhatsApp 回。
工具立即返回——客户当下就拿到回复,排期的结果稍后再来。
这个工具不会阻塞当前这一轮。它写一行数据就返回,所以客户当下就拿到回复,排期的结果稍后再来。
为什么队列就是一张数据库表
这里没有 Redis,也没有消息中间件。任务就是 Postgres 里的行,这是一个刻意的取舍:
- 重启不会丢活儿。 待执行的任务是一行已提交的数据。进程可以在半途重新部署,轮询器回来的路上就把任务接着捡起来——持久性随着本就在用的数据存储白拿,而不是靠再引入一套有自己故障模式的系统。
- 少运维一样东西。 单进程部署的场景,一个轮询循环就够了。
代价是延迟精度:任务在下一次轮询时触发,而不是精确到毫秒。对于以分钟到周为单位的跟进,这点损失没有意义。
领取是原子的
到期任务用 FOR UPDATE SKIP LOCKED 领取。单个轮询器不可能需要这个——但现在就写对,意味着日后跑第二个实例不会重复发送。一条跟进发两遍不是无伤大雅的小毛病:客户会收到两遍「您的续约到期了」,然后不再信任发件的这一方。
SKIP LOCKED 还意味着一个跑得慢的任务不会把后面的队列堵住,其他工作进程会跨过被锁住的那行,去拿下一个。
故障恢复
任务被某个进程领走、而这个进程随后挂掉,它本会永远卡在执行中。一趟恢复流程会把卡住的任务退回待执行,让它们被重试,而不是无声无息地丢掉。
送达跟着来路走
结果会被推回请求发起的那个渠道,而不是平台随便挑的一个渠道。从 WhatsApp 问的,就在 WhatsApp 回。租户自己发起的定时触达——续约提醒、期限催办——也是同样的机制,按你配置的节奏执行。
这在实际使用中意味着什么
- 智能体说得出「这个我回头答复你」,而且真的做得到。
- 提醒能扛过一次发布、一次重启和一次崩溃。
- 一条跟进只送达一次,并且送到这个人当初跟你说话的地方。