最近又踩到一個很有既視感的坑:一個平常穩定運作的 AI agent gateway,突然變成「看起來有反應,但就是不回話」。訊息進來了,系統也有做出收到的動作,偏偏後面的回覆流程卡死。這種狀況最煩,因為它不是完全壞掉,而是壞得很曖昧。
最後挖到的根因其實很樸素:我把 live runtime state 放在雲端同步資料夾底下了。對,就是那種「方便備份、每台電腦都看得到」的目錄。聽起來很合理,結果對一個會頻繁寫入 SQLite、session、WAL、queue state 的服務來說,這根本是在請它自殺。
不是沒收到,是卡在回覆前
一開始看到的症狀很容易誤判。
聊天平台那邊有收到事件,bot 也有做出「我看到了」的反應,所以直覺會往幾個方向查:是不是權限壞了?是不是 token 過期?是不是 intent 沒開?是不是 webhook 或 gateway 掉線?
但這些都不是。真正的線索是:事件已經進到服務裡,後面卻停在一個很無聊的位置——寫狀態。
更精準一點說,是某段 response loop 要把 session state commit 到本機資料庫時卡住。這種卡住不一定會立刻丟 exception,它可能就是靜靜地等,等到 heartbeat 超時、connection 卡死、整個 container 看起來像還活著但其實已經半身不遂。
這也是我這次最大的提醒:debug agent 系統時,不要只看「入口有沒有收到」,也要看「狀態有沒有成功落盤」。
雲端同步不是檔案系統的透明替代品
技術人的通病是很愛把東西收斂到一個「有備份」的位置。設定檔、資料、記憶、session,全丟進同步資料夾,看起來就很安心。機器壞了還能救,換電腦也不用重配,理論上很漂亮。
問題是,雲端同步資料夾不是一般 local filesystem 的透明替代品。尤其在 macOS 上,很多同步服務背後其實是 File Provider 之類的機制。它要處理 placeholder、下載、上傳、衝突、metadata、檔案協調。一般文件、圖片、筆記放在那裡通常沒事;但高頻率、小檔案、鎖定敏感、需要強一致性的 runtime database,就完全是另一回事。
SQLite 特別容易中槍。SQLite 很可靠,但它的可靠建立在「底層檔案系統行為正常且可預期」這件事上。WAL、lock、fsync、rename,這些操作如果被雲端同步層插手,問題就會變得很玄。它不一定每次都壞,而是偶爾在最不想要的時候卡住。
這種偶發性最討厭,因為你很難用一個簡單測試重現。平常都好好的,只有在某次剛好同步、剛好 commit、剛好 container I/O 壓力比較大時,整個服務就開始裝死。
我後來怎麼切
最後解法沒有什麼魔法:把 live runtime data 從雲端同步資料夾搬回真正的本機磁碟。
概念上我現在會把資料分成三類:
- 原始設定:可以進 Git 或同步資料夾,例如 template、非敏感 config、部署說明。
- 可重建資料:可以定期匯出或備份,例如報表、快照、非即時索引。
- live runtime state:不要放雲端同步資料夾,例如 SQLite database、WAL、session store、queue、lock file、container volume。
第三類就是這次的重點。它可以備份,但不要「即時同步」。比較好的做法是放在 local volume、Docker volume,或一個明確不被雲端工具接管的本機目錄。需要備份時,用排程做 snapshot、dump、rsync 到備份位置,而不是讓同步工具直接盯著正在被服務寫入的檔案。
大概長這樣:
flowchart LR
A[Agent Service] --> B[Local Runtime Volume]
B --> C[SQLite / WAL / Session / Queue]
B --> D[Scheduled Snapshot]
D --> E[Backup / Cloud Sync]
A -. 不要直接寫 .-> E
這張圖的重點只有一句:服務寫 local,備份再同步。不要讓服務直接寫 cloud sync。
Container 也救不了爛 mount
另一個容易誤會的地方是:服務跑在 container 裡,好像就跟 host filesystem 隔離了。
沒有。只要你把 host 的雲端同步目錄 mount 進 container,container 裡面的程式看到的還是那個有問題的底層行為。Docker 只是包裝執行環境,不會把不穩定的 filesystem 變穩定。
這也是我這次覺得有點蠢的地方。明明平常在部署 production service 時,我不會把 database volume 掛到奇怪的位置;但在自己的工具鏈上,因為想省事、想備份方便,就不自覺把原則放掉。結果 production 經驗沒救到我,反而是自己的懶惰埋了一顆雷。
如果你有跑任何長駐型 agent、bot、crawler、scheduler,我會建議直接檢查:
- runtime database 是不是在雲端同步資料夾裡?
- Docker bind mount 的 host path 是不是被同步工具管理?
- session / queue / cache 是否跟文件備份混在一起?
- 有沒有把「備份」跟「即時工作目錄」當成同一件事?
如果答案是有,先搬。不要等它壞。
Debug 這類問題的幾個訊號
這次也讓我整理出幾個判斷方向。
第一,如果 bot 或服務「有收到事件但沒有產出結果」,不要只查入口。要看 response loop 卡在哪裡,特別是 database commit、state save、log write 這些看似無害的地方。
第二,如果 container restart、kill 都變得很怪,或 runtime process 卡到連正常關閉都不乾脆,就要懷疑底層 I/O。很多時候不是 application logic 死掉,而是某個檔案操作堵住。
第三,如果問題發生在 macOS、桌面機、開發機、自架工具,而且路徑裡有雲端同步痕跡,那真的不要鐵齒。雲端同步很適合保存成果,不適合承擔資料庫的熱寫入。
備份是好事,但同步不是萬靈丹
我現在對這件事的結論很簡單:備份策略要分層,不要把所有資料都丟進同一個同步資料夾假裝安全。
真正重要的資料不只是「要不要備份」,還要問:它在運作中需要什麼一致性?它會不會高頻寫入?它壞掉時能不能重建?它適不適合被雲端工具即時監控?
對 CTO 或技術主管來說,這不是一個小工具的踩坑而已,而是很典型的架構取捨:便利性、安全感、可恢復性、穩定性,常常不是同一個方向。雲端同步給的是便利跟某種心理安全感,但 runtime 系統要的是可預期的 I/O。
下次如果我又想把某個服務的 data directory 丟進同步資料夾,我希望自己先停三秒,問一句:這裡面有沒有 database、lock、queue、WAL?
有的話,拜託,放過它,也放過未來半夜 debug 的自己。