使用者在 LINE 上用完當天的額度,收到的擋門回覆只有一句:額度已用完,或升級方案以繼續使用。

這句話沒有任何東西可以拿來比較。升級之後能多聊幾次?多十次還是多一百次?會員中心的方案卡同樣只寫「進階工作」。額度在後台是用 token 計的,而 token 是使用者最不該需要知道的單位:他們沒有辦法判斷「每天 N 萬 token」夠不夠聊一頓午餐的事。

修法聽起來很直接:把額度換成「約 N 則訊息」。難的不是除法,是一則訊息到底是多少 token,以及這個數字說錯的時候,錯在哪個方向比較能接受。

一則訊息不是一個固定的東西

先看一輪對話花在哪裡。分成固定的和變動的兩部分。

固定開銷是每一輪都付的:系統提示詞,加上這一輪載入的全部工具宣告。使用者只說一聲「你好」也一樣付。我們的工具是條件式載入的,講到行程才載入日曆工具、講到錢才載入記帳工具,所以固定開銷不是一個數字,而是看這一輪講了什麼:

這一輪 固定開銷(字元)
一般問句 2,654
講到記帳 5,157
講到行程 5,445
行程加記帳 7,948
全部工具都載入 10,112

最壞情況是一般問句的 3.8 倍。這張表是 9 到 10 月陸續量的,字元數是實測,換成 token 要乘上約 0.53 的比率,那個比率是 8 月量的,不是每次重量。

變動成本是使用者的話、模型的回覆,和對話歷史。使用者訊息的中位數只有 10 個字,AI 回覆約 400 字,所以歷史的成本有 88% 來自 AI 自己的回覆,不是使用者的話。呼叫工具的那一輪還會把歷史送兩次:選工具一次,整理結果一次。每多記住一輪歷史,一輪的成本約多 250 tokens。這也是記憶只截 AI 長回覆就能有效壓住成本的原因,見用 Redis 做對話記憶。

所以「一則」是個分布,不是一個數字。我們要的是一個對多數人、多數輪次來說誠實的估計值,而且要知道它偏向哪一邊。

用遙測校準:每則 ≈ 2,350 + 250 × 記憶輪數

每個方案的記憶輪數不同,記憶越長,歷史填滿之後每則越貴。我們用正式站的對話遙測,依「這一輪帶進提示詞的歷史輪數」分組,看每輪實際用了多少 token(各取近 30 天,9 月那次查在 9 月 26 日,11 月那次是 10 月 9 日到 11 月 7 日;都排除部署後自動測試的輪次):

歷史輪數 9 月實測平均 11 月實測平均 公式
0 2,355 2,581 2,350
1~3 3,088 3,094 3,100
4~6 3,718 沒重驗 3,850
7~10 4,624 沒重驗 4,850

公式是 2,350 + 250 × 記憶輪數,拿方案的記憶輪數代進去,就是「記憶填滿時,一則大約多少 token」。9 月校準時,它刻意訂得略高於實測平均,算出來的則數偏保守。

6 週之後我們重算了一次,因為文件裡的校準有日期,而日期會過期。結果要分開講:

  • 1~3 輪那一組吻合。兩個樣本期間完全不重疊,平均只差 6 tokens,這 30 天裡有記下版本的輪次,服務的模型也全是同一個版本。
  • 0 輪那一組不吻合,從 2,355 變成 2,581,高了一成。拆開看,沒呼叫工具的輪次平均 1,828,呼叫工具的是 3,148,差 1.7 倍,所以這個平均其實是兩種輪次的混合比例在決定。我們沒有拆 9 月的組成,無法說變化從哪來。公式實際用的是記憶填滿的那幾列,不是這一列。
  • 長記憶的兩組沒有重驗。9 月那兩組的樣本就只有幾十輪,這次的樣本更不夠,所以那兩列沿用 9 月的校準,不能說「11 月也驗過」。

最後一點值得多說:平均不是上界。公式只是略高於平均,常常講到行程和記帳的人會比「約 N 則」更早被擋。這個誤差沒有消除,只能靠下一節的取整方向把它壓在安全的一側。

取整的方向要不對稱

有了每則的 token 數,兩個地方要把 token 換成則數,而它們說錯的代價不一樣:

  • 方案上限「約幾則」,用在方案卡、說明頁和擋門回覆。說多了是多承諾。
  • 剩下「約還能用幾則」,用在進度條、「我用了多少」和 50%、90% 提醒。這個數字會被拿來決定「現在還要不要傳」,說多了的結果是使用者在第 N 則之前就被擋下。

兩者的取整規則因此不同:

20 則以下 20~99 則 100 則以上
上限 捨去到整數 四捨五入到 5 四捨五入到 10
剩下 捨去到整數 捨去到 5 捨去到 10

幾個細節:

  • 20 則以下,上限也是捨去。「約 6 則」說成 7 則就是多承諾了,這個範圍的整數每一格都看得見。
  • 20 則以上,上限才四捨五入,因為那段本來就是概數,「約 40 則」和「約 45 則」使用者感受不出差別。
  • 剩下的數字一律捨去,所有級距都一樣。寧可少講,使用者最壞只是比預期多聊幾則。
  • 上限大於 0 但連一則都不到時,回 1,不讓文案出現「約 0 則」。

倍數:不要拿 token 直接相除

把方案互相比較時有一個陷阱。較高的方案,每日 token 上限是最低方案的 7.4 倍。但較高的方案記憶也更長,每則比較貴,約 1.5 倍。換成則數,差距只剩約 5 倍。

如果在說明頁寫「高階方案的額度是 7 倍」,使用者用起來只有 5 倍,這是另一種多承諾。所以對使用者講倍數,要先換成則數再除,不能拿 token 直接相除。

這也是整件事的核心:token 是我們內部的計量單位,使用者拿來規劃的是「我能聊幾次」。兩者之間的換算本身就是產品決策,不是顯示格式。

公式有兩份,所以要有一個會紅的測試

顯示「約 N 則」的地方分屬兩個服務:

  • FastAPI:LINE 的擋門回覆、「我用了多少」、用量提醒,以及會員中心進度條下面的「約還能用幾則」。
  • Laravel:會員中心的方案卡、說明頁的方案比較表,以及 FastAPI 連不上時的本地退路。

於是同一條公式有兩份實作。最初的防護是兩邊各有一組測試,釘同一組數字,程式碼註解裡還寫著「改一邊,另一邊要一起改」。

問題在這裡:改了一邊的常數,另一邊的測試不會紅。兩組測試各自驗各自的實作,沒有任何一個測試同時看過兩份。註解是提醒,不是檢查。這是監控那篇講過的那種形狀:檢查存在,卻不可能因為真正要防的事而失敗。

後來補了一個跨讀的一致性測試,它直接讀另一個服務的原始檔,比對兩個常數、兩個門檻(20 與 100)、兩個級距(5 與 10),以及兩種捨入方式。它有兩個弱點,各補一道:

  • 只有完整 checkout 才跑得到。本機的測試容器只掛一個服務的目錄,這個測試在本機會被跳過;跳過不是失敗,沒人會發現。所以真正的把關在 CI。
  • CI 依改動的路徑決定跑哪些測試。只改一份公式時,另一邊的整組測試不會被排進去。所以改任一份公式的路徑,都被登記成「兩個服務都要跑」。

常數綁在模型版本上

2,350 與 250 是這個模型的量測結果。換一個模型,tokenizer 不同,每輪的 token 數就不同,兩個常數同時失效。

這是我們把模型版號釘死、不用浮動別名的一個理由:浮動別名等於讓模型在沒人量過的情況下自己換掉。每一輪實際服務的模型版本也記在遙測裡,上面重算的那 30 天,就是靠它確認全是同一個版本,數字才可以拿來比。換供應商時,這把尺的差異有多大,見規則要先於數字那篇的 token 一節。

減固定開銷,通常比調高額度划算

被問到「額度太緊,要不要調高」時,先看固定開銷。一輪的固定部分和使用者問什麼無關,卻每次都付。

這件事我們做過一次:文章摘要工具的宣告有 364 個字元,原本每一輪都送,30 天裡只被用了 25 次。改成只有「有網址、而且明確要摘要」才載入之後,一般問句的固定開銷從 3,018 降到 2,654 字元,少了 12%。這段期間固定開銷有減也有增(新增了提醒的參數、使用說明工具),11 月重算時,1~3 輪那一組的平均仍落在原處。

對「約 N 則」來說,這代表降低固定開銷會讓估計值變得更保守,不會變得更危險。反方向才要小心:新增一個每輪都載入的工具,會讓每則變貴,而公式不會自己知道。

組不出數字時,降級的是精確度,不是事實

文案要查資料才組得出來,查詢可能失敗。我們為「組不出則數」準備了固定的退路:一句不含則數的文案,仍然擋下、仍然說是額度用完。

另一個選項是掉進通用的「系統忙碌,請稍後再試」。那會讓使用者以為重試就會成功,一直傳,一直被擋。額度擋門是正常行為,退路只能少講數字,不能改講別的原因。

擋門回覆裡的升級選項也是同一個想法:取「啟用中、價格比目前高」的前兩個方案,依價格排序。方案上下架不必改程式,文案自己會跟著變。

可以帶走的原則

  • 使用者看得見的地方,額度講他們的單位。token 是我們的計量單位,不是他們的。
  • 一則訊息是分布不是常數。公式要說清楚它代表平均、不是上界,並且用取整把誤差壓向安全的一邊。
  • 兩個數字說錯的代價不同,取整方向就該不同:說多了會多承諾的,一律捨去;只是概數的,才四捨五入。
  • 比較方案時先換算再相除。token 的倍數不等於使用者感受到的倍數。
  • 校準要標日期,也要標沒驗到的部分。重算時分開講哪一組吻合、哪一組不吻合、哪一組根本沒樣本。
  • 同一條公式有兩份實作,就需要一個同時讀過兩份的測試。註解寫「要一起改」不算檢查,在本機被跳過的測試也不算。
  • 常數綁在模型版本上。釘死版號,並在每一輪記下實際服務的版本,換版時才知道哪些數字要重量。
  • 調高額度之前先看固定開銷:每輪都付、和問題無關的那一塊,往往有可以不付的部分。
  • 降級的是精確度,不是事實。算不出則數時,仍要說是額度用完。