iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
AI 自動化

情報進來,內容發出去:30 天拆一條每天真的在跑的 AI 內容產線系列 第 27

Day 27|本機能跑,上線後為什麼變成另一條流程?

  • 分享至 

  • xImage
  •  

一支腳本被排進排程系統,畫面上顯示已經註冊。這只代表系統知道有這個任務存在。

它有沒有真的被觸發過一次,觸發後有沒有跑完,跑完的東西有沒有寫到該去的地方,是三個獨立的問題。各自都可能悄悄失敗,而且失敗時往往什麼提示都不會有。

Day 26 看發生了什麼,今天看換了地方為什麼不一樣

Day 26 處理的是同一個環境裡,一段流程走到一半失敗,要怎麼判斷已經成功的部分。

今天換一個問題:同一段邏輯,本機測試都過,換到排程系統實際執行時卻走上完全不同的路,甚至完全沒有錯誤訊息。這不是對帳能解決的問題,因為連「有沒有出事」都不確定。

把「跑得動」拆成四層來看比較清楚。第一層是登記成功,排程系統知道有這個任務。第二層是觸發成功,系統真的在該執行的時間點啟動了它。第三層是執行完成,啟動之後一路跑到結束沒有中途死掉。第四層是落點正確,跑完的結果真的寫到預期的地方。

這四層是我自己整理出來配合下面三個真實案例的框架,不是業界既有的正式分類法。Kubernetes 官方文件把「活著」「準備好接流量」「啟動完成」定義成三個獨立的健康層次,跟這裡的四層不是同一套規格,只是同一個核心想法在容器編排領域已經有正式對應的設計:正常不是一個單一狀態,是好幾層各自獨立的狀態。

三個真實案例,各卡在不同一層

我手上剛好有三個真實案例,合起來把這個四層階梯裡的每一段斷點都示範過一次。

第一到第二層:已登記不等於已觸發。 另一個專案的排程文件記著一條原則:launchctl list 能列出一個任務,只代表它註冊成功,不代表它真的被執行過。要用 launchctl kickstart -k 實際觸發一次,再去看 log 有沒有留下真實紀錄,才算驗證過。Apple 官方文件也寫得很白:對呼叫端來說,代表這個服務的埠隨時可用,但實際上這個背景程序可能沒有在跑,等到真正有請求進來,系統才可能去啟動它處理。「隨時可觸發」跟「真的在執行」中間,隔著一層。

第二到第三層:觸發了,但連腳本一行都沒執行到。 同一份文件記著另一種失敗:repo 曾經放在受保護目錄下,macOS 的權限機制不讓背景排程程序存取這類目錄,症狀是權限被拒加上錯誤結束碼,畫面上沒有任何異狀。這樣連續失敗了 14 次。這次任務確實被系統啟動了,只是連腳本本身都還沒讀到就被擋下,不是跑到一半失敗,是一行都沒執行過,反而是最乾淨的一種失敗。這個權限行為不是我瞎猜的:有開發者在 Apple 官方論壇回報,背景程序用無介面身分執行時讀取受保護資料夾會直接失敗,系統不會跳出任何提示,因為背景程序沒有介面可以顯示;另一串討論裡,Apple 工程師直接參與確認這是已知行為。兩則都只是論壇討論,不是正式規格白紙黑字的承諾,但方向一致:權限被擋的時候,沒有人會告訴你。

第三到第四層:跑完了,卻寫錯地方。 我自己一支排程腳本的工作目錄變數,預設值到現在還是寫死指向另一個資料夾裡一份已經過期的 git clone。這份舊 clone 結構跟現役的一模一樣,切換目錄、寫檔案這些動作全部會成功,沒有任何錯誤,只是產出全部靜默寫進了那份不會再被使用的複本。這個預設值今天還在程式碼裡,只是可以被另一個環境變數覆寫,不是已經被改掉。寫這段風險的人自己補了一句誠實話:這個情況是否曾經實際發生過,並沒有證據,查過兩份 clone 都沒有找到某段時間之後的產出,比較可能的解釋是那幾天排程根本沒有部署,不是真的寫錯了地方。附帶一提,這支排程截至目前也確實沒有被註冊在系統裡,跟這句推測一致。AWS 的官方架構框架把這種情況點名得很準:最糟的失敗模式是靜默失敗,功能其實已經壞了,卻沒有直接的方式偵測到,只能間接發現,往往是使用者比維運者自己先注意到。

案例 卡在哪一層 症狀
已登記不等於已觸發 第一到第二層 系統顯示註冊成功,沒有主動觸發過就不知道真的能不能跑
觸發了卻一行都沒執行到 第二到第三層 權限被拒,畫面無異狀,連續 14 次全滅
跑完了卻寫錯地方 第三到第四層 預設值指向過期複本,一切動作都成功,只是東西進錯地方

三個案例原始文件裡並沒有被放在同一個框架下討論過,這張對照表是我自己歸納的。

部署後跑的是另一個副本

這支寫死過期路徑的腳本,還有一個相關但不同的問題:部署之後,排程系統實際執行的是另一個位置的副本,不是原本編輯的那一份,原因同樣是系統權限機制不讓排程程序讀取原始位置的檔案。改完程式碼要另外跑部署腳本才會生效。忘記這一步的話,本機看到的最新程式碼跟排程實際跑的內容是兩份不同的檔案,差的不是邏輯,是版本。這跟前面「跑錯地方」不是同一個問題:那個問題是「跑完之後東西寫去哪」,這個問題是「這次跑的到底是哪一份程式碼」。

我踩過的環境差異,跟排程沒關係,但是同一種病

三個案例都跟排程系統有關,但這種病不是排程獨有的。

我自己就踩過一次,跟 launchd 完全無關:把系統的 node 換成 nvm 版之後,Claude Code 的 hook 在互動終端機裡跑得好好的,但輪到非互動環境執行時,直接報 node: command not found。原因是 nvm 只在互動 shell 才會把自己載進 PATH,非互動環境完全不會經過那個載入步驟,NVM_DIR 是空的,連帶 npx 也一起失效。本機測試永遠是互動終端機,這個差異在本機不可能被測出來。修法是在非互動環境也會讀到的路徑上,建一組指向 nvm 版本的符號連結,等於把「哪個環境會經過哪段初始化」這件事,直接寫死成不依賴初始化順序。

這條跟前面三個排程案例的共同點,是同一句話:本機能跑,靠的是本機這個環境剛好具備某個條件;換一個環境,那個條件不一定還在,而且不會有人事先提醒你少了什麼。

環境契約要包含哪幾件事

把三段斷點的案例,再加上部署副本的版本落差一起回推,一份「環境契約」至少要講清楚五件事:這個任務會被排程系統怎麼觸發、它需要哪些設定鍵、秘密要用什麼方式注入(不能寫死在檔案裡)、跑的版本是哪一份部署產物、以及它預期會存取或寫入哪個具體位置。前面幾個真實案例,各自對應這五項裡某一項沒有講清楚,或講清楚了卻沒有被檢查。

Twelve-Factor App 方法論明講,會隨部署環境改變的東西要放進環境變數,不能寫死在程式碼裡,理由是設定會隨部署而變,程式碼不會。那支工作目錄寫死過期路徑的腳本,違反的正是這一條:預設值本身已經是一個環境限定的假設,而它剛好是一個過期的假設。同一份方法論也點名,開發者常用一套工具,正式環境卻用另一套,是被正式承認的問題。今天講的落差不是技術棧不同,是同一份程式碼在本機跟排程系統底下,能拿到的權限和讀到的檔案本來就不一樣,但道理是同一個:本機能跑,不能拿來當作正式環境會一樣跑的證據。

防呆檢查擋得住一種錯,擋不住另一種

那支寫死過期路徑的腳本,其實加了一段防呆:切換到工作目錄之後,反查目前所在位置實際的版本控制根目錄,跟預期的工作目錄變數比對,兩者對不上就中止並發出通知。

這段檢查能擋住「中途被帶去別的地方」。但它比對的對象,正是那個可能本身就設錯的變數:如果工作目錄變數從一開始就指向那份過期複本,切換過去、反查版本控制根目錄,兩者會相等,檢查判定通過。這段防呆擋不住「一開始設定的目標就是錯的」,而這剛好是它原本想解決的那個案例。它也需要目前所在位置真的是一個版本控制倉庫,如果連這個前提都不成立,檢查只能回報查無法辨識再中止。

防呆檢查降低了某一種錯誤被忽略的機率,不是把所有錯誤都變成不可能發生。Google 的《SRE Book》講的是同一個道理:不能只信任系統回報成功就結束,要用獨立的方式主動查證資料是否真的正確,壞掉的資料不會安靜地待著,會繼續往下游擴散。老實說,這段防呆已經是主動查證的一個小規模實例,只是它查證的基準本身還有一個沒堵上的洞。

每一層要用不同的方法驗證,而且越往後面的層級,需要的驗證動作越主動。登記層用列表指令確認任務存在就好;觸發層要主動下一次手動觸發指令,不能只看有沒有列出來;執行層要看執行後留下的 log 有沒有真實紀錄,不能假設沒有錯誤訊息就等於跑完了;落點層要在腳本裡主動反查實際位置,跟預期值比對,不能只靠「沒有報錯」當作寫對地方的證據。光是被動等錯誤訊息出現,越到後面越不夠用。

兩組測試環境,看檢查邏輯抓不抓得到

原則講完了,我做了一個小實驗:建立兩組完全不含任何真實秘密或帳號的測試環境,一組具備完整設定鍵、正確版本標記與正確端點,另一組刻意缺一個設定鍵、版本標記對不上部署產物、端點指向錯誤位置。用同一套檢查邏輯分別跑過這兩組,看它能不能正確分辨「環境契約齊全」跟「環境契約有缺口」,而且缺口要能逐項點名,不能只回一句「有問題」。

每組環境要能回答三個問題:哪一項環境契約缺了或錯了、檢查邏輯有沒有正確標出是哪一項、這個判定跟事先定義的正確答案是否一致。跟 Day 25、Day 26 的做法一樣,先定義正確答案,再讓檢查邏輯去對,不是先跑再回頭合理化結果。

環境 檢查邏輯找到的違規項目 是否吻合事先答案
env-complete 無,0 個違規 吻合
env-deficient missing_config_key(缺一個設定鍵)、version_mismatch(部署版本標記對不上)、write_target_mismatch(寫入位置對不上) 吻合,精確 3 個,不多報也不漏報

兩組都對上我設計案例時就先寫好的答案。這裡的「吻合」是指程式真的跑完、沒有中途丟例外:比對邏輯設計成只要有任何一項答不上,就直接中止,所以能跑到最後本身就是所有項目都對上的證據,不是另外算出一個分數。秘密注入方式在兩組環境裡都刻意維持符合,沒有跟著一起弄錯,這樣違規項目數才會剛好卡在 3 個,不是巧合湊出來的數字。

這個實驗只驗證檢查邏輯本身能不能正確分辨契約齊全或有缺口,不驗證「觸發方式」這個維度,也不驗證秘密內容本身是否正確,只驗證秘密注入方式的宣告。跟前兩天的實驗一樣,全部是純記憶體合成,我在程式裡自己宣告零真實秘密值、零真實部署、零真實排程觸發。這是我對實驗範圍的聲明,不是掃描出來的量測結果,不能拿來當作正式環境已經穩定執行的證據。

旁註:還有一種更隱蔽的失敗

三個案例都是「這一次執行環境對不對」的問題。還有一種更隱蔽的失敗,跟環境無關:同一份文件裡記著,一支腳本裡兩段用來清理狀態的收尾邏輯互相覆蓋,導致清理動作沒有真的執行,下一輪啟動時被自己殘留的鎖檔擋住,從此每一輪都靜默失敗。

這不是單次執行環境對不對的問題,是這次成功了,不代表以後每一次都會繼續成功。這種要跨越多次執行才看得出來的存活問題,留給 Day 29 處理,這篇不展開。

今天解決的,跟明天要接的問題

今天解決的是「同一段邏輯,換了執行的地方會不會走上不同的路」:把跑得動拆成四層,每一層都可能悄悄失敗,也各自需要不同的主動驗證動作,再用一份講清楚五件事的環境契約,把「本機測試都過」跟「正式環境會一樣跑」這兩件事分開。

明天要處理的是另一個問題:就算環境完全一致,模型或提示詞換了版本,怎麼知道原本能做到的事,現在還做得到。這不是環境契約能回答的,是內容和模型本身的品質問題。

參考資料


上一篇
Day 26|發到一半失敗,系統怎麼知道哪一步已經成功?
下一篇
Day 28|模型或提示詞更新後,怎麼知道舊能力沒有壞?
系列文
情報進來,內容發出去:30 天拆一條每天真的在跑的 AI 內容產線30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言