返回經歷
Intuit · GTM Technology

重放 Agent

一個擁有生產環境寫入權限的 AI agent

一個執行高風險生產資料操作的 AI agent——把數百萬條歷史客戶記錄重新跑一遍 Intuit 的行銷流水線——這項操作過去需要專職人員斷續盯上一到三天。它可以無人值守連續執行數天,在自己的伺服器於執行途中被銷毀並替換後依然能接著跑,透過 Slack 與 JIRA 協調三個團隊,經由一套經過稽核的審批流程寫入生產環境,並獨立核驗資料在每一個目的地都正確送達。

1.33 : 1
測試程式碼與生產程式碼之比
22
持久化工作流狀態數
6 週
從零到上線
真正的難點

核心的工程難題不是讓它跑起來,而是讓它安全地失敗。

01

一忘就出事的操作

客戶資料從來源系統出發,經過一條流水線,流向三個目的地:資料倉儲、Adobe Experience Platform 和 Marketo。一旦下游哪個環節資料落錯了,修復辦法就是做一次 replay(重放)——把歷史記錄重新發布一遍,讓它們再走一遍全流程。

replay 不是唯讀操作,這正是問題的全部所在。要跑一次 replay,你得先在生產資料庫裡暫時調低這批資料的優先權,等上幾個小時甚至幾天,再把優先權調回來。漏掉最後這一步,或者跑到一半就掛掉,生產環境就會無限期地停留在錯誤設定上,而線上的客戶資料還在以錯誤的優先權持續流入。沒有人會收到告警,因為什麼都沒有當機。

手工做的話,這是斷續盯上一到三天:在聊天裡協調三個團隊、手工編輯生產資料庫、盯著儀表板、抽查兩個目的地,還要記得把異動復原。這一整套操作是否正確,全靠有沒有人忘記。

02

扛過自己被銷毀

這個 agent 跑在 Kubernetes 上,而 Kubernetes 會經常性地銷毀並重建它的伺服器——一次部署、一次機器故障、一次觸及記憶體上限,都會觸發。一次 replay 要跑上好幾天。所以這個 agent 會在執行途中被反覆殺掉,這是家常便飯。

它是一台 22 個狀態的狀態機,每走一步都會把完整狀態寫入持久化儲存。這裡的正確性論證比「我們保存了進度」要強得多:決定下一步做什麼的 17 個函式,全部是基於已保存狀態的純函式。沒有一個會去讀時鐘、讀網路、讀隨機來源,或者讀某個模組級全域變數。所以恢復執行不是一次可能和系統原本行為對不上的重建,而是拿同樣的輸入,把同一套計算原樣重跑一遍。

負責推動工作流往前走的那個元件,刻意不持有任何狀態,每個週期都會把一切從持久化儲存裡重新讀一遍。Kubernetes 滾動部署時,新舊兩台伺服器會有幾秒鐘同時在跑,兩邊都在驅動同一個 replay。放在大多數設計裡,這是一個嚴重的重複處理 bug;而在這裡,兩個驅動者產生的結果和一個驅動者完全一樣——這是架構上刻意設計出來的。

有一類失敗,光靠持久化狀態解決不了:這個 agent 用來做身分驗證的憑證,10分鐘就會過期,而一次 replay 要跑上好幾天。持久化狀態能回答「接下來該做什麼」,卻回答不了「該用什麼憑證去做」。

03

死不放手的鎖

如果一次 replay 調低了生產優先權,還沒來得及復原就掛了,生產環境就處於錯誤設定狀態。這時如果第二次 replay 又在有重疊的資料上啟動,它會把當前那個——已經被調低過的——值當成「原始值」讀進來,跑完之後再復原到這個錯誤的基準上。真正的原始值,從此在任何地方都不存在了。沒有錯誤訊息,沒有告警。

所以,一次結束時還欠著一次復原沒做的 replay,不會釋放全域鎖,後續每一次 replay 都會被擋住,直到有人來處理。真正見分曉的細節在於:系統判斷不出來的時候會怎麼辦——一份讀不出來的記錄表,意味著「判斷不出來」,而「判斷不出來」會被當成「還欠著復原」來處理,鎖就這樣被繼續持有。

這是刻意的取捨:寧要一個吵鬧、顯眼、煩人的失敗,也不要一個安靜、永久、看不見的失敗。同樣的思路,出現在每一處「卡住的鎖本可以自動清除」的地方:悄悄把它放開,等於是拿一把安全的卡住的鎖,去換一個安靜但不安全的「生產環境一直沒復原,誰都不知道」。

04

LLM 該出現在哪裡,不該出現在哪裡

這個 agent 在好幾個地方用到了 LLM。但它刻意不用 LLM 來判斷一份資料核驗報告到底是不是通過——這個結論本來就已經是結構化的數字了,讓模型去重新讀一段寫著「通過」的文字,等於讓一次生產 replay 只隔一次幻覺的距離。真正的判斷,就是四個普通的布林比較。

用到模型的地方,它負責寫審批閘門通知裡那些給人看的文字——它從來不決定通知發給誰,也從來不決定流程走哪個分支。如果它徹底失效,系統會回退到一套確定性範本,所以一道閘門永遠不會被模型卡住。

即便有這樣的限制,還是差點釀成一次事故。一次 LLM 改寫審批閘門通知的時候,悄無聲息地刪掉了其中兩項事實:一項範圍不匹配的警告,還有一個 pull request 的連結——而這恰恰是那則本該擋下一次錯誤合併的訊息裡最關鍵的部分。這段改寫讀起來流暢、可信,也不是空的,所以每一項樸素的品質檢查都放行了它。它只是把這則訊息存在的唯一意義——它要傳達的那個事實——給刪掉了。

這條教訓能推廣到這套系統之外:當措辭本身就是通往人類決策的介面時,把模型限制在「只管措辭」上,並不是足夠的防護。一段丟了警告的改寫,本身並不構成一個錯誤的決策——它刪掉的是人做出正確決策所需要的證據。修復方案是把那些承重的事實釘住,只要缺了一項就回退到範本,而且這些「釘子」是從訊息內容本身擷取出來的,不是寫死的,這樣即便後來措辭改了,守衛也不會還釘在一個已經不存在的字串上。一道悄悄停止把關的防線,比壓根沒有防線更糟。

05

把審批自動化,但不取消審批

三道審批閘門被實現了自動化,但沒有一道因此不再是「人來審批」的閘門。三道閘門依然都可以由人來核准;拒絕按鈕在每一道閘門上都照樣能用。

最直接的實作方式——把這個階段直接改成自動階段——會連帶取消掉在這道閘門上拒絕的能力,因為拒絕路徑本身要檢查這個階段是不是「可由人核准」的。這樣一來,agent 獲得了核准的權力,同時也就剝奪了人拒絕的權力。這裡的不對稱很關鍵:真正扛分量的決策是「不行」,因為一旦拒絕,就會走一條會把生產環境復原的清理路徑,把這次執行終止掉。

取而代之的做法是,agent 透過和人一樣的通道來提交它的決定。要不要回滾,變成了一個設定開關,而不是一次程式碼異動;自動路徑和人工路徑共用同一套實作;稽核軌跡、逾時語意,還有操作員原本的心智模型,全都完整保留了下來。三項自動化裡有兩項預設是關閉發布的,而且是刻意關閉的——兩個看起來對稱的開關,影響半徑可以天差地別。放開其中一道閘門,後面直接通向一次生產 replay;而另一道閘門就算出錯,頂多是優先權沒復原,這是可以補救、也看得見的。

06

沒做檢查,絕不能顯示成檢查通過

核驗環節會把來源和每個目的地逐條記錄做比對。各個目的地本身就有20到30分鐘的延遲,所以一條記錄缺失,可能是真的遺失,也可能只是還沒到;各處的格式又不一樣,逐位元組比對反而會產生大量誤報。

真正微妙的問題在於範圍。如果一次 replay 從一開始設定的就只發往一個目的地,那麼把兩個目的地都查一遍、在第二個目的地什麼都沒查到,報告出來的結論會是「兩個目的地都查過了,一切正常」——這句話和「真的全部成功」、以及「全部送達失敗」,三種情況讀起來一模一樣。人類審閱者通常能從上下文裡推斷出到底是哪一種。自動核准者做不到這一點。要讓自動核准安全,必須先有範圍意識,而且一個不確定的範圍,永遠不能被放寬成「全部目的地」。

同一條原則,以不同的名字,在至少六個獨立的地方反覆出現。一份不包含任何被檢查欄位的負載,算的是擷取失敗,不是相符。一個不在任何目的地欄位對映裡的屬性,算的是一個錯誤。一條沒有涵蓋到這個屬性的規則,會被徹底排除在統計之外,而不是算作通過。就連測試替身也被改掉了,因為一個對超出範圍的目的地偽造出「相符」結果的假物件,會讓測試斷言出恰恰是這項工作要消滅的那種悄悄的部分通過——一項安全屬性,是可能從測試替身這個缺口漏出去的,而這一點很少有工程師會想到。

四條隨處適用的經驗

  1. 要自動化,就往決策通道裡加一個決策者,而不是把通道本身刪掉。拿掉一個審批,連帶拿掉的還有拒絕的能力、稽核軌跡、逾時語意,以及操作員的心智模型。
  2. 把模型放在輸入本質上就是非結構化、沒辦法再壓縮的地方——絕不要放在結構化答案已經存在的地方。而且不管模型放在這個迴圈的哪個位置,都要讓它失效時的方向是「去問人」。
  3. 「只管措辭」不是一種安全的限制,只要措辭本身就是人的決策介面。把承重的事實釘住,讓這些「釘子」從內容本身推導出來,這樣才不會過期失效,而且連那套確定性的回退方案也要一併守好。
  4. 影響半徑決定預設值,而不是結構上的相似。三個幾乎一模一樣的開關,正確的做法是給出不同的預設值;其中一個開關出問題,恰恰是因為它被硬湊成了和另一個「看齊」。

它沒做到的事

這個頁面所依據的那份紀錄,自己就有一節專門講局限性,而這一節恰恰是最值得了解的部分。一個表示「無法判定」的結論狀態,已經被完整地打通、納入統計、也會被攔截,但目前沒有任何一條程式碼路徑真的會產生它。一個逾時缺口,已經被記錄在案、有人認領負責,而不是被悄悄依賴——但它是「上線了但沒修」,不是「修好了」。一把全域鎖,讓整個組織同一時間只能跑一個 replay,而且會在長達24–48小時的人工審批閘門裡一直被持有:這是一個清醒的取捨,失敗方向是安全的,但也是一個真實存在的吞吐量天花板。還有,兩項自動化的分階段灰度發布,有遙測資料,卻沒有退出標準——沒有閾值,沒有負責人,沒有日期。

關於哪些地方並不特別,他講得同樣直接:帶檢查點的持久化工作流,加上人在迴路裡的中斷機制,這些都是底層框架本身就提供的能力。這是把一套持久化執行引擎用好,而不是發明了一套。