有一天,有人這樣拜託我:
「請幫我粗估使用 OpenAI API 的聊天機器人每月大概要花多少錢。」
「大概」。我懂。英文有種經驗法則像是「1 個英文單字 ≒ 1.3 token」。那日文呢?
「1 個字元差不多 1 token?」
…不對。
「那每個文節 1 token?」
…也不對。
重要的是:同樣數量的日文字元,根據內容不同,token 數會有超過 2 倍的變動。所謂的「大概估算」這個概念從一開始就不太成立。
越查越深,這篇就是我掉進那個絕望深淵後的紀錄。
為了幫助理解詞元化(tokenization),在這裡我用「兌換所(外幣兌換)」的比喻來說明。
大型語言模型(LLM)無法直接理解人類語言。模型內部會把所有文字換成稱為 token(詞元)的「通貨」來處理。而 OpenAI API 的計費,就是根據這些兌換後的 token 數量來計算。
關鍵問題在於,這個「兌換匯率」會因語言而大不相同。英文通常享有「優惠匯率」,會被轉成較少數量的 token;而日文則常被視為「較貴的匯率」,會消耗更多 token。
OpenAI API 以 token 為單位計費。相同語意的句子,用日文書寫時可能比英文消耗 3–4 倍的 token。在做成本估算時若忽略語言差異,預算很可能會大幅偏差。
那麼這個「兌換匯率」是如何決定的呢?OpenAI 的 tokenizer 採用了 BPE(Byte Pair Encoding)演算法。
3.1 BPE 的運作 —「記住常用的組合」
用兌換所的比喻來說,BPE 就像是「針對經常出現的紙鈔組合,準備專屬的通貨」的機制。
舉例來說,在英文文本中 th 這樣的位元組序列會大量出現。BPE 會把它合併成一個 token。再如 the 也非常常見,所以整個 " the"(包含空格)可能被收錄為一個 token。
英文在 token 化時,會相對有效率地把「單字」或「單字的一部分」合併成 token,這就是所謂的「優惠匯率」來源。
3.2 結構上日文為何不利
而日文的情況則是絕望的關鍵所在。
理由 1:UTF-8 編碼下位元組數較多
BPE 並不是直接處理字元,而是基於 UTF-8 的位元組序列做合併。
a → 0x61あ → 0xE3 0x81 0x82猫 → 0xE7 0x8C 0xAB🔥 → 0xF0 0x9F 0x94 0xA5英文的 cat 是 3 bytes,日文的 猫 也是 3 bytes。但 cat 在 BPE 的訓練資料中大量出現,能被合併成 1 個 token;猫 的出現頻率相對較低,常會被拆成多個位元組的 token(2–3 token)。
所以即使是相同的位元組數,因為訓練資料中出現頻率不同,token 數會完全不同。日文在訓練資料中的出現頻率遠低於英文,導致位元組合併較少、token 效率差。
理由 2:Unicode CJK 統一漢字過多
Unicode(截至 15.1)包含超過 97,000 個 CJK 統一漢字。cl100k_base 的詞彙表大約有 100,000 個 token,如果要把所有 CJK 漢字都納入,詞彙表幾乎會被耗盡。結果是很多漢字會以位元組形式被拆開成多個 token。
理由 3:日文沒有以空格分詞的概念
英文句子單字間有空格,使得 BPE 的前處理能容易形成像 " the" 這樣的單字塊。日文如「私は猫が好きです」是連續的字串,BPE 的基於正規表達式的前處理,在日文上無法像英文那樣有效地切分與合併。
接下來實際使用 tiktoken 做驗證。把日文與英文拿到兌換所去換錢,親自感受匯率差異。
4.1 環境建置
pip install tiktoken
4.2 驗證程式碼(含中文化的輸出說明)
import tiktoken
def compare_tokens(text_ja: str, text_en: str, encoding_name: str = "o200k_base"):
"""比較日文與英文文字的 token 數量"""
enc = tiktoken.get_encoding(encoding_name)
tokens_ja = enc.encode(text_ja)
tokens_en = enc.encode(text_en)
print(f"=== {encoding_name} ===")
print(f"日文: 「{text_ja}」")
print(f" 字元數: {len(text_ja)}, token 數: {len(tokens_ja)}, 字元/Token 比: {len(text_ja)/len(tokens_ja):.2f}")
print(f"英文: \"{text_en}\"")
print(f" 字元數: {len(text_en)}, token 數: {len(tokens_en)}, 字元/Token 比: {len(text_en)/len(tokens_en):.2f}")
print(f" Token 比(日/英): {len(tokens_ja)/len(tokens_en):.2f} 倍")
print()
return tokens_ja, tokens_en
# 測試案例
pairs = [
("こんにちは", "Hello"),
("私は猫が好きです", "I like cats"),
("本日は晴天なり", "It is sunny today"),
("機械学習モデルの性能を最適化する方法について解説します",
"This article explains how to optimize machine learning model performance"),
("お誕生日おめでとう", "Happy Birthday"),
]
for ja, en in pairs:
compare_tokens(ja, en)
4.3 執行結果 — 絕望的表格
(原文中的表格意圖示例,下方用文字重現)
日文 / 英文 範例 | JP 文字數 | JP token | EN 文字數 | EN token | JP/EN 比
こんにちは / Hello | 5 | 3 | 5 | 1 | 3.0 倍
私は猫が好きです / I like cats | 8 | 6 | 11 | 3 | 2.0 倍
本日は晴天なり / It is sunny today | 7 | 5 | 11 | 4 | 1.25 倍
機械学習モデルの... / This article... | 26 | 15 | 57 | 11 | 1.36 倍
お誕生日おめでとう / Happy Birthday | 9 | 8 | 11 | 2 | 4.0 倍
「お誕生日おめでとう」是 8 token,而 "Happy Birthday" 是 2 token。4 倍。
為了傳達相同的祝福,日文竟然要付出英文 4 倍的 API 費用。現在似乎是個「用感情收費」的時代。
這時你可能會問:「那日文到底是一個字會變幾個 token?」來看細節。
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
text = "お誕生日おめでとう"
tokens = enc.encode(text)
print(f"文本: {text}")
print(f"token 數: {len(tokens)}")
print(f"token ID: {tokens}")
print()
for token_id in tokens:
token_bytes = enc.decode_single_token_bytes(token_id)
try:
decoded = token_bytes.decode("utf-8")
except UnicodeDecodeError:
decoded = repr(token_bytes)
print(f" ID:{token_id:>6} → {repr(token_bytes):>20} → {decoded}")
在 o200k_base(GPT-4o)下的結果可能像:
文本: お誕生日おめでとう
token 數: 8
ID: 8930 → b'\xe3\x81\x8a' → お
ID: 9697 → b'\xe8\xaa' → (不完整)
ID: 243 → b'\x95' → (不完整)
ID:128225 → b'\xe7\x94\x9f\xe6\x97\xa5' → 生日
ID: 8930 → b'\xe3\x81\x8a' → お
ID: 17693 → b'\xe3\x82\x81' → め
ID: 43834 → b'\xe3\x81\xa7\xe3\x81\xa8' → でと
ID: 12735 → b'\xe3\x81\x86' → う
(;゚д゚) 呆住了。
「誕」字被撕成了兩個 token。b'\xe8\xaa' 與 b'\x95' 分別是 UTF-8 的位元組被斷在中間(誕 的 UTF-8 是 E8 AA 95)。而「生日」兩個字卻被合成為 1 個 token。
也就是說:
總結:即便在同一句日文裡,每個字的 token 效率差異極大。平假名通常效率較好(1 字 ≒ 1 token),漢字則會根據出現頻率在 1–3 token 間波動。這種不規則性是導致「大概估算」會失準的根本原因。
不同模型使用的編碼(也就是「兌換所」)不同。
| 編碼 | 詞彙表大小 | 對應模型 |
|---|---|---|
| r50k_base | 約 50,000 | GPT-3 時代模型 |
| cl100k_base | 約 100,000 | GPT-4、GPT-3.5-turbo |
| o200k_base | 約 200,000 | GPT-4o、GPT-4o-mini |
詞彙表越大,代表能把更多的位元組組合收錄為已知 token,也就是兌換所的貨品越齊全。
不同編碼下的日文 token 效率示例:
import tiktoken
text = "お誕生日おめでとう"
for enc_name in ["r50k_base", "cl100k_base", "o200k_base"]:
enc = tiktoken.get_encoding(enc_name)
tokens = enc.encode(text)
print(f"{enc_name:>15}: {len(tokens)} tokens")
結果可能是:
r50k_base: 14
cl100k_base: 9
o200k_base: 8
同一句話在不同編碼下會接近翻倍的差異。因此在做 API 成本估算時,必須記得「使用哪個模型」會改變 token 數。由於成本 = token 單價 × token 數,模型選擇對成本有雙重影響(單價與 token 效率都不同)。
到目前為止可以確定:用「字數 × 固定係數」去估算日文 token 是從原理上就不精確的。但在商務情境中常常還是需要一個估算數字。以下提供三個層級的現實做法。
7.1 等級 1:超粗略(用於簡報 / 初期提案)
精度低,但能抓到數量級即可。
一般日文可用「字數 ≒ token 數」作為最粗略的近似。以上係數基於 o200k_base(GPT-4o 系列)。若使用 cl100k_base(GPT-4),通常 token 數會再高出 1.1〜1.3 倍。
7.2 等級 2:抽樣測量(用於正式估算書 / 預算申請)
準備與實際使用情境接近的樣本文本,使用 tiktoken 做計數。
範例估算程式(已中文化):
import tiktoken
def estimate_monthly_cost(
sample_texts: list[str],
avg_requests_per_day: int,
model: str = "gpt-4o",
input_price_per_1m: float = 2.50, # $/1M token(輸入)
output_price_per_1m: float = 10.00, # $/1M token(輸出)
avg_output_ratio: float = 1.5, # 輸出/輸入 token 比例
):
"""從樣本文本估算 API 月額成本"""
enc = tiktoken.encoding_for_model(model)
token_counts = [len(enc.encode(text)) for text in sample_texts]
avg_input_tokens = sum(token_counts) / len(token_counts)
avg_output_tokens = avg_input_tokens * avg_output_ratio
monthly_requests = avg_requests_per_day * 30
total_input_tokens = avg_input_tokens * monthly_requests
total_output_tokens = avg_output_tokens * monthly_requests
input_cost = (total_input_tokens / 1_000_000) * input_price_per_1m
output_cost = (total_output_tokens / 1_000_000) * output_price_per_1m
print("=== 月額成本估算 ===")
print(f"模型: {model}")
print(f"樣本平均輸入 token: {avg_input_tokens:.0f}")
print(f"估計平均輸出 token: {avg_output_tokens:.0f}")
print(f"每月請求數: {monthly_requests:,}")
print(f"每月輸入 token: {total_input_tokens:,.0f}")
print(f"每月輸出 token: {total_output_tokens:,.0f}")
print(f"輸入成本: ${input_cost:.2f}")
print(f"輸出成本: ${output_cost:.2f}")
print(f"合計: ${input_cost + output_cost:.2f} / 月")
return input_cost + output_cost
# 使用範例:客服 Bot 的估算
sample_queries = [
"商品の返品方法を教えてください。注文番号は12345です。",
"先月購入したノートPCのバッテリーが膨張しています。交換対応は可能でしょうか?",
"配送状況を確認したいのですが、追跡番号がわかりません。名前と住所で検索できますか?",
"クレジットカードの請求額が注文金額と異なるのですが、内訳を確認できますか?",
"解約手続きをお願いします。今月末で契約終了にしてください。",
]
estimate_monthly_cost(sample_queries, avg_requests_per_day=500)
7.3 等級 3:正式營運日誌分析(用於運營優化)
上線後應累積 API 日誌,從中分析實際的 token 消耗並修正估算。OpenAI 的 API 回應會包含 usage 欄位,例如:
# API 回應中的 usage 範例
{
"usage": {
"prompt_tokens": 156,
"completion_tokens": 342,
"total_tokens": 498
}
}
把這些 usage 數據存下來並統計平均,會得到最準確的估算方式。
| 症狀 | 原因 | 對策 |
|---|---|---|
| 安裝 tiktoken 時出錯 | 需要 Rust toolchain | 通常 pip install tiktoken 可解決;若仍需 Rust,可安裝 rustup |
| 使用 encoding_for_model("gpt-4o") 時發生 KeyError | tiktoken 版本過舊 | pip install --upgrade tiktoken 更新至最新版 |
| 把日文 token decode 後看到亂碼 | token 在 UTF-8 字元中間被分割 | 用 decode_single_token_bytes() 取得各 token 的位元組後,先連接再 decode |
| 估算與實際 API 計費差距很大 | 忽略 system prompt 或會話歷史的 token | 計算時要把送到 API 的所有訊息(system、user、assistant)全部加總 |
| 同一文字在不同呼叫中 token 數會變 | 通常不會變;若變動可能是有不可見字元混入 | 在計數前對文字做正規化(例如 NFKC) |
| 最常見的估算錯誤 | 只計算使用者輸入 token,忘記 system prompt 和歷史 | 實際計費是 system prompt + 全部會話歷史 + 本次輸入 + 輸出 |
下面的腳本可以直接複製貼上,檢測任意文本的 token 效率。
import tiktoken
import sys
def diagnose_tokens(text: str):
"""診斷文本的 token 效率"""
encodings = {
"o200k_base (GPT-4o)": "o200k_base",
"cl100k_base (GPT-4)": "cl100k_base",
}
print(f"輸入文本: {text[:50]}{'...' if len(text) > 50 else ''}")
print(f"字元數: {len(text)}")
print(f"UTF-8 位元組數: {len(text.encode('utf-8'))}")
print()
for label, enc_name in encodings.items():
enc = tiktoken.get_encoding(enc_name)
tokens = enc.encode(text)
ratio = len(text) / len(tokens) if tokens else 0
byte_ratio = len(text.encode('utf-8')) / len(tokens) if tokens else 0
print(f"--- {label} ---")
print(f" token 數: {len(tokens)}")
print(f" 字元/Token 比: {ratio:.2f} (越接近 1.0 越有效)")
print(f" 位元組/Token 比: {byte_ratio:.2f}")
# token 效率評估
if ratio >= 1.0:
print(" 評價: ✅ 良好(1 字元 ≦ 1 token)")
elif ratio >= 0.5:
print(" 評價: ⚠️ 普通(接近日文平均效率)")
else:
print(" 評價: ❌ 非效率(可能漢字或特殊字元較多)")
print()
# 使用範例
if __name__ == "__main__":
test_texts = [
"こんにちは、今日はいい天気ですね。",
"機械学習における勾配降下法の最適化手法について概説する。",
"Hello, it's a nice day today.",
"RTX 5090でCUDA環境を構築してローカルLLMを動かす手順を解説します。",
]
for text in test_texts:
diagnose_tokens(text)
print("=" * 60)
API 成本計算公式很簡單,但變數很多,先整理一下:
成本 = (輸入 token 數 / 1,000,000) × P_in + (輸出 token 數 / 1,000,000) × P_out
(下表為 2026 年 3 月的主要模型定價範例)
| 模型 | 編碼 | 輸入 ($/1M token) | 輸出 ($/1M token) |
|---|---|---|---|
| GPT-4o | o200k_base | $2.50 | $10.00 |
| GPT-4o-mini | o200k_base | $0.15 | $0.60 |
| GPT-4 | cl100k_base | $30.00 | $60.00 |
GPT-4o 不僅 token 單價大幅下降(約 GPT-4 的 1/12),o200k_base 的編碼也改善了日文的 token 效率。對於日文使用者來說,「單價便宜 × token 效率提升」是雙重利好。
10.1 日文 1,000 字的大致成本估算(近似值)
| 模型 | 輸入 1,000 字 | 輸出 1,000 字 |
|---|---|---|
| GPT-4o | 約 $0.0025 | 約 $0.010 |
| GPT-4o-mini | 約 $0.00015 | 約 $0.0006 |
| GPT-4 | 約 $0.039 | 約 $0.078 |
註:此處用「日文 1,000 字 ≒ 1,000 token」做概算(以 o200k_base 為基準;cl100k_base 可能高出 1.1–1.3 倍)。
本文觸及了日文詞元化的「黑盒」問題。建議的後續學習步驟如下。
從一句「幫我大概算一下」出發,調查結果遠比預期複雜得多。
主要結論:
作為日文使用者,看到這種 token 效率差距多少會有些無奈:英文 "Happy Birthday" 只要 2 token,而「お誕生日おめでとう」竟然是 8 token,同樣是祝福,卻被多收 4 倍的費用。
不過 GPT-4o 世代的編碼改進確實在改善這種不平衡。o200k_base 的詞彙表擴大是對非英文語系的一大步。短期內最務實的做法仍然是:用 tiktoken 實測你的樣本,並以實測數據作為估算依據。
原文出處:https://qiita.com/GeneLab_999/items/04ff280edc3178b60104