波斯语 token 税:非英语 LLM 应用的成本

1,012 个平行句子:波斯语在 GPT-4o 的分词器上需要英语 1.53 倍的 token,在 GPT-4 上是 3.26 倍。方法、结果,以及该测什么。

同样的内容,波斯语(Farsi)文本比英语更费 token。这笔额外开销就是波斯语的 token 税;它有多大,更多取决于分词器,而不是文字本身。我用 FLORES-200 基准里同样的 1,012 个句子,分别数了英语版和波斯语版的 token。在 o200k_base(OpenAI 的 tiktoken 库把 GPT-4o 映射到这个编码)上,波斯语需要英语 1.53 倍的 token;在同一个库映射给 GPT-4 的 cl100k_base 上,则是 3.26 倍。这两个数是文本生成模型所用的四个分词器里的两端;嵌入模型(embedding model)bge-m3 的分词器只有 1.10 倍。Claude 和 Gemini 不在这次测量里:Anthropic 不公开 Claude 的分词器,Google 的 SDK 为 Gemini 提供的本地分词器我也没有测。这两家都可以通过 API 为自家模型数 token。

这些数字是一份有日期的快照:测量于 2026 年 9 月 17 日,分词器版本全部固定。

波斯语为什么更费 token:字节、合并与训练数据

语言模型 API 按 token 计费。2023 年,Ahia 等人找到的证据显示,“大量受支持语言的使用者付出了过高的费用,同时得到的结果反而更差”。Petrov 等人指出,同一段文本翻译成不同语言之后,分词长度的差距“在某些情况下高达 15 倍”。2025 年一项覆盖两百多种语言的研究报告称,非拉丁文字和形态复杂的语言“往往有高出 3 到 5 倍的 RTC 比值”,其中 RTC 是作者定义的“相对分词成本”(Relative Tokenization Cost)。

这就是 token 税:意思一样,账单更大。

每个字节级分词器都从字节开始

字节级 BPE(byte-pair encoding,字节对编码)分词器,也就是 GPT-4o、Qwen3 和 DeepSeek-V3 背后的那一类,从文本的 UTF-8 编码字节出发,把高频的字节序列合并成 token。一个不带变音符号的拉丁字母是一个字节;约鲁巴语那些带声调符号和下点的字母要两个或更多。一个波斯语字母是两个字节,而波斯语词内使用的零宽不连字是三个。在 FLORES 上,波斯语文件的 UTF-8 字节数是英语文件的 1.69 倍,字符数却只有 0.94 倍。波斯语并不是字符更多,而是编码更重。

接下来会发生什么,取决于分词器从训练数据里学到的合并规则。从 cl100k_base 换到 o200k_base,波斯语的 token 数减少了 53.5%,英语只减少了 1.1%。新词表改变的是波斯语,不是英语。

用拉丁字母书写的约鲁巴语,反而付得更多

如果决定这笔税的是文字,那么用拉丁字母书写的语言几乎不必缴税。Roy、Roy 和 Patel 的一篇研讨会论文(已被 IJCAI 2026 的一个研讨会接收,自 2026 年 8 月起可在 arXiv 上读到)发现,在他们的审计中,“约鲁巴语尽管使用拉丁字母,却有着最高的 GPT-4o 分词溢价”,为 2.37 倍。作者把原因指向变音符号和词表覆盖。在那篇论文里,约鲁巴语只是探索性的对照:它的译文由机器翻译生成、再经人工检查,而经过验证的案例是孟加拉语和印地语。

我这次测量的结果指向同一个方向。五个分词器里,约鲁巴语有四个比波斯语更贵,阿拉伯语也有四个比波斯语更便宜。阿拉伯语的那个例外是 bge-m3,一个建立在 XLM-RoBERTa 及其词表之上的嵌入模型。XLM-R 的训练数据取自 CC-100 中的一百种语言:波斯语有 13,259M 个 token,阿拉伯语只有 2,869M 个,而约鲁巴语不在其列。这与“训练数据的份额比文字更重要”这一说法一致,但并不能证明它。

结果:波斯语、阿拉伯语、约鲁巴语与英语的对比

分组条形图:同样的 FLORES-200 句子在五个分词器上相对英语所需的 token 数。波斯语从 bge-m3 上的 1.10 倍到 tiktoken 映射给 GPT-4 的那个分词器上的 3.26 倍不等,而约鲁巴语在其中四个上比波斯语更费 token。

表里每一格,都是该语言的 token 总数除以同一分词器上英语的 token 总数:

分词器

波斯语(相对英语)

阿拉伯语(相对英语)

约鲁巴语(相对英语)

cl100k_base

3.26 倍

3.03 倍

2.95 倍

Qwen3

2.58 倍

1.63 倍

2.69 倍

DeepSeek-V3

1.79 倍

1.62 倍

2.83 倍

o200k_base

1.53 倍

1.38 倍

2.17 倍

bge-m3

1.10 倍

1.17 倍

2.27 倍

最大的变量是分词器本身。波斯语在最差的分词器上的比值(3.26 倍),大约是它在最好的分词器上那个比值(1.10 倍)的三倍;即便只看文本生成用的那四个,最高的比值也是最低的大约两倍(3.26 倍对 1.53 倍)。Qwen3 对阿拉伯语的处理远好于波斯语,1.63 倍对 2.58 倍,所以用一个“阿拉伯字母”的数字同时代表这两种语言是行不通的。cl100k_base 的结果与此前已发表的统计吻合:Petrov 等人把 FLORES-200 的 dev 和 devtest 两部分合在一起统计,西波斯语是英语的 3.28 倍,而我只用 devtest 得到 3.26 倍。

这些比值背后的总数,以及每种语言的文件在被任何分词器碰过之前的大小:

统计

英语

波斯语

阿拉伯语

约鲁巴语

字符数

131,966

123,830

116,320

126,848

字节数(UTF-8)

132,096

223,504

211,276

168,857

token 数,cl100k_base

27,182

88,513

82,288

80,247

token 数,Qwen3

27,621

71,183

44,991

74,226

token 数,DeepSeek-V3

27,041

48,342

43,713

76,526

token 数,o200k_base

26,873

41,116

37,117

58,238

token 数,bge-m3

30,661

33,631

35,979

69,672

换成你自己的语料,数字会不一样

Roy 等人的审计是在 120 个从英语翻译而来的 Python 调试样本上测 o200k_base 的,并按样本取平均。它报告阿拉伯语为 1.44 倍、约鲁巴语为 2.37 倍,两者都来自机器翻译。而在由人翻译的 FLORES 散文上,同一个 o200k_base 给出的阿拉伯语是 1.38 倍、约鲁巴语是 2.17 倍。分词器相同,文本和翻译方式不同,结果就不同。做预算之前,先数一数你自己的提示词、检索到的片段和回答。

我是怎么数波斯语 token 的

一份真正平行的语料

NLLB 团队把 FLORES-200 描述为“一个由人工翻译的基准”:每种语言里都是同样的句子,顺序也相同。我用的是它的 devtest 划分,许可证为 CC BY-SA 4.0,从官方存档下载并核对了 SHA-256 哈希:1,012 个来自 Wikinews、Wikivoyage 和 Wikibooks 的句子,语言为英语、西波斯语、现代标准阿拉伯语和约鲁巴语。

平行比规模更重要。经过改写的译文,比如一份双语简历,会增补、删减和重写内容,于是它测出的比值里,既有分词器的影响,也有这些编辑的影响。FLORES 也有自己的局限:它是新闻、旅行指南和教科书式的散文,不是聊天、代码或合同。

公开的分词器、固定版本,以及 Claude 和 Gemini 为什么缺席

五个分词器,全部无需账号即可下载:

  • tiktoken 0.14.0:cl100k_base 和 o200k_base。在 tiktoken 的映射里,GPT-4 和 GPT-3.5 Turbo 属于前者,GPT-4o、o1、o3、GPT-4.1 和 GPT-5 属于后者。

  • Qwen3(Qwen/Qwen3-8B)和 DeepSeek-V3(deepseek-ai/DeepSeek-V3),两者都是字节级 BPE,用 Hugging Face 的 tokenizers 0.23.2 在固定的 Hub revision 上加载。

  • bge-m3(BAAI/bge-m3),一个嵌入模型,它的分词器是使用 XLM-R 词表的 SentencePiece Unigram 模型,而不是字节级 BPE。Unigram 是另一套算法:它从一份固定的词表中挑出文本最可能的那种切分。它关系到检索,而不是生成。

每个句子单独编码,不加特殊 token,再把计数相加。逐句的中位数和总数说明的是同一件事。核心代码只有几行:

# after: tar -xzf flores200_dataset.tar.gz
import tiktoken
from huggingface_hub import hf_hub_download
from tokenizers import Tokenizer


def sentences(lang):
    with open(f"flores200_dataset/devtest/{lang}.devtest", encoding="utf-8") as f:
        return f.read().rstrip("\n").split("\n")


english, persian = sentences("eng_Latn"), sentences("pes_Arab")

o200k = tiktoken.get_encoding("o200k_base")
qwen3 = Tokenizer.from_file(
    hf_hub_download(
        "Qwen/Qwen3-8B",
        "tokenizer.json",
        revision="b968826d9c46dd6066d109eabc6255188de91218",
    )
)


def o200k_tokens(texts):
    return sum(len(o200k.encode_ordinary(t)) for t in texts)


def qwen3_tokens(texts):
    return sum(len(qwen3.encode(t, add_special_tokens=False).ids) for t in texts)


print(round(o200k_tokens(persian) / o200k_tokens(english), 2))
print(round(qwen3_tokens(persian) / qwen3_tokens(english), 2))

这份测量记录足以把这次运行原样重跑一遍:

corpus: https://dl.fbaipublicfiles.com/nllb/flores200_dataset.tar.gz
sha256: b8b0b76783024b85797e5cc75064eb83fc5288b41e9654dabc7be6ae944011f6
split: devtest
measured: 2026-09-17
libraries:
  tiktoken: 0.14.0
  tokenizers: 0.23.2
  huggingface_hub: 1.31.0
encodings: [cl100k_base, o200k_base]
hub_revisions:
  Qwen/Qwen3-8B: b968826d9c46dd6066d109eabc6255188de91218
  deepseek-ai/DeepSeek-V3: e815299b0bcbac849fa540c768ef21845365c9eb
  BAAI/bge-m3: 5617a9f61b028005a4858fdac845db406aefb181
method: each sentence encoded on its own, no special tokens, counts summed per language

Claude 和 Gemini 是有意缺席的。Anthropic 不公开 Claude 的分词器,所以我给出的任何 Claude 比值,都只是一个带小数点的猜测。Google 的 Gen AI Python SDK 附带一个实验性的、只处理文本的本地分词器,它把部分 Gemini 模型映射到开源的 Gemma 分词器;这个我没有测。这两家都可以替你数 token。Anthropic 的 count_tokens 端点免费但有速率限制,其文档把结果称为“一个估计值”;Gemini 的 count_tokens 数的是输入。Claude 4.7 及之后的模型使用新版分词器,同一段输入文本产生的 token “比早期模型多出大约 30%”。token 数属于一个模型,而不是一个服务商。

两条成立的边界,和一条不成立的边界

字节级 BPE 分词器在合并之前会先把文本切成预分词(pre-token),这里的四个都是如此;在这类分词器上,token 数被两条边界框住。token 数不可能多于分词器规范化之后文本的 UTF-8 字节数,因为每个 token 至少覆盖一个字节。它也不可能少于该分词器自己的预分词数,也就是它的切分规则把文本切出来的那些块,因为合并从不跨越预分词的边界。这两条边界,我在四个字节级 BPE 分词器和全部四种语言上逐句检查过:16,192 次检查,0 次违反。

本站的分词器试验场会在浏览器里为你输入的任何文本算出这两个数:cl100k_base 的预分词切分,以及文本的 UTF-8 字节数。在 cl100k_base 下,真实的 token 数落在两者之间,每个面板下方的说明写的正是这句话。

下界只属于某一个分词器。cl100k_base 的切分并不是 o200k_base 的下界:在 28 个英语句子上,o200k_base 需要的 token 比 cl100k_base 切出的预分词还少。这两条边界,对 bge-m3 的 Unigram 模型都不适用。

token 税如何影响价格、延迟、上下文窗口和速率限制

价格

API 服务商按模型处理和生成的 token 收费。在每 token 单价相同的前提下,同样的内容用波斯语写,在 o200k_base 上大约是英语的一倍半,在 cl100k_base 上是英语的三倍多。当提示词和回答都是波斯语时,这笔额外成本会同时落在账单的两侧。

延迟

决定节奏的是输出 token。OpenAI 的延迟优化指南指出,“把输出 token 削减 50%,延迟可能也随之减少约 50%”。压缩提示词的作用要小得多:“把提示词削减 50%,可能只带来 1–5% 的延迟改善”。一个比英语版本需要更多 token 的波斯语回答,流式输出所需的时间也更长。这是把服务商的指引套用到上面那些比值上得出的,不是我自己做的延迟测量。

上下文窗口

固定大小的 token 窗口装得下的波斯语更少。除以这些比值可知,就这份语料而言,同一个窗口在 cl100k_base 上只装得下相当于英语 31% 的波斯语内容,在 Qwen3 上是 39%,在 DeepSeek-V3 上是 56%,在 o200k_base 上是 65%,在 bge-m3 上是 91%。Roy 等人的审计对孟加拉语给出了同样的结论:它在 GPT-4o 上 1.56 倍的溢价,把“名义上 128k token 的上下文窗口,压缩成实际相当于 82k token 英语内容的容量”。在一个 RAG 提示词里,这意味着留给检索片段的空间更少。

速率限制

速率限制同样按 token 计。Claude 的 API 限制“以每分钟请求数(RPM)、每分钟输入 token 数(ITPM)和每分钟输出 token 数(OTPM)来衡量”。在每分钟 token 的上限之下,每个请求消耗更多 token 的工作负载,用更少的请求就会撞到上限;波斯语在这里测的每一个分词器上都是这样。在大多数 Claude 模型上,从缓存读取的 token 不计入输入上限。

省 token 的规范化,和破坏召回的规范化

波斯语书写中有两个特征,看上去像是省 token 的便宜办法。两个都是陷阱。

ZWNJ(نیم‌فاصله)是字符,不是空格

U+200C ZERO WIDTH NON-JOINER(零宽不连字)在 Unicode 里的一般类别是 Cf,属于格式控制字符,不是空白字符。它让两侧的字母不再连写,所以 می‌شود 读起来仍是一个词,只是中间有一道看得见的断口。FLORES 的波斯语文件用了它 1,739 次,分布在 1,012 个句子中的 735 个里。

把每一个 ZWNJ 都换成空格,波斯语的 token 数在 cl100k_base 上减少 1.59%,在 o200k_base 上减少 1.82%,在 Qwen3 上减少 1.57%,在 DeepSeek-V3 上减少 8.08%,在 bge-m3 上则是 0.00%。bge-m3 之所以纹丝不动,是因为它的规范化器本来就把 ZWNJ 映射成空格:全部 735 个被改动的句子,token id 完全相同。

这点节省在三个分词器上很小,在 DeepSeek-V3 上大一些,而且它改变了文本:一个词变成两个,你存下来、显示出去或原样引用的东西,已经不是作者写的那个。存储的文本里要保留 ZWNJ,至于匹配器把什么当作相同,则另行决定。本站正是这么做的:只有比较时使用的那层折叠,才把 ZWNJ 当作空格。

阿拉伯语的 yeh、kaf 与波斯语的 yeh、keheh

ARABIC LETTER YEH(U+064A)和 ARABIC LETTER FARSI YEH(U+06CC)是两个不同的字符,ARABIC LETTER KAF(U+0643)和 ARABIC LETTER KEHEH(U+06A9)也是。在词首或词中它们看起来一样,在词尾则不同。字符串比较看到的则永远是两个不同的字符。连 FLORES 都混用它们:它的波斯语文件里有 11 个阿拉伯语 yeh 和 22 个阿拉伯语 kaf,同时有 9,570 个波斯语 yeh 和 2,957 个 keheh。

把波斯语文件里所有波斯语 yeh 和 keheh 都改写成阿拉伯语 yeh 和 kaf 之后,token 数在 cl100k_base 上增加 2.20%,在 o200k_base 上增加 9.49%,在 Qwen3 上减少 7.48%,在 DeepSeek-V3 上增加 10.39%,在 bge-m3 上增加 11.95%。一种在某个分词器上省 token 的改写,在另外四个上都要多花 token。

真正的问题是召回(recall)。如果存储的文本和查询用的是不同的码位,精确匹配和词法匹配会悄无声息地失败。本站在写入时会折叠波斯语文本:阿拉伯语 yeh 和 kaf 变成对应的波斯语形式,ZWNJ 保留。搜索查询也按同一种方式折叠。没有这一步,一个用阿拉伯语 yeh 输入的查询,找的是索引里根本不存在的字符,于是什么也返回不了。

更一般的规则要窄一些:只折叠波斯语文本,因为在阿拉伯语里,阿拉伯语 yeh 和 kaf 才是正确的字母。本站自己的检索器还没有做这个区分:它把波斯语折叠用在所有语言的内容和问题上,阿拉伯语也不例外。波斯语 RAG 的混合检索一文说明了这个缺口,也讲了检索这一侧的其余部分。

波斯语数字

把 ASCII 数字写成波斯语数字,token 数增加 0.48% 到 1.47%。这不足以成为改变读者所见内容的理由。

怎样降低波斯语的 token 成本:要测试,不要假设

下面每一条,都要先在你自己的流量上测过,再去依赖它。

  • **用你自己的文本选模型。**在同样的句子上,波斯语与英语的 token 比值从 1.10 倍到 3.26 倍不等。

  • **用服务商自己的端点数 token。**对 Claude 和 Gemini 来说,这里测出的比值什么也说明不了。

  • **换模型就重新数一遍。**一个新版分词器,可能改变你定下的每一份预算。

  • **分块大小和上下文预算按 token 定,并且分语言定。**一个按英语调好的字符上限,在波斯语里会装进更多 token:在 bge-m3 上大约多六分之一,在 cl100k_base 上大约是三倍半。

  • **限制输出长度。**输出 token 同时决定延迟和成本。Anthropic 建议限定句数或段数,而不是字数。

  • **缓存重复的上下文,然后测命中率。**任何节省都取决于你的前缀到底重复了多少次。

  • **把英语系统提示词当成一个假设。**它占用的 token 更少,但它对波斯语回答有什么影响,我没有测量数据。

  • **不要为了省 token 去掉 ZWNJ 或改换字母形式。**省下的 token 很少,而且并不稳定;对文本和匹配的损害却不小,也很确定。

怎样为波斯语的 token 做预算

“每个 token 多少字符”是一个随分词器而变的猜测

波斯语从 cl100k_base 上的每 token 1.40 个字符,到 bge-m3 上的 3.68 不等,o200k_base 上是 3.01。英语则在 4.30 到 4.91 之间。Google 的 Gemini 文档称“一个 token 大约相当于 4 个字符”,并在旁边配上一个按英语单词数的换算:这是一条照着英语的形状造出来的规则,不是关于波斯语的结论。

“每两个波斯语字符预留一个 token”听上去很宽裕。本站以前就是这么预留的,所以我在这些句子上测了它:在 cl100k_base 上,它对全部 1,012 个波斯语句子都低于真实的 token 数,在 Qwen3 上是 976 个。在 o200k_base 和 bge-m3 上它站得住,在 DeepSeek-V3 上有 6 个句子不够。一条在某个分词器上安全的规则,换一个可能就变得乐观,所以本站不再使用这样的规则。

一个硬上限:字节数

对字节级 BPE 来说,分词器规范化之后文本的字节数是一个硬上限,但相当宽松。把预分词数记作 0、字节数记作 1,英语落在 0.015 到 0.017 之间:几乎每个英语预分词,大致也就是一个词,都是单独一个 token。波斯语在 o200k_base 上落在 0.07,在 cl100k_base 上落在 0.31:被切碎了,但离碎成字节还差得远。因此,按字节预留会大幅超额。对一个谁都不许越过的上限来说,这个取舍是对的;对一次预测来说,它是错的。对 Claude 和 Gemini,调用前用它们的端点去数,调用后记录它们报告的用量。

调用前预留,调用后记账

本站的助手把这两件事分开。在调用语言模型之前,它会把这次请求的估算量,也就是提示词 token 的一个上界加上完整的输出上限,加到本月已记录的支出上,再拿总数与月度上限比较。如果支出计数器读不出来,它就按失败关闭(fail closed)处理。到达上限时模型不会被调用,读者看到的是一条提示,而不是一个错误。调用之后,它记录服务商报告的用量。估算只决定一次请求能不能开始;累计总额来自服务商报告的 token 数。

这两个环节各有各的局限,方向正好相反。调用前的估算,把提示词按它的 UTF-8 字节长度来计:也就是上一节那个上限,而不是一次预测。这个助手能指向的每一份词表都是字节级 BPE,所以无论它在跟哪个模型说话、访客用哪种语言书写,这个上限都成立。对波斯语来说,这意味着预留的是提示词真实成本的好几倍,只在这一次请求期间占用,也从不作为支出记录下来。它以前用的是上面测过的“每两个字符一个 token”那条固定规则来数提示词,而在 cl100k_base 上,那条规则对全部 1,012 个波斯语句子都低于真实的 token 数。预留得太小,意味着支出上限在唯一不该出错的方向上出了错,所以这次测量就是它被改掉的原因:估算函数的文档字符串引用的正是这次 1,012 个句子的运行。不论是哪一种方式,月度总额都由记录下来的用量构成,而不是由估算构成。

记账这一侧要应对一种沉默。OpenAI 和 Anthropic 都在生成的文本之后才报告输出 token 数,而 OpenAI 的流式接口参考提醒说,如果数据流被中断或取消,“你可能收不到最后那个带用量的分块”。被提前切断的数据流,无论切断它的是一道流式护栏还是一个被关掉的标签页,都在这份报告到达之前就结束了,而它已经产生的每一个字符都在账单上。所以助手会在流式输出的同时保留文本,凡是服务商什么都没报告的地方,就用同一个字节数去补,这样是多记而不是少记。只要服务商给了数字,就以它自己的数字为准:一轮完整结束的对话,记录的正是被测到的量,绝不是它的近似值。RAG 幻觉护栏一文展示了这道检查在助手其他各层之间的位置。

同样的原则也适用于产品。在我主管 AI 工程的 Booq,每一项任务在运行之前就会显示它的价格,或者它的起始估算,而失败的任务会自动退回费用,Booq 案例研究描述了这套做法。

常见问题

在 OpenAI 的模型上,波斯语比英语更费 token 吗?

是的。在同样的 FLORES-200 句子上,波斯语在 o200k_base(tiktoken 把 GPT-4o、GPT-4.1 和 GPT-5 映射到它)上需要英语 1.53 倍的 token,在 cl100k_base(GPT-4)上是 3.26 倍。你自己的流量会给出另一个比值,所以要自己去数。

波斯语在 Claude 或 Gemini 上要用多少 token?

这次测量回答不了。Anthropic 没有公开 Claude 的分词器;Google 的 Gen AI SDK 为部分 Gemini 模型提供了一个基于开源 Gemma 分词器的实验性本地计数,我没有测过它。上面的比值对这两家都不适用。请在真实请求上使用 Anthropic 的 count_tokens 端点或 Gemini 的 count_tokens,换模型时再数一遍。

阿拉伯语比波斯语更便宜吗?

在这里测的五个分词器中,有四个是这样:在 o200k_base 上,阿拉伯语需要英语 1.38 倍的 token,波斯语是 1.53 倍。在 bge-m3 上则反过来,1.17 倍对 1.10 倍,这与 XLM-R 的训练数据里波斯语远多于阿拉伯语一致。

为了省 token,该去掉 ZWNJ 或改用阿拉伯语字母形式吗?

不该。把 ZWNJ 换成空格,在字节级分词器上让 token 数减少 1.57% 到 8.08%,同时也改变了词本身。阿拉伯语字母形式只在 Qwen3 上省 token,在别处让 token 数增加 2.20% 到 11.95%。除非每一个查询都跟着一起改,否则这两种做法都会破坏精确匹配。

一个波斯语 token 是多少字符?

要看分词器:在这次测量里,从 cl100k_base 上的 1.40 到 bge-m3 上的 3.68,而英语是 4.30 到 4.91。一条在某个分词器上安全的固定规则,在另一个上可能少算。本站在这次运行之后弃用了自己“每两个字符一个 token”的预留,改为预留字节数;一般性的建议也是如此:按一个硬上限或服务商的计数去预留,而不是按经验法则。

参考来源


如果你正在为讲波斯语的用户规划 LLM 功能,可以从项目诊断开始。