最近在處理一個 AI 助理接聊天平台的功能時,我又踩到一個看起來很小、實際上很煩的坑:slash command 明明已經在程式裡加了,服務也重啟了,使用者介面就是看不到。
聽起來很像快取問題對吧?對,一開始我也很想把鍋丟給快取。畢竟聊天平台的 slash command 本來就常有同步延遲,client 端也可能自己留一份舊資料。但這次真正的問題不是平台慢,而是我們自己的狀態機太樂觀:它把「準備同步」記成了「同步成功」。
看起來一樣的 /command,其實不是同一件事
先講第一個容易混淆的地方:同樣長得像 /xxx 的指令,在一個 AI 助理系統裡可能有兩層。
第一層是聊天平台原生的 slash command,也就是你打 / 之後,平台介面會跳出選單、顯示描述、帶 autocomplete,最後送出一個結構化 interaction 給 bot。
第二層是 agent 自己的文字解析器。也就是你直接傳一段文字,例如 /xxx do something,bot 收到訊息後自己解析,當成內部指令執行。
這兩者在使用者眼中都叫 /xxx,但對工程師來說完全不是同一條路徑。
flowchart LR
A[使用者輸入 /xxx] --> B{平台是否有原生指令?}
B -- 有 --> C[平台送出 structured interaction]
B -- 沒有,只是文字 --> D[Bot 收到一般訊息]
C --> E[Gateway handler]
D --> F[Agent text parser]
E --> G[執行 skill / action]
F --> G
這張圖很簡單,但它解釋了很多「明明可以手打,為什麼選單看不到」的問題。
因為手打能執行,只能證明 agent 的文字解析器有支援;不能證明聊天平台上的原生 slash command 已經註冊成功。反過來,平台選單看得到,也不代表文字 parser 一定支援同樣的語法。
這種 UX 最麻煩的地方是:使用者看到的是同一個名字,工程師 debug 的卻是兩個系統。沒有先把路徑拆開,很容易在錯的地方找半天。
真正陰險的是 sync state
把兩層路徑拆清楚後,下一步就是確認原生命令到底有沒有註冊上去。
這類系統通常會做一個最佳化:啟動時算出 command definition 的 fingerprint。如果這次定義跟上次一樣,就不要再打 API 同步,避免每次重啟都去更新遠端設定。
想法沒問題,而且很常見。問題出在「什麼時候寫入這個 fingerprint」。
這次踩到的 bug 大概是這樣:
- 啟動時發現本地 command 定義變了;
- 系統準備呼叫平台 API 同步;
- 在同步真正成功前,先把新的 fingerprint 寫進 sync state;
- 後面同步因為某個原因中斷或失敗;
- 下次重啟時,系統看到 fingerprint 已經一樣,就判斷「不用同步」。
然後災難就開始了。
本地狀態說:「我同步過了。」
遠端平台說:「我沒收到。」
使用者介面說:「我什麼都看不到。」
工程師說:「蛤?」
這種 bug 特別討厭,因為它不是單純的失敗,而是失敗後留下了一個會阻止自我修復的錯誤狀態。你重啟服務也沒用,因為重啟反而會更堅定地相信那份錯的 state。
狀態機要記成功,不要記意圖
這件事給我的教訓很直白:狀態機應該記錄已確認的事實,不要記錄尚未完成的意圖。
「我要同步」不是狀態。
「我已經成功同步到遠端,而且遠端回應確認」才是狀態。
如果真的需要記錄中間過程,也應該明確分開,例如:
pending:偵測到需要同步,但尚未送出;in_progress:同步正在進行;success:遠端確認成功,並記錄成功時間;failed:同步失敗,保留錯誤原因與下一次重試條件。
最怕的是用一個欄位同時代表「我想做」、「我做了」、「我做成功了」。平常看起來省事,出事時就是一坨糊。
尤其整合外部平台時,成功不能只看本地流程有沒有跑到最後。你要問的是:遠端實際狀態是不是我以為的那樣?如果沒有讀回確認,至少也要把 API 的成功回應當成寫入 state 的最低門檻。
Debug 時不要相信單一真相來源
這種同步問題,我現在會刻意看三份資料:本地定義、本地 sync state、遠端實際命令。
本地定義回答的是:「程式想要註冊什麼?」
本地 sync state 回答的是:「程式以為自己註冊到哪個版本?」
遠端實際命令回答的是:「平台上現在真的有什麼?」
三者只看其中一個都不夠。只看程式碼,你會覺得功能已經寫了;只看 state,你會被錯誤 fingerprint 騙;只看使用者介面,又可能被平台或 client cache 誤導。
所以我比較喜歡用一個很土的方法:直接打遠端 API 查目前註冊的 commands。不要猜,不要拜託快取快一點,也不要一直重啟服務求神明保佑。
工程裡很多 debug 最後都會回到這句話:看實際資料。
不是看你以為送出去什麼,也不是看程式理論上會做什麼,而是看對方現在真的收到了什麼。
Autocomplete 也有產品細節
同一輪處理中,還有另一個小坑:slash command 的 autocomplete 不是無限列表。很多平台會限制一次最多顯示固定數量的 choices,例如 25 筆。
這代表如果你把一大堆可執行能力都塞進同一個選單,使用者打開 /skill 看到的可能只是排序前面的那一小段。他要找的指令其實存在,但因為沒有輸入關鍵字,根本不會出現在候選清單。
這不是後端功能壞掉,而是產品體驗壞掉。
所以後來我會傾向把常用能力拉成少數原生 shortcut,剩下的放在通用 skill runner 裡,並且讓 autocomplete 搜尋描述、別名與命令 key。換句話說,不要把平台選單當資料庫瀏覽器,它承載不起完整知識庫。
這也是 AI Agent 做產品化時很容易忽略的地方:能力很多不等於體驗好。使用者需要的是「我想做的事很快找得到」,不是「理論上所有東西都可以被呼叫」。
我會怎麼修
如果要把這類同步流程做得比較穩,我會抓幾個原則:
第一,fingerprint 只能在遠端同步成功後更新。失敗就保留舊狀態,讓下次啟動仍然知道需要重試。
第二,state 裡要保留 last_success_at、last_error、遠端回應摘要。不要只留一個 hash,因為 hash 告訴你版本,卻不告訴你過程。
第三,提供人工驗證或修復工具。像是「列出遠端目前有哪些 command」、「強制重新同步」、「清掉本地 sync state」這種操作,平常用不到,出事時會救命。
第四,補 regression test。這類 bug 很容易修完一次,半年後重構又回來。測試至少要覆蓋「同步失敗時不能寫入成功 fingerprint」以及「舊 state 不應該阻止必要同步」。
結論:整合系統最怕假成功
這次踩坑其實不大,但很典型。
AI 助理、聊天平台、技能系統、遠端 API、client cache,全部串在一起後,最危險的不是單點失敗,而是假成功。因為失敗會吵,假成功會安靜地讓你相信世界已經照你的意思更新了。
做整合型系統時,我越來越在意「確認」這件事。寫入前可以樂觀,寫入後一定要保守。狀態機不要記你的願望,要記已經被驗證的事實。
如果一個系統能誠實地說「我還沒同步成功」,那其實很好修。最麻煩的是它笑笑地告訴你「都好了」,然後遠端什麼都沒有。
這大概也是工程裡最樸素、但最有用的原則之一:不要把意圖當成果,不要把本地狀態當世界真相。尤其當你的使用者正在介面前面找一個看不到的 /command 時,這句話會突然變得非常有感。