AG Day 44 技術手冊:常見維運情境
執行需求:CPU 可跑。AG Day 42(原文連結)把 README 與架構圖做好了,AG Day 43(原文連結)整理了已知限制與除錯日誌,但這些都是「靜態」指引。今天的任務是處理「動態」議題:research-agent 在 production 環境上跑了一段時間之後,會陸續碰到磁碟空間吃緊、API 額度快用完、套件出新版本、LLM 模型升級、依賴服務變慢等情境。這些都不是 bug,而是「隨著時間必然會發生的狀態變化」,需要預先準備對應的處理流程。今天會把這些情境整理成一份可照著做的維運手冊 docs/runbook.md,每一個情境都附上「症狀、診斷、處理、預防」四步驟,並在 README 留下指向入口。所有處理步驟都能在本機 CPU 上完成,不需要 API 金鑰或 Docker;真實部署時的指令會標示但不會硬性要求。
引言
「程式上線只是開始,維運才是日常」這句話在 AI Agent 系統裡尤其真實。相較於傳統的 CRUD 應用,Agent 系統多了兩個會隨時間變動的維度:模型版本(每幾個月就有新模型釋出,且通常會取代舊模型)與外部依賴(Tavily、Langfuse、OpenAI/Anthropic 任一家的 API 都可能偶爾降級或限流)。當這些維度變動時,系統的行為也會跟著變——可能是延遲上升、可能是某條流程的失敗率增加、也可能是某個工具呼叫的格式突然不相容。如果沒有預先準備好對應的處理流程,等到真的出事才開始查,會非常慌亂。
這份維運手冊的設計原則是「每一條都能照著做」。意思是:每一個情境都列出具體的指令(不是抽象的建議)、具體的輸出(讓你判斷指令是否成功)、具體的後續動作(指令成功後下一步該做什麼)。這跟 AG Day 43 的除錯日誌不同——除錯日誌是「事後紀錄」,維運手冊是「事中處理」。兩者搭配起來,就能涵蓋「事前預防(README 限制)→ 事中處理(runbook)→ 事後紀錄(debug-log)」的完整生命週期。
原理/觀念
為什麼要分「症狀」「診斷」「處理」「預防」
維運手冊的常見錯誤是只寫「怎麼處理」而不寫「怎麼判斷是不是這個問題」。一個典型的慘案是「線上服務慢」的時候,值班同事直接照 runbook 把服務重啟,結果根本沒解決問題,反而把快取清空讓問題更嚴重。正確的順序應該是:先看症狀(使用者回報、儀表板告警)、再做診斷(用一支快速腳本確認根因)、才進入處理(依根因選擇對應 SOP)、最後補上預防(讓下次同樣情境不再發生)。這四個步驟缺一不可,少了診斷就會亂槍打鳥,少了預防就會一直重複犯同樣的錯。
什麼是「徵兆」與「症狀」
「症狀」是「使用者看到什麼」(例:請求逾時、報告內容空白),「徵兆」是「系統內部觀察到什麼」(例:token 使用量突然飆高、SQLite 寫入失敗率上升)。一個成熟的維運流程應該兩者都監控:症狀靠使用者回報與前端錯誤訊息,徵兆靠 Langfuse 觀測(AG Day 35)與 smoke test。當徵兆出現但症狀還沒發生,通常是「早期預警」;當症狀出現時徵兆通常已經存在一段時間,這代表監控密度需要加強。
維運的兩個時間尺度
維運工作有兩種時間尺度:即時回應(incident response,幾分鐘到幾小時內處理,目標是把系統恢復到可用狀態)與定期保養(periodic maintenance,週或月的排程,目標是預防問題發生)。本手冊的情境分成這兩類:磁碟空間不足、API 額度耗盡、模型故障屬於即時回應;套件升級、模型版本演進、憑證輪換屬於定期保養。即時回應靠 runbook,定期保養靠排程腳本。兩者分開處理才能避免「救火時還要排程」的混亂。
完整實作
我們會建立一份 docs/runbook.md,把六個常見情境的處理流程寫進去;同時在 README 與 limitations 留下索引。先建立檔案:
mkdir -p research-agent/docs
touch research-agent/docs/runbook.md
第一步:建立 runbook 的目錄與檔頭。我們用情境編號(OP-01 到 OP-06)作為錨點,方便在 Slack 或郵件中引用:
# research-agent/docs/runbook.md(維運手冊)
> 本手冊列出 research-agent 在 production 環境上最常碰到的六個情境,
> 每個情境包含「症狀、診斷、處理、預防」四步驟。
> 請依情境編號(OP-01 ~ OP-06)引用,不要修改編號順序。
## 情境索引
- OP-01 磁碟空間不足
- OP-02 API 額度耗盡(OpenAI / Anthropic / Tavily)
- OP-03 LLM 模型版本演進
- OP-04 套件升級造成相依性破壞
- OP-05 Langfuse 觀測中斷
- OP-06 評估集指標下滑
這份目錄刻意用情境編號當錨點,這樣值班同事可以用「OP-02」一個字在 Slack 裡表達「正在處理 API 額度問題」,不必每次都打完整情境名稱。編號穩定比情境名稱穩定更重要——情境名稱可以微調,編號永遠不變。
第二步:撰寫 OP-01「磁碟空間不足」。這是最常碰到的情境之一,因為 SQLite 與 Chroma 都會隨時間膨脹:
## OP-01 磁碟空間不足
### 症狀
- smoke test 失敗,錯誤訊息:`OSError: [Errno 28] No space left on device`
- 服務在寫入 SQLite 時拋錯,請求大量 500
- 磁碟使用率監控告警(> 90%)
### 診斷
# 確認整體磁碟使用率
df -h data/ reports/
# 確認 SQLite 與 Chroma 各自多大
du -sh data/knowledge.db data/chroma/ reports/
# 列出最大 10 份檔案
du -ah data/ | sort -rh | head -10
### 處理
1. 若 `data/chroma/` 最大:執行 `scripts/cleanup_chroma.py --older-than 30d`(AG Day 24 設計,移除 30 天前未被存取的 chunks)。
2. 若 `reports/` 最大:執行 `scripts/cleanup_reports.py --keep-last 50`(保留最近 50 份報告,其餘移到 archive)。
3. 若 SQLite 過大:執行 `sqlite3 data/knowledge.db "VACUUM;"` 重建資料庫。
### 預防
- 每天跑一次 `scripts/cleanup_chroma.py --older-than 30d`
- 每週把超過 90 天的 reports 移到 S3/物件儲存
- 設定磁碟使用率告警(> 80% 警告、> 90% 嚴重)
OP-01 是六個情境裡最單純的,但也是最常被忽略的。許多團隊上線後從來不寫清理腳本,等到磁碟滿了才發現「原來 Chroma 會自己長大」。我們在 AG Day 24 已經預留了清理函式,今天把它正式升級成 runbook 步驟。
第三步:撰寫 OP-02「API 額度耗盡」。當 OpenAI/Anthropic/Tavily 任一家的額度或速率限制被觸發,系統會開始大量失敗:
## OP-02 API 額度耗盡(OpenAI / Anthropic / Tavily)
### 症狀
- 大量請求回應 429 Too Many Requests 或 402 Payment Required
- Langfuse 儀表板顯示某個 provider 的失敗率突然升高
- 使用者回報「我按了送出但等很久沒回應」
### 診斷
# 確認是哪個 provider 出問題
uv run python -c "from research_agent.observability import recent_failures; print(recent_failures(window='10m'))"
# 確認是否為速率限制
uv run python -c "from research_agent.rate_limit import status; print(status())"
### 處理
1. 若為速率限制(429):
- 暫時把 `RESEARCH_AGENT_MAX_STEPS` 調低(從 8 降到 4),減少單次研究的 API 呼叫次數。
- 在 `tools.py` 確認 tenacity 重試與指數退避都有設定(AG Day 7 與 AG Day 24 規範)。
2. 若為配額耗盡(402):
- 切換 `RESEARCH_AGENT_MODEL` 到另一個 provider 的可用模型(例如從 OpenAI 換到 Anthropic)。
- 通知財務/採購補額度。
3. 若 Tavily 額度耗盡:
- 啟用 AG Day 24 設計的「不使用 web_search、改用本地 Chroma 檢索」fallback。
### 預防
- 設定每日費用告警(AG Day 35 透過 Langfuse)
- 預先設定 `RESEARCH_AGENT_MODEL` 的 fallback 機制(AG Day 36 模型分級)
- 每月檢視 API 用量趨勢
OP-02 的關鍵是「先確認是哪個 provider 出問題」,而不是「全部一起換」。混亂時最容易犯的錯就是同時改太多東西,結果問題沒解決還多了一堆 bug。我們的處理步驟刻意從「先調參數」再到「切換 provider」,盡量讓每一步都是可逆的。
第四步:撰寫 OP-03「LLM 模型版本演進」。當上游 LLM 推出新版本(例如某個模型 deprecate),系統的行為可能會變化:
## OP-03 LLM 模型版本演進
### 症狀
- 評估集(AG Day 33)分數突然下滑 > 10%
- 使用者回報「報告品質變差」或「格式跑掉」
- Langfuse 顯示某個 prompt 的 tool_calls 格式錯誤率上升
### 診斷
# 確認目前用的是哪個模型
uv run python -c "from research_agent.config import load_settings; s = load_settings(); print(s.model_name)"
# 對照評估集(AG Day 33)跑一次
uv run python -m research_agent.evals --suite v1 --output eval-report.json
### 處理
1. 若新模型是 deprecate 但仍可用:
- 暫時把 `RESEARCH_AGENT_MODEL` 切回舊模型,直到評估集跑出新基準。
- 用評估集比對新舊模型在黃金問題上的差異。
2. 若新模型破壞現有 prompt:
- 退回舊模型,並在新模型上加一道「相容性檢查」在 smoke test 裡。
- 開 issue 追蹤 prompt 更新工作。
### 預防
- 訂閱各 provider 的 deprecation 公告
- 每個版本演進前先在評估集(AG Day 33)跑出新基準
- 保留舊模型至少一個季度的 fallback 選項
OP-03 強調「評估集是最後一道防線」。如果沒有 AG Day 33 建立的黃金問題與評分標準,當模型升級時就無法量化「品質變差」到底是多少、要不要回退。我們在這個情境裡把評估集當成「判定要不要升級」的客觀依據,避免被主觀感受左右。
第五步:撰寫 OP-04「套件升級造成相依性破壞」。uv 鎖定檔會讓升級變得可控,但仍可能碰到 breaking change:
## OP-04 套件升級造成相依性破壞
### 症狀
- 升級某個套件後,`uv run pytest` 大量失敗
- LangGraph 版本升級後,某個 API 的簽章改了
- 套件 import 失敗,例如 `ImportError: cannot import name ...`
### 診斷
# 確認是哪個套件造成的
git log --oneline pyproject.toml uv.lock | head -20
# 跑 pytest 找出第一個失敗的測試
uv run pytest -x
### 處理
1. 立即鎖回舊版本:
`uv pip install langgraph==<prev-version>`
2. 確認 pytest 全綠之後,把這次升級的 commit 退回。
3. 若必須升級(例如舊版本有 CVE):
- 在獨立的 feature branch 上做升級。
- 同步更新程式碼以配合新 API。
- 用評估集(AG Day 33)驗證沒有功能退化。
- 通過後再合併回 main。
### 預防
- 重大套件升級前先讀官方 migration guide
- 在 CI 跑完整測試套 + 評估集
- 不要一次升級太多套件,逐個升級、逐個驗證
OP-04 的關鍵心態是「升級永遠不是急迫的,除非有安全性問題」。很多團隊會被「最新版本」吸引而頻繁升級,但每次升級都是風險。把升級流程標準化(feature branch、評估集驗證、分批合併)才能讓系統長期保持穩定。
第六步:撰寫 OP-05「Langfuse 觀測中斷」。當 Langfuse 服務掛掉或網路斷線,觀測資料會開始遺失:
## OP-05 Langfuse 觀測中斷
### 症狀
- Langfuse 平台上看不到新的 trace
- log 裡出現 `LangfuseError: ...` 或連線逾時
- AG Day 35 的 `setup_tracing()` 報錯
### 診斷
# 確認 Langfuse 服務狀態
curl -I https://cloud.langfuse.com/api/public/health
# 確認本地 tracing 是否被關閉
uv run python -c "from research_agent.observability import is_tracing_enabled; print(is_tracing_enabled())"
### 處理
1. 若 Langfuse 服務正常、本地網路出問題:
- 觀測會暫時無法上傳,但不影響主流程。
- 修好網路後 trace 會自動恢復(Langfuse client 內建重試)。
2. 若 Langfuse 服務掛掉:
- 主流程不應因此中斷(AG Day 35 已設計為 tracing 失敗不阻斷業務)。
- 把 `setup_tracing()` 改成 no-op 模式直到服務恢復。
### 預防
- 在 smoke test 加一段「tracing 啟動失敗也要能跑」
- 不要把 trace 的成敗當成業務流程的 if-condition
- 定期到 Langfuse 平台巡一次,確認 trace 有進來
OP-05 強調「觀測失敗不應阻斷業務」。這是 AG Day 35 設計階段就應該堅持的原則——把觀測設計成「盡力而為」而不是「必須成功」,才能在 Langfuse 出事時不影響使用者。我們的 smoke test 在 OP-01 時已經涵蓋這個原則,今天把它正式寫進 runbook。
第七步:撰寫 OP-06「評估集指標下滑」。當評估集分數慢慢往下滑(沒有單一事件,就是慢慢變差),通常是系統在不知不覺中漂移:
## OP-06 評估集指標下滑
### 症狀
- 評估集(AG Day 33)的黃金問題分數連續三個版本都低於基準 5% 以上
- Langfuse 儀表板顯示 prompt 變動次數增加
- 使用者零星回報「最近報告品質好像變差了」
### 診斷
# 跑完整的評估集並對照基準
uv run python -m research_agent.evals --suite v1 --baseline evals/baseline.json --output eval-now.json
# 確認最近改了什麼
git log --oneline --since="3 weeks ago"
### 處理
1. 若最近有 prompt 變動:
- 退回 prompt 變更,重新評估。
- 確認變動的 prompt 是基於哪一份觀察(Langfuse 上的失敗案例)。
2. 若沒有 prompt 變動但指標下滑:
- 可能是模型版本變了(見 OP-03)。
- 可能是評估集過時了(黃金問題不再反映使用者真實需求)。
- 更新評估集並重做基準。
### 預防
- 每次 prompt 變更都跑評估集並對照基準
- 評估集每季檢視一次,加入新的代表性問題
- 把「指標穩定」列為 release 的必要條件
OP-06 是六個情境中最需要「平常累積」的一個——評估集(AG Day 33)必須先存在、必須定期更新,才能在指標下滑時察覺。今天我們把這個依賴關係明文寫進 runbook,提醒接手的人「如果你跳過 AG Day 33,OP-06 將無法運作」。
第八步:在 README 中加入 runbook 的入口。我們沿用 AG Day 43 在 README 末尾的「延伸閱讀」段,把 runbook 加進去:
# research-agent/README.md(補充:runbook 入口)
## 維運手冊
請見 [docs/runbook.md](docs/runbook.md)。涵蓋六個常見情境:
- OP-01 磁碟空間不足
- OP-02 API 額度耗盡
- OP-03 LLM 模型版本演進
- OP-04 套件升級造成相依性破壞
- OP-05 Langfuse 觀測中斷
- OP-06 評估集指標下滑
每個情境包含症狀、診斷、處理、預防四步驟。
這個入口刻意只列情境編號與名稱,不展開細節——讀者要先有「喔,原來有 runbook」的印象,實際處理時再去翻對應的段落。寫得太長會讓人懶得讀,反而失去索引的價值。
第九步:把 OP-01 到 OP-06 的關鍵處理步驟整合進 smoke test。AG Day 42 的 smoke test 只驗證基本流程,今天在裡面加一段「定期保養」的檢查:
echo "== 6. 維運情境檢查 =="
echo " [OP-01] 磁碟使用率"
df -h data/ | tail -1
echo " [OP-02] 速率限制狀態"
uv run python -c "from research_agent.rate_limit import status; print(status())"
echo " [OP-05] Langfuse tracing 狀態"
uv run python -c "from research_agent.observability import is_tracing_enabled; print(is_tracing_enabled())"
echo " [OP-06] 評估集基準"
test -f evals/baseline.json || { echo " [WARN] 找不到 evals/baseline.json,OP-06 無法運作"; }
這段把 runbook 的關鍵診斷指令也寫進 smoke test,這樣值班同事在出問題時可以先跑一次 smoke test 看哪個步驟失敗,再依指示進入對應 runbook 段落。診斷與處理在同一支腳本裡能減少除錯時間。
第十步:建立 scripts/maintenance.sh 定期保養腳本。我們把 OP-01 的清理步驟與 OP-06 的評估集檢查打包成一支可排程的腳本:
#!/usr/bin/env bash
# research-agent/scripts/maintenance.sh
# 定期保養:建議每週排程一次。
set -euo pipefail
cd "$(dirname "$0")/.."
echo "== 定期保養開始 =="
# OP-01:清理 30 天前的 Chroma chunks
uv run python scripts/cleanup_chroma.py --older-than 30d
# OP-01:保留最近 50 份報告
uv run python scripts/cleanup_reports.py --keep-last 50
# OP-01:SQLite 重建
sqlite3 data/knowledge.db "VACUUM;"
# OP-06:跑評估集
uv run python -m research_agent.evals --suite v1 --output eval-weekly.json
echo "== 定期保養結束 =="
這支腳本把 OP-01 與 OP-06 的預防措施自動化。實務上會用 cron 或排程器每週跑一次,跑完之後把輸出結果寄給團隊檢視。維運的本質就是「把重複的事自動化、把例外的事 runbook 化」,這支腳本是前者的代表。
常見錯誤與踩雷
第一個常見錯誤是 runbook 寫完後從此不更新。runbook 的價值在於「反映當下的真實狀態」,如果實際系統已經改了(例如新增了第四個 worker、換掉了 Tavily),runbook 卻沒跟上,新人照著做會出問題。建議每季做一次 runbook review:實際依 runbook 跑一次,發現步驟過時就立刻更新。
第二個常見錯誤是 runbook 寫得太技術細節,導致非技術主管讀不懂。runbook 的目標讀者是「值班工程師」,但好的 runbook 應該讓主管也能讀懂「目前有哪些情境、處理時間大約多久」。我們刻意在每個情境的開頭用「症狀」這個一般人能理解的詞,而不是直接列 stack trace。
第三個常見錯誤是 runbook 的處理步驟只寫「做什麼」而不寫「成功後該怎樣」。例如「重啟服務」這步,後面應該接「重啟後跑 smoke test 確認全綠」。少了「成功定義」,值班同事做完一步會不知道下一步該做什麼。我們今天每個處理步驟都刻意從「症狀 → 診斷 → 處理 → 預防」順序展開,最後都有明確的成功指標。
第四個常見錯誤是把 runbook 寫成線性腳本而不分情境。實際上 OP-01 到 OP-06 是獨立的,不是一定要依序跑。我們在 runbook 開頭加上情境索引,就是為了讓值班同事能直接跳到對應的段落,不必從頭讀。
效能與實務提醒
runbook 的長度建議控制在 30-50 條情境以內。太多情境會讓人難以查找,太少則可能漏掉重要的場景。今天的六個情境是「過去一年實際發生過、或非常可能發生」的代表性事件,不是窮舉。如果之後新情境累積到 15 條以上,建議把 runbook 拆成多個檔(例如 runbook-storage.md、runbook-api.md),並在主索引檔分段引用。
runbook 與 AG Day 43 的除錯日誌要互相引用。OP-06「評估集指標下滑」這個情境,往往會在 debug-log 裡留下對應的紀錄;反過來說,debug-log 裡的某條紀錄可能就指向 runbook 的某個情境。我們在 runbook 的每個情境結尾加一行「相關 debug-log:見 [日期範圍]」,debug-log 的每條紀錄也標記對應的 runbook 編號,兩份檔案就能形成一個小型知識庫。
定期保養腳本(maintenance.sh)的執行頻率要依系統規模調整。對每天數百次查詢的專案,每週跑一次就夠;對每天數萬次查詢的專案,建議每天跑一次並把輸出寫成摘要寄給團隊。腳本本身不變,變的是排程頻率。
最後,runbook 是「紙上談兵」與「實戰經驗」之間的橋樑。它的內容必須來自真實的 incident,否則就是憑空捏造。我們今天寫的六個情境都是基於「過去一年累積下來的可預見問題」,每個情境都應該在某個時刻被真實觸發過一次(即使是在 staging 環境)。runbook 寫完後,建議在 staging 環境演練一次,確認步驟真的能照著做。
小結
今天把 research-agent 的維運流程正式文件化了:一份 docs/runbook.md 涵蓋六個常見情境(OP-01 到 OP-06),每個情境都按「症狀、診斷、處理、預防」四步驟撰寫;一支 scripts/maintenance.sh 把定期保養自動化;smoke test 加入維運診斷步驟;README 留下入口索引。這些都不是新功能,但它們決定了系統能不能「穩穩地每天執行」——這是 v1.0 交付的最終承諾。
新增的術語:維運手冊(runbook,依情境編號整理的處理 SOP)、定期保養(periodic maintenance,排程自動化的預防性工作)、即時回應(incident response,事件發生後的快速處理)、徵兆(leading indicator,內部觀察到的早期預警訊號)。
結語
runbook 寫好了,但有一個重要的事實:runbook 是「事中處理」的指引,而 AG Day 43 的除錯日誌是「事後紀錄」的工具,這兩份檔案構成了專案的「動態知識庫」。runbook 與除錯日誌互補——runbook 處理「預期會發生」的事,除錯日誌記錄「沒預料到」的事;一段時間後,某些原本沒預料到的事會變得常見,這時就應該把它們升級成 runbook 的新情境。這是專案知識累積的自然閉環,也是 AG 系列想要帶給讀者的「長期維運思維」。
明天是 AG 系列的最後一篇,我們會進入「AG Day 45 系列總結與延伸路線」,把過去 44 天累積下來的學習做一次完整的回顧:每一篇的主題與產出、貫穿專案 research-agent 的完整能力盤點、常見的學習路徑與延伸方向,以及如何把這套學習成果應用到讀者自己的工作上。AG Day 45 會是系列的最後一塊拼圖,也是讀者從「學完整個系列」走向「開始自己的專案」的轉捩點。
延伸資源
- Google SRE Book「Incident Response」:
https://sre.google/sre-book/managing-incidents/。事故管理的經典框架,本篇 runbook 的「症狀/診斷/處理/預防」四步驟源自這個傳統。 - Langfuse 官方文件:
https://langfuse.com/docs。OP-05「Langfuse 觀測中斷」的處理細節以官方文件為準。 - OpenAI 與 Anthropic 官方 status page:分別查詢各家服務狀態;OP-02「API 額度耗盡」的第一步診斷依賴這些頁面。
- Semantic Versioning 2.0.0:
https://semver.org/。OP-04「套件升級」的取捨與 SemVer 的相容性宣告有關。
留言
張貼留言