宅中地 - 每日更新
宅中地 - 每日更新

贊助商廣告

X

當「恢復」不等於「恢復」:一份讓AI工作流框架現出原形的機器驗證契約

2026年08月14日 首頁 » 熱門科技

你有沒有想過這樣一個問題:當一個正在自動處理你銀行轉賬的AI程序被強制關機,重啟之後,它會不會把你的錢轉兩次?

這不是一個假設。這是一篇論文用五個主流AI智能體框架、三種資料庫後端、真實的SIGKILL強制殺進程實驗,一點一點驗證出來的真實答案。而答案,比大多數人想像的要糟糕得多。

一個所有人都默認成立、卻從沒人驗證過的承諾

現在幾乎所有做AI智能體(agent)的開發團隊都在用一類工具:LangGraph、CrewAI、LlamaIndex Workflows 這些工作流框架。它們共同的賣點是"持久化",說白了就是:你的AI程序在執行到一半時可以暫停,等人工審批,或者哪怕伺服器崩了,重啟之後也能接著跑,不用從頭再來。

這個承諾聽起來樸素,但背後藏著一個極其棘手的問題:那些已經執行完的動作,恢復之後到底會不會重來一遍?

如果你的AI智能體只是在讀寫文本、生成摘要,重複執行一次問題不大,頂多浪費點算力。但現在的AI智能體開始被授權去做真正有後果的事情:轉賬、發郵件、寫文件、下單。這時候"重複執行"就不再是小毛病,而是真金白銀的損失。

論文作者做了一件很基礎但此前沒人做過的事:把五個框架的官方文檔擺在一起對比。結果發現,三個框架給出了三種互相矛盾的說法。CrewAI 的檢查點功能文檔明確寫著,恢復時"不會重新運行已完成的工作"。LlamaIndex Workflows 的文檔卻告訴用戶,某個模式下代碼會被重新執行,所以你得自己保證前面的代碼"重新執行也是安全的"。而 LangGraph 採用的是"記憶化"策略,聲稱已完成的任務結果會被記住,不會重複執行。

三個框架,三套完全不同的承諾。而當作者實際去測試時,發現更讓人意外的事實:

**三個框架里,有兩個連自己文檔里寫的承諾都沒做到。**

這就是這篇論文的起點:一個開發者如果想把一個涉及真實資金操作的AI工作流從一個框架遷移到另一個框架,他沒有任何類型系統、任何簽名、任何文檔能告訴他這兩個框架的"恢復"語義是否一致。這個資訊真空,就是一個個GitHub issue,一次次線上事故的源頭。

把"恢復"這件事,拆成六個必須回答的問題

論文作者沒有急著去抓框架的bug,而是先做了一件更根本的事:定義什麼樣的行為才算是"正確地恢復"。

這套定義被稱為**RESUME CONTRACT(恢復契約)**,包含六條核心性質,外加一條協議層面的補充要求。

第一條是**前綴延續性(PC,Prefix Continuation)**:恢復必須從已經持久化記錄的那個斷點狀態開始,不能憑空捏造一個初始狀態去重新推導。

> 前綴延續性*:簡單說,就是"接著往下走"而不是"從頭再走一遍",哪怕走的路徑和之前一樣,狀態也必須是從真實記錄里讀出來的,不能是重新猜的。

第二條是**效果恰好一次(EO,Effect Exactly-Once)**:每一個帶有副作用的任務,在一次分支執行里最多只能真正觸發一次外部效果。

第三條是**分叉確定性(FD,Fork Determinism)**:如果同一個中斷點被兩次不同的輸入值恢復(比如人工審批先後給了"同意"和"拒絕"兩個答案),這兩次恢復必須走向不同的分支結果。

第四條是**檢查點有效性(CV,Checkpoint Validity)**:任何被持久化保存的狀態記錄,都必須符合預定義的數據結構,格式錯誤的數據不應該被悄悄存進去。

第五條是**消費一次(CO,Consume-Once)**:一個中斷點只能被一次恢復"消費掉",之後重複發來的恢復請求應該是無效的。

第六條是**恢復確定性(RD,Recovery Determinism)**:從完全相同的持久化記錄出發,兩次獨立的恢復過程必須做出完全相同的決策。

這六條性質里,第三條和第五條之間藏著一個有意思的邏輯矛盾。想像你負責審批一筆付款,你回復"同意"。過了一會兒,你改變主意,又發了一次"拒絕"。系統應該怎麼辦?

從分叉確定性的角度看,第二次的"拒絕"應該被當成一次全新的分叉請求,走向不同的結果。但從消費一次的角度看,這個中斷點已經被"同意"消費過了,第二次請求應該是無效的、被忽略的。這兩個要求,字面上完全對立。

論文用一個數學證明(引理1)說明了這一點:**如果協議本身沒有辦法區分"這是一次故意的重新分叉"和"這只是重複發送的舊消息",那麼無論用什麼算法,都不可能同時滿足分叉確定性和消費一次這兩條性質。**

這就好比你收到兩條一模一樣的簡訊,一條是朋友手滑重發的,一條是他改了主意重新發的,內容卻字面相同。如果簡訊本身沒有攜帶任何"這是第幾次發送"的標記,你壓根沒法區分這兩種情況,無論你多聰明都做不到。

所以論文引入了第七條性質:**分叉意圖可表達性(FI,Fork-Intent Expressibility)**。它要求協議本身必須能夠表達"這是一次故意的分叉"這個信號,比如帶一個明確的序號或標誌位。這不是一條行為性質,而是一條對接口設計的硬性要求:**如果協議連"我想分叉"這句話都說不出來,那底層實現就只能靠猜,猜錯了就會出問題。**

而LangGraph,恰好就是那個"說不出這句話"的協議。它的恢復地址(checkpoint_id)在官方文檔里被賦予了兩種含義:一種是"創建分支的時間旅行",一種是"重放"。這種文檔層面的雙重解釋,本身就是FI缺失的活生生的例子。

用一台"數學顯微鏡"照出問題的根源

光提出概念還不夠,作者接下來做了一件更硬核的事:把整套契約寫成TLA+形式化規範,用模型檢測器TLC做窮舉驗證。

> TLA+*:一種用來精確描述系統狀態和狀態轉換規則的形式化語言,由圖靈獎得主Leslie Lamport發明,亞馬遜內部大量使用它來驗證分布式系統設計。

>

> TLC*:TLA+配套的模型檢測工具,能夠自動窮舉一個系統在給定範圍內所有可能的狀態,檢查是否有任何一種情況違反了你定義的規則。

作者建了一個"參考語義"模型,也就是理論上完全正確、六條性質都滿足的理想版本。TLC對這個模型做了窮舉檢查,在擴大規模之後跑出了740萬個不同狀態,全部合規,零錯誤。

接著,作者往這個模型里注入了六種"故障開關",每一種都對應一個真實框架里觀察到的具體問題,比如"恢復時從任務1重新開始執行"(對應CrewAI的行為)、"第二次恢復被強行服務成第一次的結果"(對應LangGraph的#6663號問題)。每打開一個開關,模型就會在4到7步之內自動找到一個具體的違規場景,白紙黑字地展示出問題究竟是怎麼發生的。

更進一步,作者還做了一件很講究的事:把每一個故障開關,拿去和全部六條性質逐一交叉檢驗,一共39次運行。結果發現,有的故障只破壞它"該破壞"的那一條性質,比如分叉故障只破壞分叉確定性;但有的故障會連帶破壞好幾條性質,比如"恢復決策不確定"這個故障,會同時破壞效果恰好一次、前綴延續性和恢復確定性三條。

這個交叉驗證矩陣回答了一個可能有人會問的質疑:這些故障開關是不是"照著答案反推出來的",專門設計成剛好破壞對應的那條性質?如果真是這樣,那麼每個開關理應只破壞自己的目標性質,其他格子應該全是乾淨的。但實際結果是,有四行故障的交叉格子裡出現了"意外"的違規,說明這些故障開關反映的是真實的機制問題,而不是湊出來的標籤。

論文還進一步證明了這六條性質里,有五條(EO、PC、FD、CV、RD)在邏輯上彼此獨立,也就是說,滿足其他五條,完全不能保證第六條也成立,必須單獨驗證。唯一的例外是消費一次(CO)這條性質,它拆成兩半後,其中"效果惰性"這一半,在數學上必然被"效果恰好一次"所蘊含,屬於結構性依賴,而"消費計數"那一半則依然獨立。

這個獨立性證明不是靠抽樣驗證的,而是靠窮舉出一個"完全滿足其他五條、卻違反目標那一條"的具體模型來證明的。這種證明方式在邏輯學裡叫反例見證,一旦找到一個完整窮舉過的反例,結論就是確定的,不需要靠"跑得越多越可信"這種統計式的信心。

後來,作者甚至用TLAPS對參考語義整體做了無界證明,一共處理了196個證明義務,全部通過,意味著這個結論不再局限於某個具體規模的狀態空間,而是對任意滿足模型假設的取值都成立。

> TLAPS*:TLA+ Proof System,一個能對TLA+規範做數學定理證明的工具,比TLC的"窮舉有限狀態"更進一步,可以證明性質在無限狀態空間下也成立。

LangGraph的分叉漏洞:不是bug,是設計選擇的影子

理論建好了,接下來是把它砸向真實框架。這部分測試結果,是全文最刺激的部分。

先說LangGraph,這是目前最流行的AI智能體工作流框架之一。作者發現它有一個持續存在了至少五個版本、跨越一年發布周期的分叉漏洞,編號為#6663。

測試場景很簡單:一個流程在某個節點暫停,等待人工輸入。測試者先發送"同意"(True),再對同一個暫停點發送"拒絕"(False)。按理說,第二次輸入應該觸發一個新的分支,走向"拒絕"對應的結果。但實測結果是:**兩次恢復返回的都是第一次的值。第二次輸入的"拒絕"被徹底吞掉,從未被採納。**

這個bug在三種不同的儲存後端(內存、SQLite、真實的PostgreSQL資料庫)上表現一致,說明它不是某個儲存實現的偶然問題,而是更上層的執行邏輯出的問題。

作者沒有止步於"發現問題",而是深入源碼,找到了問題的確切位置。原來LangGraph為了讓"用同一個值重複調用"這件事變得冪等(也就是重複調用不會產生額外副作用),設計了一條去重規則:只要一個恢復通道已經記錄過寫入值,之後的調用就直接服用之前記錄的那個值,不再重新處理新輸入。

這個設計本身是有道理的:如果同一個人不小心把審批按鈕點了兩次,你當然不希望系統真的執行兩次轉賬。**但這條為了防止"意外重複"而設計的規則,恰好把"故意的二次分叉"也一併擋在了門外。**

論文用一個專門針對LangGraph的形式化模型,重現了這個機制:給兩次調用不同的值 va 和 vb,模型服務出來的結果是 va 和 va,正是實測中看到的現象。也就是說,**這個bug不是程序員寫錯了,而是"防重複"和"允許重新分叉"這兩個需求,在沒有區分標誌位的前提下天然衝突,被設計選擇精確地實現出來了。**

這就好比一個門禁系統,為了防止有人反覆刷卡騙系統多次開門,設定了"同一張卡在短時間內只認第一次刷卡結果"的規則。這在防重複闖入上非常有效。但如果這張卡的持有人真的改變主意,想告訴門衛"我其實是想讓另一個人進",系統卻壓根不聽第二次的意思,只會重複第一次的判斷。如果不設這條規則,會怎樣?會議室門禁可能被人一直刷卡騙開無數次;設了這條規則,又會導致合法的"改主意"請求被忽略。**兩難的根源在於,卡片本身沒有告訴門禁"這是第幾次嘗試"這個資訊。**

更讓人在意的是,這個漏洞在LangGraph官方文檔里其實有兩種矛盾的解釋。文檔一方面把這種恢復地址描述成"在任意檢查點分叉圖狀態"的時間旅行功能,另一方面又暗示它是重放機制。**在這兩種解釋里,無論按哪一種理解,實測的行為都構成了對文檔承諾的違背。**

悄無聲息的數據腐敗:檢查點有效性的崩塌

另一個在LangGraph 1.2.9上發現的問題,比分叉漏洞更隱蔽,也可能更危險。

測試構造了一個用pydantic定義了嚴格數據結構的狀態(比如要求某個欄位必須是字符串列表),然後讓某個節點故意寫入一個不符合結構的值(比如None)。按理說,這種寫入應該被拒絕,或者至少在讀取時報錯提醒用戶"這裡有問題"。

在早先的版本里,這種非法寫入確實會被持久化保存,然後在之後調用歷史讀取接口時拋出異常,好歹給了個提示。但在最新的1.2.9版本里,**這個非法數據被悄悄地、完整地寫入了資料庫,讀取歷史記錄時什麼錯誤都不會報,調用者完全不知道自己的數據已經壞了。**

作者進一步做了細分測試,發現這個"悄無聲息"其實分兩種情況。如果非法寫入發生在流程的最終節點,那就是徹底的沉默,你調用狀態讀取接口拿到的就是那個壞掉的值,直到你嘗試用pydantic驗證它才會報錯,而且這個沉默甚至能在數據從磁盤重新加載後依然保持。但如果非法寫入發生在流程中間的某個節點,情況會稍好一點,壞數據先被寫進去,但下一步執行時會立刻拋出驗證錯誤,之後調用這個流程的讀取接口也會持續報錯。

這兩種情況都不理想,但第一種更危險,因為它給人一種"一切正常"的假象。

崩潰路徑 vs 中斷路徑:同一個API的兩副面孔

如果說前兩個問題是明顯的功能缺陷,那接下來這個發現就更微妙、也更具啟發性了:**LangGraph在"人工中斷恢復"這條路上做得對,但在"進程崩潰恢復"這條路上卻做得不對,而這兩條路走的是同一個持久化接口。**

測試構造了一個兩步任務流程,第一步的結果已經被持久化寫入資料庫,然後模擬第二步執行時進程崩潰。恢復之後,正確的行為應該是跳過已完成的第一步,直接從第二步繼續。但實測結果是,**已經完成、且已經持久化保存了結果的第一步,被重新執行了一遍。**

為了排除"這只是異常測試的假象"這種質疑,作者還做了真實的SIGKILL強制殺進程實驗,用文件系統屏障精確同步殺進程的時機,確保殺進程發生在結果已經寫入磁盤之後。結果依然一樣:**重啟一個全新的解釋器進程,那個本該被跳過的任務,又被執行了一遍。**

而LangGraph自己的文檔,明確寫著已完成節點的寫入之所以要被儲存,就是為了"讓你在恢復時不需要重新運行成功的節點"。這句承諾,在崩潰這條路徑上,被自己的實現打破了。

這個發現意味著什麼?意味著**同一個持久化API,在被中斷喚醒時表現出"恰好一次"的語義,在崩潰恢復時卻表現出"至少一次"的語義。**同一套代碼,兩種截然不同的可靠性保證,取決於你是通過哪種方式觸發恢復的。這種不一致,對一個想要設計可靠系統的開發者來說,幾乎是災難性的,因為它意味著你沒法通過閱讀一次文檔,就摸清楚整個系統的行為邊界。

CrewAI:寫的承諾和做的事完全是兩回事

如果說LangGraph的問題主要出在執行細節層面,那CrewAI的問題就更直接:**它的檢查點功能文檔明確承諾"恢復時不會重新運行已完成的工作",但實測結果直接推翻了這句承諾。**

測試流程是這樣:任務s1執行完成並寫入檢查點,任務s2執行時拋出異常導致流程中斷。作者用官方推薦的from_checkpoint方法從最新檢查點恢復流程。結果是,s1這個明明已經完成的任務,被重新執行了一次,它的效果計數器從1變成了2。

更微妙的是,流程本身顯示的狀態計數器讀數卻是"正確"的數字,看起來完全沒問題,因為CrewAI在恢復時是把狀態從初始值重新計算出來的,而不是真的從持久化記錄里繼續。**這意味著,如果第一步是"給信用卡扣款",那麼這張卡會被真實地扣兩次款,而流程本身呈現給你的狀態卻顯示一切正常。**

這正是為什麼論文堅持要用一個獨立於框架內部狀態的外部效果台賬(ledger)來做驗證的原因。**如果只相信框架自己匯報的狀態,你根本發現不了這個重複扣款的問題,因為框架的狀態顯示層本身就是錯的。**

這就好比一個自動記賬軟體,明明重複給你的信用卡扣了兩次錢,但賬本上卻只顯示扣了一次,因為賬本是根據"這個月總共應該花多少錢"這個公式重新算出來的,而不是逐筆記錄實際發生了什麼。如果你只看賬本,你永遠不會發現錢被多扣了。你必須去查銀行卡的真實流水,才能發現問題。論文作者用的外部SQLite賬本,扮演的正是"銀行卡流水"這個角色。

作者還發現了一個更寬泛的規律:只要crewAI的持久化邊界不完整,無論在哪一個操作節點上模擬崩潰,都會導致已完成的工作被重新執行。這不是某一個偶然的邊界情況,而是這套持久化機制在設計上就存在的系統性缺陷。

pydantic-graph:安全得像個死人

第四個被測試的框架叫pydantic-graph,它的問題走向了另一個極端:**它沒有出現任何重複執行的問題,因為它壓根不讓你從崩潰中恢復。**

測試場景是這樣:第一個節點執行完成並生成了快照,然後在第二個節點執行過程中模擬崩潰。作者調用框架自己文檔記載的恢復入口,結果直接拋出異常:"無法從狀態持久化中恢復快照"。

從"不會重複執行副作用"這個角度看,這個框架是安全的,因為它壓根沒有恢復執行,自然也就不會有重複。但這種安全是一種"安全得像個死人"的安全。論文管這種現象叫**"安全性靠死寂"**:所有安全性質都滿足,但活性(liveness,也就是"流程最終能跑完"這條基本要求)徹底失敗了。

這就好比一輛車,為了絕對避免撞車事故,永遠不啟動。從"零事故"這個KPI來看它完美達標,但它已經不能算是一輛能開的車了。如果不追問"能不能開"這個問題,光看事故率,你會誤以為這是最安全的車。

有意思的是,這個bug只在"進程在節點執行中途崩潰"這種情況下出現。如果崩潰發生在流程暫停等待人工輸入的時候,恢復反而是正常的。這說明問題出在一個很窄但很關鍵的時間窗口裡,而現實中長時間運行的AI節點(比如等待一個模型響應)恰好最容易崩潰在這個窗口內。

並發才是真正的深水區:一個approve被同時消費了好幾次

前面說的所有問題,都是單個進程、單次操作觸發的。但論文測試的最後一個、也是最驚人的一個發現,出現在並發場景下。

**在只有一個進程訪問的情況下,LangGraph對"消費一次"這條性質是完全合規的。**一個已經被消費過的中斷點,再次收到相同的恢復請求,會被正確地忽略,不產生任何重複效果。

但如果換成兩個獨立的作業系統進程,同時對同一個暫停中的中斷點發起恢復請求呢?

結果是:**在開發機上跑10次,10次全部觸發了兩次效果執行。在容器環境裡也復現了。**換到基於真實網路PostgreSQL資料庫的場景下,同樣是10次里10次重複。**這個漏洞跨越了不同的宿主機,甚至跨越了不同的物理機器,兩個在不同機器上的競爭者,10次里10次都產生了重複消費。**

這個問題的本質,是一個經典的資料庫並發問題:**"讀取-判斷-寫入"這個操作序列,沒有被設計成原子操作。**兩個進程幾乎同時讀到"這個中斷點還沒被消費"這個狀態,於是都判斷自己可以繼續執行,各自都觸發了副作用,等它們再去寫"已消費"這個標記時,已經太晚了。

> 競態條件(race condition)*:指多個進程或線程同時訪問、修改同一份共享數據時,由於執行順序的不確定性導致結果出錯的現象。這裡具體表現為"丟失更新"這種經典的資料庫並發異常。

這就好比一張演唱會門票,兩個人幾乎同時在網上查詢"這張票是否還能用",系統顯示"可用",於是兩個人都刷了這張票進場。等系統真正把票標記為"已使用"時,兩個人都已經進去了。**如果檢票系統只是簡單地"先查詢再更新",而不是把"查詢和更新"綁成一個不可分割的操作,那麼在兩個人同時到達檢票口的極端情況下,這張票就會被"消費"兩次。**

作者沒有止步於發現這個漏洞,還專門做了一次劑量-反應式的深度測量,系統地改變並發進程數量(從2個到16個)和到達時間差(從0毫秒到25毫秒)。結果發現,這個漏洞窗口的寬度,**恰好和被中斷的那個節點自身的執行時長掛鉤**:如果一個節點本身要跑2秒鐘(比如在等一個大模型的響應),那麼在這2秒鐘之內到達的所有並發請求,幾乎全部都會觸發重複消費,飽和率達到100%。測試範圍內沒有觀察到任何"進程數量的上限",16個並發進程同時湧入,16次全部觸發了重複執行。

這個發現的意義在於:**這不是一個只在極端壓力測試下才出現的邊角案例。任何一個真實部署的AI智能體,只要它的暫停節點背後是一次真實的模型調用(通常都要幾秒鐘),這個窗口就是活生生存在、隨時可能被觸發的。**

REMIT:一個帶數學證明的修複方案

發現了問題之後,論文沒有止步於"發現",還給出了一個具體的修復實現,叫做**REMIT**,一個可以直接安裝使用的Python包(remit-contract)。

REMIT的核心思路是在框架的持久化接口那一層做攔截,而不是去改框架本身的代碼。它內部用Rust編寫核心邏輯,通過PyO3綁定暴露給Python調用,並且這套Rust核心的關鍵不變式用Verus這個工具做了機器驗證。

> Verus*:一個用來對Rust程序做形式化驗證的工具,可以數學證明代碼的某些性質在所有可能的執行路徑下都成立,而不是靠測試用例去抽樣檢查。

針對分叉確定性這個問題,REMIT把每個恢復分支按一個唯一的鍵值(檢查點ID加恢復序號)來區分,而不是像LangGraph原本那樣只服用第一次記錄的值。作者一開始嘗試在"寫入"這一層做這個修復,但發現完全沒用,因為LangGraph的執行循環壓根不會在決定該走哪條分支之前去詢問儲存層的意見,決策是在儲存層之上的執行邏輯里就已經做完了。

**真正有效的修復點,在"讀取"這一層。**也就是說,在框架從資料庫把狀態讀出來、準備決定下一步該怎麼走的那個瞬間去介入,而不是在寫入資料庫的那個瞬間。這個"寫入路徑失敗、讀取路徑成功"的模式,後來在修復並發消費問題時又重複出現了一次,說明這不是巧合,而是這類"先讀、再決定、後寫"架構的一個通用規律:**任何在決策發生之後才介入的補丁,都沒法否決那個已經做出的決策。**

針對並發消費的問題,REMIT的修複方案是在共享的儲存層加一個基於唯一約束的原子插入操作,誰先成功插入這條"聲明"記錄,誰就贏得消費權,另一個進程會收到一個明確的拒絕異常,而且這個拒絕發生在任何節點真正執行之前。修復之後,10次並發競爭測試里,變成了穩定的"1個贏、10個都被拒絕",無論是在同一台機器還是跨越兩台不同的物理機器上,效果都是一致的。

REMIT的驗證工作被分成了好幾個明確標註的層級,論文很坦誠地說明了哪些地方是完全經過數學證明的,哪些地方只是經過了大量測試但沒有完整證明。比如核心的恢復決策邏輯,不僅在抽象模型層面被證明正確,還進一步驗證了這段可執行的Rust代碼和實際發布的庫中的代碼逐行一致,並且這個一致性檢查被放進了持續集成流程,每次代碼提交都會自動核查。但從"經過驗證的抽象模型"到"編譯出來的最終二進制文件"之間,並沒有一個完整的數學證明鏈條把兩者嚴格連接起來,作者明確把這一點列為尚未完成的工作,沒有藏著掖著。

五個框架,沒有兩個是一樣的

把所有測試結果放在一起看,會得出一個很紮實的結論:**在被測試的五個框架里,沒有任何兩個框架擁有相同的合規表現。**

作者專門做了一個兩兩配對分析,十對框架組合里,九對是在同一個測試場景下給出了不同的行為結果,剩下的一對(AutoGen和LlamaIndex Workflows)雖然在共同測試的場景里表現一致,但因為兩者能測試的路徑本身就不完全重疊,所以嚴格來說也算不上"完全相同"。

這個結果說明了什麼?說明**目前整個AI智能體框架生態,在"恢復"這件最基礎的事情上,處於一種徹底碎片化、互不兼容的狀態。**一個開發者今天用LangGraph寫好的、依賴恢復語義的業務邏輯,換到CrewAI上大概率會出問題,換到pydantic-graph上可能壓根跑不起來。

論文還發現,兩個此前被官方修復過的LangGraph問題(編號#7361和#6792),是在1.1.x版本里作為回歸引入的,直到1.2.9版本才被修復。這說明**在沒有一套標準化契約和自動化測試套件的情況下,即便是同一個框架,語義也會隨著版本疊代悄悄漂移。**今天正確的行為,明天的一次更新就可能悄悄破壞它。

寫在後面

讀完這篇論文,最讓我意外的其實不是那些具體的bug,而是那個關於"分叉確定性"和"消費一次"互相矛盾的數學證明。這不是一個可以靠更仔細寫代碼解決的bug,而是一個協議設計層面的根本矛盾:如果你的通信協議里沒有攜帶"這是第幾次嘗試"的資訊,你永遠沒法區分"重複"和"改主意"。這讓我想到,很多看似是實現細節的問題,追根溯源其實是接口設計階段就埋下的根,寫代碼的人再仔細也補不回來。

另一個讓人印象深刻的細節是,論文裡反覆出現"寫入路徑的修復沒用,讀取路徑的修復才有效"這個模式,而且是在兩個完全不同的問題(分叉確定性、並發消費)上各自獨立發現的。這種重複出現的結構性規律,比單獨任何一個bug報告都更有價值,因為它揭示的是一類系統架構(先讀狀態、再決策、最後才通知儲存層)的通用弱點,而不是某個框架的偶然疏忽。

論文裡沒有回答、但我很好奇的一個問題是:這套契約有沒有可能反過來推動框架團隊在設計階段就把"恢復語義"當作一等公民來對待,而不是像現在這樣,等用戶提交issue之後才發現問題?畢竟網路協議和文件系統花了幾十年才建立起對"崩潰恢復"這件事的行業共識,AI智能體框架會不會也需要經歷類似的漫長過程?

Q&A

Q1:RESUME CONTRACT具體包含哪幾條性質?

A:一共六條核心性質,分別是前綴延續性(恢復必須從持久化記錄的斷點開始)、效果恰好一次(副作用不能重複觸發)、分叉確定性(不同輸入應導向不同分支結果)、檢查點有效性(持久化數據必須符合規定格式)、消費一次(中斷點只能被消費一次)、恢復確定性(相同記錄必須導出相同決策),外加一條協議層面的分叉意圖可表達性要求。

Q2:LangGraph的分叉漏洞#6663具體是什麼問題?

A:當同一個中斷點先後收到兩次不同的恢復值(比如先"同意"後"拒絕")時,LangGraph會把兩次都服務成第一次的結果,第二次輸入被靜默忽略。這個漏洞在五個版本、三種儲存後端上都穩定存在,根源是框架為了讓重複調用冪等而設計的去重規則,恰好把合法的重新分叉請求也一併擋住了。

Q3:並發場景下"消費一次"這條性質為什麼會失敗?

A:因為"讀取中斷狀態、判斷是否已消費、寫入消費標記"這三步操作沒有被設計成一個不可分割的原子操作。當兩個進程幾乎同時讀取到"未消費"的狀態時,都會各自判斷自己可以繼續執行,導致副作用被觸發兩次。測試顯示這個漏洞窗口的寬度跟著被中斷節點自身的執行時長走,節點跑得越久,窗口越大,重複消費的概率越接近100%。

宅中地 - Facebook 分享 宅中地 - Twitter 分享 宅中地 - Whatsapp 分享 宅中地 - Line 分享
相關內容
Copyright ©2026 | 服務條款 | DMCA | 聯絡我們
宅中地 - 每日更新