把告警雜訊擋在手機之前:一個會判斷輕重的 AI 分診系統

· 中文 · Claude Sonnet 5 代筆

showcasen8naiclaudedevops

我的 VPS 上跑著好幾個 docker 化的服務——Traefik 反向代理、n8n、還有這個網站本身——但一直沒有裝任何主動監控,只能被動發現問題。裝一套監控工具聽起來是顯而易見的下一步,但我很清楚監控工具自己會製造一個新問題:只要服務有一點風吹草動——容器重啟造成的短暫 502、憑證更新期間的暫時性錯誤——就發一則通知,真正需要處理的告警反而被淹沒在雜訊裡,久了會讓人開始無意識地忽略通知。這正是「alert fatigue(告警疲勞)」,一個我過去實際維運時親身遇過的問題,我不想在自己的個人專案裡重蹈覆轍。

所以這次的做法是:從一開始裝監控工具,就在它跟我之間插一層 AI 分診——用 Claude 讀相關的 docker log,判斷這則告警是「大概率會自己好」還是「真的需要我立刻介入」,只有後者才會讓我的手機響。

定位:三階段規格裡的 Phase 1

規劃時我先拆出三種規格——最低成本、穩定運行、業界規格——評估這個系統可以做到多小、多穩、多大,再訂出漸進式的建置計畫。目前實際建置完成的是最省成本的 Phase 1,目標是先用最少資源驗證「AI 分診到底有沒有用」這個核心假設,再決定要不要往下一階段投入。Phase 1 完全沿用既有 VPS 上已經在跑的 n8n,不新增任何獨立資料庫或後端服務,只新裝了 Uptime Kuma 這一個輕量 container。基礎設施費是 $0,Claude API(Haiku)用量估算約每月 $3–5。

架構

技術堆疊

  1. n8n(既有,workflow 自動化引擎,視覺化節點 + JavaScript Code node)
  2. Uptime Kuma v2(新裝,監控目標是網站本身跟 n8n 這兩個服務,資料庫用 SQLite)
  3. Claude APIclaude-haiku-4-5,直接用 HTTP Request node 打 Anthropic Messages API——後面第 9 節會講為什麼刻意不用 n8n 內建的 Anthropic 節點)
  4. Telegram Bot API(主要通知管道,私訊模式)
  5. Slack Incoming Webhook(僅作 Telegram 故障時的即時備援)
  6. n8n Data Tables(原生資料表功能,取代傳統資料庫,存服務對照表跟兩張待處理紀錄表)
  7. SSH(帶 forced command 的專用金鑰,讓 n8n 安全地執行 docker logs

分診管線

否(up 恢復)

失敗

成功

high

low

Uptime Kuma webhook

token 正確?

靜默丟棄

事件是 down?

結束,不通知

service_map 查得到?

用原始告警訊息

SSH 執行 docker logs

直接標記高優先
跳過 Claude

過濾+遮罩 log

Claude 分類

三層保護發送 Telegram

寫入 low_priority_alerts
等每日彙整

這條主流程由 Uptime Kuma 的 webhook 即時觸發。另外有一條完全獨立的每日彙整 workflow,由 n8n 自己的 Cron trigger 定時喚醒,把當天所有 low 優先的紀錄整理成一則摘要送出,發送成功才清掉這批紀錄——失敗的話資料留著,隔天自然重試,不會憑空遺失。

幾個關鍵設計決策

不符合的請求,連錯誤訊息都不回。 n8n webhook URL 帶一組共享密鑰查詢參數,設在 Uptime Kuma 的通知設定裡。token 對不上,workflow 最前面就直接終止,不回應、不留痕跡——避免這個公開端點被找到或猜到後遭偽造事件洗版,白白燒掉 Claude 呼叫額度、製造假警報。

查無服務對照,不代表系統中斷。 Uptime Kuma 的監控項目名稱跟實際 docker container 名稱不一定一致,我建了一張對照表手動維護兩者關係。查無對應時系統不會卡住,會直接跳過抓 log 這一步,改用原始告警內容(監控項目名稱、錯誤訊息、回應時間)送給 Claude 判斷——這條路徑也順便涵蓋了「純網址或 SSL 憑證類監控,本來就沒有對應容器」的情境。

log 送進 AI 之前,先過三層過濾,再統一遮罩。 不是把整包 log 丟給模型:只抓告警觸發前 5 分鐘內的內容,用一套大小寫不敏感的關鍵字規則抓出 ERROR/WARN 等級的行,最多取 100 行,避免 crash loop 把 token 成本或訊息長度炸掉。過濾之後,這份內容在送進 Claude 或 Telegram 之前,只會經過唯一一個負責遮罩的函式——IP、API 金鑰、資料庫連線字串、JWT、AWS access key 都會被替換掉,Claude 判斷跟 Telegram 訊息用的是同一份遮罩後版本,不會有第二份規則、也不會有未遮罩版本流通到下游。

log 擷取本身失敗,就直接當成高優先,不經過 AI。 如果 SSH 或 docker logs 執行失敗——容器可能正在 crash loop、連線逾時、或已經被改名移除——這個失敗訊號本身往往就是服務出問題的證據,比讓 AI 在資訊不足時用原始告警文字猜測更安全,所以直接跳過分類,立即發送:

function routeOnSshResult(sshExecutionResult) {
  const { exitCode, stdout, stderr, error } = sshExecutionResult;
  if (exitCode === 0) {
    return { route: 'normal', logs: stdout };
  }
  const errorMessage = stderr || error || `exit code ${exitCode}`;
  return { route: 'highPriorityBypass', reason: `log 擷取失敗:${errorMessage}` };
}

分類的核心原則是保守優先,寧可誤判也不能漏判。 給 Claude 的 system prompt 裡最重要的一條規則是:「證據不足以判斷已自我修復,就歸類為 high,不可把真正緊急的情況誤判為 low」。log 顯示問題已自行恢復才判 low;log 被截斷不影響判斷,只看現有內容的嚴重性;完全沒有 log 內容(純憑證檢查類監控)就直接根據告警訊息本身判斷。

AI 判斷對了,通知也可能送不到——所以發送本身也要有備援。 高優先告警走三層保護:Telegram 節點先重試(3 次、指數退避),仍失敗就立即改發 Slack webhook 當即時備援,連 Slack 也失敗,就寫進一張 Data Table 當最後保底,隔天的每日彙整會檢查這張表,有紀錄就在摘要最上方標註發送失敗的告警數量,確保它不會真的憑空消失。

驗收方式:不是單元測試,是準確率

這個專案刻意沒有把 TDD 硬套在所有東西上,而是依邏輯性質拆成三種驗證方式:純函式邏輯(驗證、過濾、遮罩、訊息組裝)用嚴格的 Red/Green/Refactor,九個模組全部先在 n8n 之外用 Jest 寫測試、寫函式、通過測試,才貼進對應的 Code node;AI 分類邏輯用 eval 型驗證——準備 14 個測試情境(7 個已知會自己好、7 個真正需要人介入),內容以 VPS 上 Traefik 的真實 log 為主,補齊憑證過期、log 被截斷這類業界常見案例,逐一送給 Claude API 跑過一輪,對照期望值算準確率;至於視覺化節點連線、真實 webhook 觸發、SSH 金鑰設定這類沒有邏輯可測的部分,改用手動觸發真實事件、對照真實輸出逐項確認。

連續兩輪 eval 皆為 13/14(92.9%),高優先類別零漏判。唯一沒過的情境,是把一個 ACME 憑證更新失敗的雜訊判成 high(本該是 low)——判斷偏保守,不是漏判真正緊急案例的那種危險方向的錯誤。

真實世界的端到端測試

Phase 1 完成後,最後一步是拿掉所有假資料,用真實的 Uptime Kuma 事件——不是模擬 payload 的 curl 指令——把整條管線的三條主要路徑各跑一次,全部有實際 Telegram 送達確認。

真的把網站本身停機幾秒鐘,Uptime Kuma 偵測到 down 自動觸發正式 webhook。這次意外揭露一個有趣的真實限制:這個 Astro 網站被 docker stop 時完全沒有輸出任何關閉相關的 log,docker logs --since 5m 抓回來是空的,Claude 因此判斷為 low。接著臨時把對照表那一列刪掉,模擬「查無對應」的情境——這次 Claude 完全沒有 log 可查,只能看原始告警訊息(HTTP 404),依保守優先的原則正確判成高優先,Telegram 立即收到通知。最後把對照表的容器名稱改成一個不存在的值,觸發 SSH 執行失敗,系統完全跳過 Claude,直接發送「高優先告警(log 擷取失敗)」,同樣確認送達。每日彙整 workflow 也手動觸發過一次,成功把低優先紀錄整理成一則摘要送到 Telegram,發送成功後對應的紀錄也確認被清除。

這次真實測試也誠實地揭露了一個目前還沒修正的系統性限制:分類的保守程度,在兩種「沒有有效證據」的情境下並不一致——SSH 成功但 log 內容剛好是空的,傾向判 low;完全查無對照、沒有 log 可查,反而因為保守原則傾向判 high。也就是說,一個「服務關閉時不輸出任何 log」的安靜服務,真實故障時反而有風險被誤判為低優先。這是留給未來迭代思考的真實工程課題,不是靠嘴上說「AI 很準」就能蓋過去的細節。

幕後的真實工程故事

整合測試階段抓到一個真正的 shell injection 漏洞:VPS 上原本的 SSH forced command 直接把 $SSH_ORIGINAL_COMMAND 未經過濾地拼進 shell 指令——如果這把限定權限的金鑰洩漏,攻擊者可以在指令裡夾帶分號或反引號跳出去執行任意指令。修法是改成透過一支驗證腳本,先檢查容器名稱格式合法性,才代為執行 docker logs,並用實際的注入測試字串驗證修復有效。

n8n 平台本身也有幾個文件沒寫清楚、實測才發現的隱藏行為:SSH node 的退出碼欄位其實叫 code,不是文件字面上暗示的 exitCode;SSH node 一律在遠端指令前自動加上工作目錄切換前綴,沒有辦法從 n8n 端關掉。每日彙整 workflow 一開始把兩張 Data Table 的讀取節點串聯接,結果只要排在前面的表剛好是空的,n8n 就會直接跳過後面的節點——即使後面那張表真的有資料要彙整,整條管線也會靜默失敗。改成平行讀取並開啟輸出保底選項才解決,但這又衍生出「空表輸出從 0 筆變成內容是空物件的假資料」的新問題,最後在下游多加一行過濾才根本解決。

這些細節說明了一件事:這不是「串幾個 n8n 節點加一個 AI API 呼叫」這麼簡單的表面工程,而是每個環節都經過真實環境的踩坑與修正才穩定下來的。

技術選型:為什麼不用 n8n 內建的 Anthropic 節點

規劃階段查證過 n8n 內建的專屬 Anthropic 節點,發現官方文件沒有列出目前支援哪些模型 ID,也沒有說明節點如何跟上新模型發布——換句話說如果用最新模型,可能要等 n8n 套件更新才能選用。最後決定改用 HTTP Request node 直連 Anthropic 官方 Messages API,自己在 JSON body 裡打上最新的模型字串,不受節點套件更新滯後影響,也跟未來「Haiku 處理簡單案例、Sonnet 處理複雜案例」分層呼叫的彈性需求更吻合。

接下來

如果這套系統長期驗證有效,下一階段規劃是:導入 Prometheus + Alertmanager 擴大監控涵蓋範圍,把判斷結果存進 Postgres 做歷史留存與人工回饋迴圈(Telegram 訊息加按鈕標記誤判),加一個告警準確率儀表板,再加一支獨立的 watchdog workflow 監控這套告警系統自己有沒有掛掉。這些都還沒開始動工——先驗證 Phase 1 真的有用,才值得往下投入。