構建能夠在自身執行中存活的智慧體
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 把智慧體的執行變成可存活資料的方式。