排程跑 AI Agent,我踩過的三個坑:exit 0 不代表成功
把 AI agent 掛上排程是很自然的想法:每天早上自動整理資料、產報告、跑摘要,醒來看結果就好。
我在 macOS 上用 launchd 做這件事。腳本在終端機手動跑,一百次一百次過;掛上排程的第一個晚上,它死了。
更糟的是我隔天早上才發現——因為它死得很安靜。
坑一:排程環境碰不到你以為碰得到的目錄
macOS 有一層叫 TCC 的隱私保護:~/Documents、~/Desktop、~/Downloads 這些目錄,每個 App 要經過使用者同意才能讀。你在終端機裡沒感覺,是因為終端機 App 已經被授權過了。
但 launchd 直接拉起來的 process 不繼承任何授權。它去讀 ~/Documents 下的專案目錄,拿到的是 EPERM——或者更迷惑的行為:卡住不動。
對 AI agent 這件事特別致命,因為 headless 模式的 agent CLI 通常以工作目錄為中心讀寫檔案。工作目錄放在受保護路徑下,等於整個 agent 泡在權限錯誤裡。
| 環境 | 讀 ~/Documents | 原因 |
|---|---|---|
| 終端機手動跑 | 正常 | 終端機 App 已被 TCC 授權,子 process 繼承 |
| launchd 排程跑 | EPERM 或卡死 | launchd process 沒有 TCC 授權可繼承 |
解法不是想辦法給 launchd 授權(做得到,但脆弱——授權綁定二進位的 hash,工具一更新就悄悄失效),而是繞開整個問題:
排程任務的工作目錄和所有讀寫檔,一律放在中性路徑,例如 ~/.local/share/<任務名>/。真的需要讓結果出現在 Documents 裡,用 symlink 指過去——互動環境讀寫 symlink 完全無感。
同場加映:排程環境也不載入你的 shell profile。PATH 裡沒有你裝的工具,在腳本裡明確 export,不要假設。
坑二:非 TTY 的 stdin 會讓 CLI 永遠等下去
第二個坑的症狀最詭異:agent CLI 在排程裡啟動之後什麼都不做。不報錯、不輸出、不結束,連 session 檔都沒建。
原因是 stdin。互動環境裡 stdin 是你的鍵盤;排程環境裡 stdin 是什麼?如果沒有明確指定——很多 CLI 會嘗試從 stdin 讀輸入,然後永遠等一個不會來的 EOF。
# 錯:CLI 等 stdin EOF,無限阻塞
codex exec "整理今天的資料" > log 2>&1
# 對:明確給它一個立刻 EOF 的 stdin
codex exec "整理今天的資料" < /dev/null > log 2>&1通則是:任何 CLI 在非 TTY 環境下行為都可能改變——讀 stdin、關掉顏色輸出、要求互動確認。背景跑任何東西之前,先想清楚它的 stdin 是什麼;拿不準就給 /dev/null。
輸出端同理:寫到檔案再事後解析,不要接 pipeline——pipeline 下游退出時的 SIGPIPE 是另一窩坑。
坑三:exit code 會說謊
第三個坑最貴,因為它壞的方式是「看起來一切正常」。
我的排程腳本原本用 exit code 判斷成敗:工具回 0 就記成功。直到有一天我發現連續好幾天的「成功」裡,產出檔一個字都沒更新——headless 的 agent CLI 遇到權限錯誤,把 EPERM 印在輸出裡,然後回傳 exit 0。
對一般程式,exit code 是可靠的契約。對 AI agent 這種「內部吞掉錯誤繼續嘗試」的工具,它常常只代表「process 正常結束」,不代表「任務完成」。
所以我後來給每個排程任務都上了四件套,原則是壞的時候必須看得到:
| # | 做法 | 防什麼 |
|---|---|---|
| 1 | 驗證結果本身:檢查產出檔有今天的日期與預期內容,而不是看 exit code | exit 0 但實際失敗 |
| 2 | watchdog:子 process 設時限,超時 kill(macOS 沒有 timeout 指令,用 kill -0 輪詢) | 卡死佔著排程,永不結束也永不報錯 |
| 3 | 失敗主動通知:系統通知 + 狀態檔 + log 裡醒目的 FAILED + exit 非 0 | 安靜地失敗好幾天沒人發現 |
| 4 | 用真排程觸發做端對端驗證,不只手動測 | 手動過、排程掛的所有環境差異 |
第四點值得展開:手動測過不等於排程會過,這正是前面兩個坑教的事。macOS 上可以強制觸發一次真的排程執行:
launchctl kickstart gui/$(id -u)/<你的任務label>這一行跑出來的環境,才是你的任務每天晚上真正面對的環境。上線前用它做一次端對端,比手動跑十次都有用。
診斷心法:先懷疑環境,不是邏輯
三個坑收斂成一句話:互動跑得過、排程跑不過,第一個該懷疑的是環境差異——TCC、PATH、stdin、keychain、HOME——而不是程式邏輯。
有效的隔離法是建一個一次性的 probe 任務,在真排程 context 下分別測最小動作:能不能讀那個目錄、工具能不能最小執行。二分定位,比盯著自己的程式碼猜快得多。
無人值守的自動化,難的從來不是自動化本身,是接受一個事實:那個環境不是你的 shell。權限、輸入、失敗回報,全部要按它的規則重新想一遍。