編譯 | 蘇宓
出品 | CSDN(ID:CSDNnews)
作為一名高級云基礎設施工程師,Jumpei Ueno 最近接手了一個由「氛圍編碼」留下的項目。
這個項目的開發者并不是專業工程師,而是公司的 CFO(首席財務官)。他借助 Claude Code,僅用兩天時間就完成了一款 SaaS 產品的開發并上線,隨后便把后續維護工作交給了 Jumpei Ueno。
對Jumpei Ueno來說,這樣的情況早已見怪不怪了。沒有工程背景的人快速搭出產品,真正負責長期維護的工程師再一點點梳理后端系統。然而,每深入檢查一次,他都能發現一些不一樣的新的問題。
不過,這次最讓他意外的,并不是密鑰管理混亂,也不是整個項目沒有一項測試,而是云服務成本已經失控,真金白銀地往外燒。
為此,Jumpei Ueno 將整個排查過程完整記錄下來,希望給更多使用「氛圍編碼」開發產品的人提個醒。
![]()
CFO:我不記得做了什么
一切事情起因源于某一天,Jumpei Ueno 在查看 LLM API 的費用圖表時,發現其中一天的支出異常突出。要知道其他天的開銷幾乎都貼著底部,唯獨那一天直沖天際,幾乎占據了整個月 API 費用的一半。
看到這個數字時,Jumpei Ueno坦言自己”簡直驚呆了”。
原因很簡單:僅僅那一天的 AI 調用費用,就超過了整個服務器集群一個月的運行成本。換句話說,維持所有服務器運行一個月,竟然比讓 AI 工作一天還便宜。這讓他一時難以理解,這樣的開銷究竟是怎么產生的。
于是,他找到當初開發這套系統的 CFO,問了一個最直接的問題:“你那天到底做了什么?”
對方的回答卻只有一句:
“說實話,我已經不記得那天做了什么。”
這個回答讓人哭笑不得。
不過,Jumpei Ueno 認為,此時也不是一個追究責任的時間點(至少不完全是)。隨著調查不斷深入,他越來越意識到,對方記不起來其實再正常不過。真正”燒錢”的,并不是某個人的一次操作,而是系統背后的重試機制。
![]()
開始排查:起初,他以為只是人為反復調用
一開始,Jumpei Ueno 想的很簡單,第一反應大概是:“你一天之內開發了這么多功能,又不斷在生產環境里測試,每測試一次都會調用昂貴的大模型 API。積少成多,最后賬單自然就炸了。”
這個推測看起來也很合理。畢竟那一天的提交記錄從早到晚幾乎沒停過,圍繞 AI 生成功能就有二十多次提交。所以,把高昂成本歸因于開發過程中反復測試,似乎說得通。
但當他們真正去排查應用側日志——包括任務隊列、數據庫以及請求記錄——時,發現事實完全不是這樣。
這并不是一次次人工操作累積出來的”慢性燒錢”,而是同一批高成本任務被機器反復完整執行了一遍又一遍。對于同一個租戶,一個正常情況下只會運行一次的任務,竟然被執行了 21 次。
人不會在一天里把同一個按鈕按 21 次。
真正反復”按下按鈕”的,并不是人,而是程序。
![]()
最可怕的地方在于:任務成功了,卻倒在了最后一步
在 Jumpei Ueno 看來,這才是整起事件的核心。
這項批處理任務會依次調用多個大模型,并將返回結果寫入數據庫,整體流程大致如下:
首先,向多個 LLM 發起一系列請求——這也是費用產生的主要環節;
隨后,將模型返回的結果寫入數據庫。
真正的問題,恰恰出現在第二步。
寫入數據庫時,程序引用了一個本應已經新增的字段,但當時生產環境中的數據庫尚未完成遷移,這個字段實際上并不存在。于是,數據庫直接拋出了 “column does not exist” 錯誤,整個任務最終以 500 錯誤結束。
很多人聽到”任務失敗”,第一反應往往是:“模型調用失敗了,錢白花了。”
但事實并非如此。
所有 LLM 請求都成功返回了 200 狀態碼,也就是說,每一次調用都已經完成計費。模型完成了推理,也返回了結果,只是在最后一步寫入數據庫時出了問題,整個任務才宣告失敗。
Jumpei Ueno 用了一個形象的比喻:
這就像在餐廳里吃完整套套餐,也已經結完賬。正準備起身說一句”謝謝招待”時,卻突然摔了一跤,失去了記憶。等醒來時,又回到了座位,以為自己還沒吃飯,于是重新點了一整套套餐。如此循環,整整 21 次。
已經吃下去的飯不會消失,就像已經完成的大模型調用也不會取消計費;但每一次重試,系統都會從頭再來。
業內有一個術語叫 “Retry Storm(重試風暴)”。通常,人們理解的 Retry Storm 是請求不斷失敗,于是系統一次又一次重試。但這次的情況完全不同。
真正發生的是:每一次調用其實都成功了,只是成功的結果被最后一步”丟掉”了,系統隨后又重新發起了一整輪新的調用。
在 Jumpei Ueno 看來,這正是整件事最違反直覺、也最可怕的地方。
![]()
兩個問題疊加,最終導致任務被重復執行 21 次
進一步排查后,Jumpei Ueno 發現,系統之所以不斷重復執行同一批任務,是兩個問題共同作用的結果。
第一個問題,是部署順序出了錯。
代碼已經率先部署到生產環境,并默認數據庫中已經存在新增字段;然而,負責添加該字段的數據庫遷移卻尚未執行。也就是說,代碼先上線,數據庫結構后更新。結果,程序每次都會訪問一個根本不存在的字段,從而穩定地報錯。
Jumpei Ueno 特別強調,這是一種確定性失敗——無論重試多少次,都不可能自行恢復。
第二個問題,則來自任務隊列的自動重試機制。
對于托管任務隊列來說,當任務返回 500 錯誤時,它會默認認為只是一次臨時故障,于是自動重新執行任務。這種機制原本是為了應對網絡抖動等偶發問題,本身并沒有錯。
但這次的問題并不是網絡故障,而是數據庫里根本沒有對應字段。字段不會因為不斷重試就憑空出現,但任務隊列卻仍然”善意”地一次又一次重新執行同一個任務。更糟糕的是,這套批處理任務本身并不具備冪等性。
也就是說,它不會檢查哪些工作已經完成,而是每次重試都會從頭開始執行。因此,每重試一次,就會重新調用所有 LLM,也重新產生一整輪完整的 API 費用。
確定性失敗 + 自動重試 + 非冪等設計。Jumpei Ueno 表示,當這三個因素疊加在一起時,資金就會在悄無聲息中不斷流失。
也正因如此,自家公司 CFO 根本不記得自己做過什么——因為真正不停”按下按鈕”的,并不是人,而是任務隊列。
當他把整個原因梳理給 CFO 聽時,對方只是皺著眉頭,一臉困惑地發出一句:“嗯?”
Jumpei Ueno 認為,對于沒有工程背景的人來說,“模型調用已經成功、費用已經產生,但最后卻把成功結果丟掉,又重新調用一遍”這樣的機制,確實很難第一時間理解。
![]()
重試機制并不總是一種善意
經歷了這次事件后,Jumpei Ueno 總結了幾條經驗。他認為,這些教訓不僅適用于自己,也適用于所有接手維護別人已經上線系統的工程師。
首先,確定性失敗并不會因為不斷重試而自行恢復。無論是數據庫 Schema 不一致,還是 4xx 這類明確屬于程序自身問題的錯誤,重試多少次,結果都不會改變。對于這類錯誤,系統應當立即終止,而不是無限重試。同時,任何重試機制都應該設置明確的次數上限,重試并不是萬能的保險。
其次,副作用越大的任務,就越需要具備冪等性。凡是涉及真實成本的操作,例如調用計費 API 或大模型接口,從第一天開始就應該具備”跳過已完成任務”的能力。否則,每一次重試都不是簡單地”重新執行”,而是在重復計費。
第三,生產環境部署必須遵循”先更新數據庫,再部署代碼”的順序。只有數據庫完成遷移后,再上線依賴新字段的代碼,才能避免在兩者之間的時間窗口內,大量產生這種確定性錯誤。
此外,他還提到,如果成本本身不可觀測,那么往往只有錢燒完之后,人們才會意識到問題已經發生。這次之所以能夠發現異常,僅僅是因為他碰巧查看了 API 成本曲線。如果沒有將生產環境與測試環境使用不同的 API Key,也沒有預算告警等監控機制,那么直到收到月底賬單之前,幾乎沒有人會察覺異常。
最后,Jumpei Ueno 也談到了氛圍編碼帶來的變化。
其表示,AI 的確大幅降低了非工程人員開發生產系統的門檻,讓他們能夠在短時間內完成產品開發。但知道如何把功能做出來,與知道系統會如何出錯、又會如何在無聲無息中燒掉大量成本,是兩種完全不同的能力。而后者,仍然需要后來接手系統的工程師來承擔。
正如他總結的那樣:
兩天時間足以開發出一個功能,但要避免”善意的自動重試”演變成”丟棄已經成功的結果,再重新計費一次”這樣的事故,卻絕不是兩天就能學會的事情。
來源:https://junueno.dev/en/retry-storm-rebilled-llm-cost/
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.