多模態那篇講的是「媒體先轉成文字,再進同一條對話管線」。這一篇是它的續集,處理的是反方向的問題:有些文字根本不該進那條管線。
使用者傳位置之後,我們會給一排按鈕:「☀️ 這裡的天氣」「🍜 附近吃的」「🅿️ 停車場」。最早的實作用的是 LINE 的 message action——按下去,等於使用者自己打了那句話。那句話接著走完整的模型管線:讀提示詞、帶對話歷史、選工具、填參數,再寫一輪回覆。
問題是,那句話是我們自己寫的。「附近哪裡可以停車」要對應到停車場類別,這件事沒有任何不確定性,卻每按一次就付四千多個 token 請模型確認一次。
按鈕送出的是一句話,還是一個意圖
LINE 的按鈕有兩種送法,差別決定了後面整條路怎麼走:
| Message action | Postback action | |
|---|---|---|
| 按下去送出什麼 | 一句文字,和使用者自己打的無法區分 | 一段給程式的結構化資料,另外可帶一句顯示在聊天室的文字 |
| 誰來理解它 | 模型,走完整管線 | 程式直接分派 |
| 按鈕文案改了會怎樣 | 判斷邏輯可能跟著失效 | 顯示文字隨便改,資料不動 |
改成 postback 之後,每顆按鈕的資料長得像 v=1&k=loc.parking:版本、意圖 id,必要時再帶幾個參數。聊天室裡顯示的仍是原本那句話,使用者看不出任何差別。
用上線前實測的每輪成本對照:
| 按鈕 | 改動前 | 改動後 |
|---|---|---|
| ☀️ 這裡的天氣 | 2,779~2,887 tokens | 0 |
| 🍜 附近吃的、🅿️ 停車場 | 4,336~4,692 tokens | 0 |
| 天氣追問時的縣市按鈕 | 約 3,900 tokens | 0 |
附近搜尋那一列的地圖 API 費用不變——那是外部帳單,跟誰來理解按鈕無關。
按鈕文字會被當成使用者說的話
省 token 是這件事比較不重要的一半。更重要的一半是:message action 的文字會被模型當成使用者的意圖重新判讀,措辭一錯,工具就路由錯。
我們在正式站遇過一次。使用者要新增一筆只說了「下午」的行程,助理追問幾點,並給了幾顆整點按鈕。產生這排按鈕的函式被「新增行程」和「修改行程」兩條路徑共用,按鈕文字寫死成「〈行程名稱〉改成 14:00」。使用者按下去,模型照字面理解成修改,去找一筆根本還沒建立的行程,回了「找不到」。
這個缺陷的每一段看起來都正常。兩輪都是「需要更多資訊」,工具層面沒有錯誤,日誌沒有紀錄,也不會觸發任何告警。它從那排按鈕上線起就存在,直到使用者回報才被發現。按鈕是我們給的,使用者按了卻壞,比他自己打錯字嚴重得多。
修法是把「哪個意圖、哪一筆、什麼值」從句子裡拿出來,變成結構化資料。新增路徑改走 postback:待建立的行程先存進 Redis 十五分鐘,按鈕只帶一個暫存 id 與時刻,程式直接建立。修改路徑保留文字,因為「改成」對它是正確的;產生器則拆成兩支,讓新增路徑不可能誤用。
這個案例也暴露了測試的盲點。原本的單元測試只斷言按鈕的 label,沒有斷言按下去實際送出的內容。對按鈕來說,label 是文案,送出的內容才是行為。
跳過模型之後,模型原本順手做的事要自己補
走完整管線時,有幾件事是那條路「順便」完成的。改走 postback 之後,這些事不會自己發生:
| 原本順手完成的事 | 跳過模型之後要自己做的 |
|---|---|
| 對話記憶 | 用按鈕的顯示文字寫一筆記憶,連位置前綴一起存(「(我現在在臺北市信義區)這裡的天氣」),下一句「那明天呢」才接得上 |
| 防濫用與配額 | 0 token 不代表 0 成本。每分鐘的請求上限照樣套用,附近搜尋的方案權限與地圖 API 每日上限一個都不能少,否則連按就變成外部帳單的放大器 |
| 遙測 | 每一輪記下觸發方式是 postback。否則資料裡分不出「按的」與「打的」,也就量不到按鈕到底有沒有被用 |
| 統計口徑 | 這類輪次永遠成功,要從模型失敗率的分母裡排除,否則會把真正的失敗率稀釋掉 |
| 舊按鈕 | 聊天室裡的按鈕會一直留著。資料帶版本號,認不得的舊按鈕回一句溫和的話,而不是報錯 |
還有一條隱私上的限制:**postback 資料不放使用者的自由文字。**那段資料會出現在 webhook 的請求紀錄裡。行程名稱這類內容應該存在自己的暫存區,按鈕只帶一個短 id 回查。
回覆也一樣:數字讓程式碼寫
理解意圖是模型的第一輪,寫回覆是第二輪。天氣是使用量最高的工具,而它的回覆原本也是模型寫的。
這一輪有兩個問題。第一是成本:第二輪要把固定提示、對話歷史與天氣資料再送一次,實測約 1,300 tokens 起跳。第二是模型會改寫數字——「降雨機率 30%」被寫成「可能會下雨」,或把兩個時段的資料混在一起講,而使用者無從察覺。
所以天氣改成一張由程式組的卡片:大字溫度、天氣現象、降雨機率。底下的提示也是規則,不是模型:
| 條件 | 提示 |
|---|---|
| 天氣現象含雷雨 | 避開空曠處與大樹下 |
| 降雨機率 ≥ 60% | 出門記得帶傘 |
| 降雨機率 30~59% | 帶把傘比較保險 |
| 高溫 ≥ 35° | 補水防曬 |
| 低溫 ≤ 12° | 注意保暖 |
| 日夜溫差 ≥ 8° | 洋蔥式穿法 |
降雨與氣溫各最多一條。這幾條規則涵蓋人最常問的「要不要帶傘、穿什麼」,而且不會編。
打字問的天氣也走同一張卡片,但多了一個判斷:使用者只是問預報,還是另外問了穿著或活動?這個判斷交給模型——在工具參數裡加一個布林值 wants_advice——但判斷之後走哪條路由程式決定。只問預報就直接回卡片,第二輪不跑;問了「穿短袖會冷嗎」,卡片照送,第二輪只回答那個問題,並被告知數字已經在卡片上。
這不是用關鍵字猜意圖。模型仍然負責理解語言,只是不再負責抄寫數字。
上線之後量到的
以下是正式站 2026-09-04 到 09-21 的遙測,排除部署後自動測試產生的輪次:
| 輪數 | 平均 token | |
|---|---|---|
| 天氣,按鈕觸發 | 55 | 0 |
| 附近搜尋與好手氣,按鈕觸發 | 13 | 0 |
| 天氣,打字問(上線後) | 191 | 1,826 |
| 天氣,上線前 30 天(當時按鈕與打字都走完整管線) | 107 | 2,945 |
打字問天氣的平均成本從 2,945 降到 1,826,中位數是 1,643。這不是對照實驗——兩段期間之間提示詞也有其他調整——但方向和上線前的估算一致,省下的主要是第二輪:191 輪裡只有 7 輪超過 3,000 tokens,大致對應模型判斷使用者另外問了建議、走了第二輪的那些。
同一段期間,天氣輪次裡約兩成來自按鈕。這個比例在改動前是量不到的:那時按鈕和打字送出的是同一句話,資料裡沒有任何欄位能分辨。
不是每顆按鈕都該跳過模型
postback 的判準不是「能不能省 token」,而是按鈕本身是否已經完整描述了要做的事。
- 已經完整描述的:「這裡的天氣」「附近停車場」「刪除這筆行程」。意圖、對象、參數都已知,程式直接執行。
- 下一步還需要理解語言的:照片的「看看這張圖」「擷取文字」本來就要模型看圖;行程的澄清選單也是——使用者從幾個候選意圖中選一個之後,仍要由模型解析原句裡的標題與日期。這類按鈕照樣可以走 postback,但它的作用是先把歧義消掉,而不是跳過模型。上線後少數幾輪花了 token 的 postback,全部是這一類。
送進模型的按鈕還有一件事要注意:那句話會被整份提示詞的規則檢視。我們有一顆「🍱 估算熱量」按鈕,上線後一直被模型拒答,原因是提示詞裡有一條「圖片上沒有的資訊一律不得推測」——而熱量本來就不會寫在圖片上。修法是把「不得捏造事實」與「不得估算」拆成兩條。寫給別的情境的規則,可能會否決一顆按鈕的整個用途。
按鈕掛出來之前,先確認這個人按得動
另一個容易漏的是掛按鈕的條件。我們曾經只判斷「這筆看起來像不像行程」就掛出「📅 其實是行程」,沒有檢查使用者有沒有綁 Google 日曆——沒綁的人按下去必定失敗。
單元測試全綠,夜間回歸也綠,因為測試帳號剛好都綁了日曆。**自動測試的帳號通常具備所有前置條件,所以這類缺陷只會出現在條件不齊的真實帳號上。**設計動作類按鈕時,要先列出「按下去要成功需要什麼」,掛按鈕的條件就要包含它們,並補一條「條件不齊時不掛」的測試。
同一個原則也適用在外部 API
附近搜尋之後,我們加了一顆「🎲 好手氣」,從這一圈的店家裡隨機挑一家。
地圖 API 按請求計費,一頁固定 20 筆,而卡片只印前 5 家——其餘那十幾家是已經付過錢、卻被丟掉的資料。所以骰子不重打 API,而是從這次搜尋的完整候選池裡抽:存在 Redis、寫入時洗好牌、抽的時候用原子遞增推一個游標,連按兩下也不會抽到同一家。0 token,0 次 API 呼叫。
重打一次反而是兩件壞事:一顆娛樂性質的按鈕變成帳單上的一筆,而且在距離排序下,重打回來的前幾家還是同一批。按鈕知道自己要什麼,資料也已經在手上,就不該再付一次錢去重新取得。
可以帶走的原則
- 按鈕送出的應該是意圖 id,不是一句要被再理解一次的話。
- message action 的文字就是使用者的意圖。共用的按鈕產生器,先問它會被哪幾種意圖呼叫。
- 測試按鈕要斷言按下去送出的內容,不能只斷言 label。
- 跳過模型之後,記憶、配額、遙測與統計口徑都要自己補回來;0 token 不代表 0 成本。
- postback 資料不放使用者的自由文字,用短 id 回查自己的暫存區。
- 數字讓程式碼寫。模型負責理解語言,不負責抄寫數字。
- 判斷可以交給模型,路徑要由程式決定。
- 一顆按鈕該不該跳過模型,看它是否已經完整描述了要做的事;還需要理解語言的,就讓 postback 先消掉歧義。
- 掛按鈕的條件,要包含按下去成功所需的全部前置條件。
- 已經付過錢的資料,按鈕應該直接用,不要再付一次。