构建能够在自身执行中存活的智能体
Conol 团队分享了一次因 RDS 重启导致 agent 停摆 9 小时的故障,并解释如何借鉴代数效应思想,把 agent 的每一次执行视为可持久化的数据,而不是依赖进程调用栈。文章介绍三种模式——动作即效果、纯函数式状态变更、状态与后续效果原子提交——以及基于 Postgres 的队列、租约和至少一次投递机制。
某天晚上,Conol 的 agent 大约有九个小时没有动弹。没有崩溃,队列里的工作就那么停滞。起因很普通:RDS 重启期间,一个瞬时数据库故障出现在 worker 的轮询循环中。循环不再消费队列,但进程因为仍持有打开的资源而继续存活,没有任何 supervisor 替换它。进程像活了一样,却什么也不做。拯救他们的不是循环本身,而是循环下面的基石:agent 的全部状态和已产生的 effects 都持久化在 Postgres 中。重新启动 worker 后,队列从停下的位置继续;他们不需要从日志重建用户会话或重放对话。这件事引出一个观点:让系统可靠的关键,不是更好的 prompt,而是把 agent 的“一轮执行”当作持久化数据,而不是一段调用栈。
很多 agent runtime 一开始是这样:一个 async 函数持续调用 LLM、执行 tool call,把所有 messages 存在本地变量和调用栈上。这在笔记本上没问题,在生产环境却致命:进程一旦挂掉或卡死,整轮 turn 就丢失。模型再聪明,运行时也取决于持有栈的那个进程有多可靠。
最有帮助的心智模型来自代数效应。它区分“要做什么”和“怎么做”:operation 描述请求,handler 决定含义,函数执行 perform 时不知道如何处理。Effect handler 可以看作可恢复的异常;例如 greet() 执行 AskUser 操作,handler 用 continuation k 返回 "Ada",计算从 AskUser 处恢复。OCaml 5 的 one-shot continuation、monads 等都有对应位置。Moggi 1991 引入 monads;Plotkin 和 Power 提出 effects 的代数描述;2009 年 Plotkin 和 Pretnar 形式化 effect handlers。OCaml 5 的 Eio、研究语言 Koka、Eff、Effekt 都探索了这些思想。React Suspense 和 durable workflow engine 也有类似形状,但不等同。
Conol 没有实现 continuation 或 effect types,只取了分离思想,用持久化数据实现。effect 变成写入 Postgres 的带标签值,比如 { type: "llm", sessionId };handler 变成读取并解释 effect 的 worker;resumption 是重建持久状态后继续,而不是恢复挂起的栈。队列可被检查,测试也能用确定性 handler 替换真实实现。两个字段承载调度:visibleAt 决定 effect 何时可被认领;稳定 id 让重复的定时任务替换已有队列行而不是重复插入。
状态推进通过纯 mutation。mutation 接收 sessionId 和 from state,返回新 state 以及这次转移产生的 effects;drop 表示输入过期,replay 表示把 effect 放回队列。每次提交都带单调 version,写入用 compare-and-swap。如果版本不匹配,runtime 会拿最新 state 重跑 mutation。纯函数让 rebase 安全:mutation 可能被多次求值,但只有赢家提交,且外部 I/O 不能放进 mutation,否则 CAS 重试会重复执行。后续工作以 ops 返回,稍后作为 effects 运行。
状态和后果必须一起移动。Conol 在同一个 Postgres 事务中先 CAS 更新 agent_sessions,再插入 effect_queue,从而保证状态与后续 effects 同时持久化;否则可能状态推进但 effects 丢失,或 effects 提交而 agent 不知道。Worker 从队列领取 effect 时使用 lease:claim_token 和推迟 visible_at;确认、重试、续租都校验 token。这提供至少一次投递,因此结果 mutation 必须幂等地处理重投递:tool result 以 toolCallId 写一次,LLM 流事件绑定 llmCall.id,重复 effect id 替换队列行。文章也特别提醒,重放安全不保证外部工具恰好执行一次;调用支付 API 等副作用需要自己的幂等键。
一个带工具调用的 turn 会变成五次持久化提交:用户 effect 到达后提交消息并发出 llm effect;llm handler 运行模型后提交 assistant 消息并发出 tool_call effect;tool_call handler 运行工具、提交一次结果并发出后续 llm effect;第二次 llm handler 用工具结果提交最终回答;没有剩余效果时结束会话。每两步之间 worker 全消失也没关系,新 worker 可以从最后提交的状态继续。于是崩溃变成重放:worker 认领 effect、开始处理,之后无论发生什么,只要状态提交,系统就能前进;如果还没提交,则 effect 会再次被投递。这就是 Conol 把智能体的执行变成可存活数据的方式。