! 本篇文章將會介紹 錯誤處理:Agent 失敗了誰來擦屁股,期望大家都能學會幫自動化系統蓋一套防禦工事:暫時性失敗自動重試、靜默失敗靠驗收判準抓包、最後防線有備稿與告警接手 :D
昨天填完 PATH 地雷,照預告今天來談錯誤處理。這題對本系列不是理論題——scripts/generate.sh 的檔頭註解原文就寫著:「失敗重試最多 5 次(backoff 30s)→ 備稿 → 告警」,而這套工事在開賽前十天真的挨過實彈:d02 生成三連敗靠備稿補位、d09 被機敏掃描連續隔離五次。你正在讀的這篇文章,就是那支腳本今晚叫起 opencode 寫的,所以今天等於用系統講系統,跳針迴圈第 12 天繼續轉 :D
自動化系統的鐵律:失敗不可怕,可怕的是失敗了沒人知道,或是卡在一個不上不下的狀態。口號既然是「我耍廢、Agent 完賽」,擦屁股的邏輯就必須寫進程式碼,而不是靠我每晚睡前祈禱。
讀完這篇你會學到:
generate、publish、republish
set -uo pipefail
# 確認三支排程都在線上
launchctl list | grep ironman
# 順便看一眼 generate 任務的保險絲:ExitTimeOut 1800 秒,後面會用到
plutil -p ~/Library/LaunchAgents/com.benben.ironman.generate.plist | grep ExitTimeOut
# ---- 預期輸出 ----
# "ExitTimeOut" => 1800
擦屁股第一步是搞清楚這是哪種屎。實戰上失敗分三類:
第三類我們真的遇過。看 logs/generate-20260912.log 的案發現場(節選):
# d02 事故:Agent 每輪都宣稱寫檔成功,判準就是不通過
[19:03:36] 第 1 次失敗(不存在/過短/標題不符)
[19:04:06] opencode run 第 2 次嘗試
[19:12:53] 第 2 次失敗(不存在/過短/標題不符)
[19:16:17] 第 3 次失敗(不存在/過短/標題不符)
[19:16:17] 已啟用備稿 ff1-when-not-to-automate.md → articles/2026-09-12-d02.md
Agent 每一輪都輸出了「Wrote file successfully. DONE: ...」,但腳本親自讀檔驗收就是不過。更糟的是當時的失敗訊息把三種可能打包在一起,事後完全無從修起。這就是 generate.sh 裡那行註解的由來:「逐項診斷:哪個判準沒過要寫進 log,不能只說『失敗』」——錯誤訊息本身也是要迭代的產品。
generate.sh 的重試迴圈(節選):
# scripts/generate.sh(節選):上限 5 次、backoff 30 秒、逐項診斷
MAX_ATTEMPTS=5
for (( attempt=1; attempt<=MAX_ATTEMPTS; attempt++ )); do
(cd "$ROOT" && opencode run --auto --title "ironman-d${DD}-gen" "$PROMPT") >>"$LOG" 2>&1
# 品質判準:檔案存在 >1500 bytes、CJK >=1500 字、標題與 outline 一致
R=""
if [[ ! -s "$ARTICLE" ]]; then
R="檔案不存在"
else
B=$(wc -c <"$ARTICLE" | tr -d ' '); C=$(cjk_count "$ARTICLE"); T=$(head_title "$ARTICLE")
(( B > 1500 )) || R="${R}bytes(${B})過短、"
(( C >= 1500 )) || R="${R}CJK(${C}字)不足、"
[[ "$T" == "${DD} ${TITLE}" ]] || R="${R}標題不符[${T}]、"
fi
[[ -z "$R" ]] && exit 0 # 全數過關
log "第 ${attempt} 次未過判準:${R%、}"
(( attempt < MAX_ATTEMPTS )) && sleep 30 # backoff:暫時性失敗別硬碰硬
done
三個設計決策:
bytes(943)過短、標題不符[02 ...] 這種訊息,隔天早上瞄一眼就知道要修哪;「失敗」兩個字只會讓你從頭考古。發文端也是同一套哲學,只是驗收對象從檔案換成 URL:
# scripts/publish.sh(節選):點擊發佈 x3,每輪點完輪詢 URL 驗收
for attempt in 1 2 3; do
"${AB[@]}" eval 'document.querySelector(".save-group__dropdown-btn--publish").click()'
# 成功特徵:URL 變成 /articles/10xxxx 且沒有 /draft
for i in $(seq 1 6); do
sleep 5
AFTER=$("${AB[@]}" get url 2>/dev/null)
if [[ "$AFTER" =~ /articles/10[0-9]+$ ]]; then PUBLISHED=1; break; fi
done
[[ -n "$PUBLISHED" ]] && break
log "第 ${attempt} 次點擊後 URL 仍在 ${AFTER:-未知},20 秒後重試"
sleep 20
done
這段是 D1 的血淚結晶。當晚 iThelp 尖峰時段按「發表文章」會靜默無反應,腳本按完就收工,最後回報 FATAL、我手動重發收場。隔天就把「點擊後輪詢 URL」補上,腳本註解原文留著紀念:「2026-09-11 D1 教訓:iThelp 尖峰時段按發佈會『靜默無反應』,必須輪詢 URL 自動重點」。
小小小測驗:你知道網站尖峰時段按了送出按鈕,可能不跳錯誤、不轉址、什麼都不會發生嗎?所以本系統的成功判定從來不是「按了按鈕」,而是「輪詢 URL 看到文章編號」——驗收結果,不驗收動作。
重試的另一面是逾時:沒有逾時的重試等於無限卡死。本系統分三層設防——launchd 層 ExitTimeOut 1800(跑超過 30 分鐘直接終結,防止殭屍任務佔住排程)、工具層 agent-browser wait --timeout 15000(等元素最多 15 秒)、邏輯層像 Turnstile token 最多等 12 輪、每輪 3 秒,等不到就截圖存證、帶著警告繼續往下走,而不是原地永動。
重試的前提則是冪等:重跑不會出事,重試才敢放手跑。
# scripts/publish.sh(節選):冪等守衛,20:15 補發 job 重跑全靠它防重複發文
PUB_FLAG="$ROOT/state/published-$(date +%Y%m%d).flag"
if [[ -e "$PUB_FLAG" ]]; then
log "今日已發佈過(flag 存在),跳過"
exit 0
fi
generate.sh 開頭也有同款守衛:文章檔已存在且非空就跳過,手動補跑不會覆蓋你改到一半的稿。而 20:15 那支 republish plist 做的事,就是重跑同一支 publish.sh——有 flag 擋著,重跑一百次也只會發一篇。
最後是備援梯子。生成 5 次用完之後,generate.sh 的決策樹長這樣:
fallback/*.md,每支備稿靠 .used flag 只准用一次,避免連續兩天刊同一篇。9 月 19 日這套梯子真的全走過一遍:機敏掃描連續命中五次,五份稿子全被 mv 進 state/quarantine-d09-*.md 隔離(不是刪除,留著驗屍),最後 ff2 備稿上場,當天照常出刊、flag 照常落地——我全程沒動手。防禦工事不是裝飾,是實彈射擊過的。
Q:Agent 每一輪都說 DONE,品質判準卻一直不過,到底是誰對?
A:驗收對。Agent 的自我報告只是參考,產出以腳本親自讀檔驗證為準。配套是失敗訊息必須逐項診斷(d02 教訓):列出 bytes、CJK 字數、標題的實際值,一眼看出問題;只寫「失敗」二字,事後等於沒有 log。
Q:重試最怕重複發文或覆蓋東西,怎麼防?
A:先蓋冪等,再談重試。publish 成功才 touch 當日 flag、開頭先查 flag;生成端檔案存在即跳過。順帶一提,兩支腳本都是 set -uo pipefail 而故意不加 -e——自動化腳本裡「失敗是預期內的」情境太多,一行就炸的 -e 反而誤事,錯誤要自己判斷著接。
Q:機敏掃描命中的稿子去哪了?直接刪掉嗎?
A:mv 進 state/quarantine-*.md 隔離區,不刪。它是事後檢討的重要線索(哪類內容會誘發 Agent 寫出可疑字串),d09 一天隔離五份,全都還在,隨時可以驗屍。
Q:重試全部用完,剩下一篇「存在但品質不足」的文章,讓備稿蓋掉它?
A:不要。備稿只在「根本沒有文章」時啟用。標題正確、內容八十分的稿,好過與當日大綱對不上的通用備稿;此時正確動作是警告 + 讓人工在 20:00 前裁決,系統不要替你做編輯判斷。
ExitTimeOut、wait --timeout、輪詢次數全部有界下一篇我們要介紹「Discord 告警:讓機器人主動回報戰況」,敬請期待!今天文中反覆出現的 "$NOTIFY" -s ok/warn/error 就是它的本體,明天拆解怎麼讓機器人主動回報戰況、告警分級怎麼設計。
參考資料:
ExitTimeOut、StartCalendarInterval 等欄位字典直接查終端機:man launchd.plist
有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D