2024 年我做一个大模型应用,要把一批用户文档喂给 LLM 做摘要和问答。第一版我做得很省事:成本怎么估?按字数。我数了数文档大概多少字,乘以一个单价,算出一个月大概多少钱;上下文会不会超?也按字数。我心里有个数——"模型上下文 8K,那我就把 prompt 控制在八千字以内"。本地测了几篇——真不错:摘要质量不错,也没报错。我心里很踏实:"token 嘛,不就是字数?按字数估一估,八九不离十。"可等这套东西真正上线、跑起真实的文档流量,一串问题冒了出来。第一种最先把我打懵:月底一看账单,实际花的钱,是我预估的两倍还多——我反复核对用量,没人滥用,就是预估的模型本身错得离谱。第二种最磨人:有些文档,我明明按字数算没超 8K,拼进 prompt 一调用,模型却报错"超出上下文长度"——同样字数的另一篇却没事,我完全摸不着规律。第三种最反直觉:同样是一段文本,纯中文的、纯英文的、中英混排的,送进去之后"算出来的长度"竟然差出一大截。第四种最隐蔽:我做流式输出时,为了截断长度,按字符数硬切了一刀,结果切出来的文本末尾糊了一团乱码。我盯着这一连串问题想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"token 就是字数,模型按字、按词读文本,数数字符就能估算一切"。这句话把 token 当成了"字符"的同义词。可它不是。大模型根本不直接读字符。它读文本前,有一道你看不见的工序——tokenizer(分词器)先把文本切成一个个 token。一个 token 不是一个字、也不是一个词,它是 tokenizer 用 BPE 这类算法切出来的"子词单元":可能是一个完整的英文单词,可能是半个单词,可能是一个汉字,也可能是几个字节。模型的计费、上下文窗口、max_tokens——全部以 token 为单位,跟字符数没有稳定的换算关系。真正用对大模型,核心不是"数数有多少字",而是理解 tokenizer 这道工序、用工具精确数 token、按 token 来规划上下文预算和成本。这篇文章就把大模型的 token 梳理一遍:为什么"token 就是字数"是错的、tokenizer 到底在做什么、怎么用工具精确数 token、上下文窗口和 max_tokens 怎么按 token 规划、成本怎么按 token 算,以及不同模型不同分词器、安全截断、特殊 token 这些把 token 真正搞明白要避开的坑。
问题背景
先把那串问题的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。
现象:一套"按字数估算一切"的大模型调用,上线后冒出一串问题:月底账单是预估的两倍多;有些文档按字数没超 8K,却报"超出上下文长度";同一段意思的文本,中文、英文、中英混排,"算出来的长度"差一大截;流式输出按字符硬切,切出来末尾糊成乱码。
我当时的错误认知:"token 就是字数,模型按字按词读文本,数字符就能估算成本和上下文。"
真相:大模型不直接处理你看到的字符。一段文本送进模型之前,要先经过一道叫 tokenizer(分词器)的工序:它把文本切成一个个 token,再把每个 token 映射成一个整数 ID,模型真正读到的、真正计算的,是这串整数。关键在于:token 不是字符,也不是单词。主流模型用的是 BPE(Byte Pair Encoding,字节对编码)这一类算法:它统计海量语料里哪些字符片段经常一起出现,把高频片段合并成一个 token。结果就是——常见的英文单词(像 the、hello)整个就是一个 token;生僻词、长单词会被拆成好几个 token;一个汉字,通常要占 1 到 2 个甚至更多 token(因为中文在训练语料里占比不同、且按 UTF-8 字节编码)。所以 token 数和字符数没有固定的换算比例。我那串问题,根子全在这里:账单翻倍,是因为我拿"字数"当"token 数"估算,而中文的 token 数往往比字数多得多;没超字数却报上下文超限,是因为上下文窗口的 8K 是 8K 个 token,不是 8000 个字;中英文长度差一截,是因为不同语言的文本,token 化的"压缩率"天差地别;截断出乱码,是因为我按字符切,切断了一个本应完整的多字节 token。
要把大模型的 token 搞明白,需要几块认知:
- 为什么"token 就是字数"是错的——模型读的是 tokenizer 切出来的 token;
- tokenizer——BPE 把文本切成子词单元,再映射成整数 ID;
- 精确计数——别再估算,用 tiktoken 这类工具数 token;
- 上下文预算——上下文窗口和 max_tokens 都是 token 单位,要留余量;
- 成本估算、模型差异、安全截断这些工程坑怎么处理。
一、为什么"token 就是字数"是错的
先把这件最根本的事钉死:你和大模型之间,隔着一道 tokenizer。你输入的是一串字符,模型吃进去的却是一串整数 ID,中间这道把字符变成 ID 的工序,就是 tokenizer 干的。这道工序不是可有可无的细节——恰恰相反,模型对外暴露的所有"长度"和"价格",都是按这道工序的产物(token)来计量的:上下文窗口写着"128K",是 12.8 万个 token;计费写着"每百万 token 多少钱",是 token;max_tokens 限制的,还是 token。你拿"字符数"去套这些数字,就像拿"页数"去估"字数"——也许在某一种排版下大致成立,但换一种语言、换一种内容,比例立刻就崩了。把 token 理解成字数,你不是估得不准,你是在用一把刻度错乱的尺子量所有东西。
下面这段代码,就是我那个"估算全靠拍脑袋"的第一版:
# 反面教材:用"字符数"估算成本和上下文,刻度从根上就是错的
def estimate_cost(text, price_per_1k=0.01):
char_count = len(text)
# 想当然:把字符数直接当成 token 数
tokens = char_count
return tokens / 1000 * price_per_1k
def will_fit_context(prompt, limit=8000):
# 想当然:8K 上下文 == 8000 个字符
return len(prompt) < limit
# 破绽一:中文一个字常占 1~2+ 个 token,字数严重低估 token 数。
# 破绽二:8K 上下文是 8K 个 token,不是 8000 个字符。
# 破绽三:中文、英文、代码的"字符→token"比例完全不同,没有通用换算。
这段代码在本地测几篇时表现不错,因为本地我挑的那几篇文档,恰好字数和 token 数偏差不算大,或者根本没逼近上下文上限,误差被"样本太少"掩盖了。它的问题不在某个数字上——单价没写错,上限也填了——而在一个被忽略的前提:它默认"一个字符就对应一个 token"。可这个换算根本不存在。于是那串问题就有了解释:账单翻倍,是因为我处理的是中文文档,而中文的 token 数往往比字数多出几成到一倍;没超字数却报错,是因为limit 那个 8000 我当成字符在比,模型那头却在按 token 算;中英文长度差一截,是因为英文一个常见词只占一个 token、压缩率高,中文压缩率低。问题的根子清楚了:用对 token 的工程量,全在"承认模型按 token 计量一切、token 和字符没有固定换算"之后——你不去精确地数它,就只能一直用错尺子。先从这把尺子本身——tokenizer——说起。
二、tokenizer:BPE 把文本切成子词单元
tokenizer 的工作,一句话:把一段文本,切成一串 token,再把每个 token 查表换成一个整数 ID。主流大模型用的切法是 BPE——它在海量语料上预先训练好一张"合并表":哪些字符片段经常一起出现,就把它们合并成一个更大的单元。用 OpenAI 的 tiktoken 库,可以把这道工序直接看见:
import tiktoken
# 取出 GPT-4o 系列用的分词器
enc = tiktoken.encoding_for_model("gpt-4o-mini")
text = "Hello, tokenizer!"
ids = enc.encode(text) # 文本 -> 一串整数 ID
print(ids) # -> [13225, 11, 2657, 12347, 0]
print(len(ids), "个 token") # -> 5 个 token
# 把每个 token ID 单独 decode 回来,看清它到底切成了什么
for tid in ids:
print(repr(tid), "->", repr(enc.decode([tid])))
# 13225 -> 'Hello' 整个单词就是一个 token
# 11 -> ',' 标点单独一个 token
# 2657 -> ' token' 注意:连前面的空格一起,是一个 token
# 12347 -> 'izer' 'tokenizer' 被拆成了 ' token' + 'izer'
# 0 -> '!'
这段输出把 token 的真相摆出来了:"Hello, tokenizer!" 这 17 个字符,被切成了 5 个 token。常见词 Hello 整个是一个 token;tokenizer 这个稍长的词,被拆成了 " token" 和 "izer" 两个;连单词前面的空格,都被算进了 token。再看中文和英文的对比,差距更直观:
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o-mini")
samples = [
"hello world", # 纯英文
"你好世界", # 纯中文
"今天 weather 不错", # 中英混排
"def add(a, b): return a + b", # 一段代码
]
for s in samples:
n = len(enc.encode(s))
print(f"字符数={len(s):>3} token数={n:>3} 内容={s}")
# 字符数= 11 token数= 2 hello world —— 英文:压缩率高
# 字符数= 4 token数= 4 你好世界 —— 中文:几乎一字一 token 起步
# 字符数= 11 token数= 7 今天 weather 不错 —— 混排:两套规律叠加
# 字符数= 27 token数= 11 def add(a, b)... —— 代码:符号多,token 偏多
看这组数字:hello world 11 个字符只占 2 个 token,而 你好世界 4 个字符就占 4 个 token。这就解释了开头的现象——同样"长度感觉差不多"的文本,英文 token 少、中文 token 多,中英混排则两种规律叠在一起,你用一个固定比例去估,必然估歪。下面这张图,把从文本到模型输入的整条链路画出来:
这里的认知要点是:tokenizer 是一个"压缩器",而它的压缩率,严重依赖文本内容。它在英文语料上训练得最充分,所以常见英文词被压成一个 token,压缩率很高;中文、代码、特殊符号,压缩率就低得多。这意味着一个残酷的事实:不存在一个通用的"字符数 ÷ N = token 数"的公式。任何形如"中文大概 1.5 倍""英文大概 0.3 倍"的经验比例,都只是某一类文本的粗略平均,换一批内容就会失准。你唯一能信赖的,是把文本真正喂给 tokenizer,数出来。既然估算不可靠,那就来看怎么精确地数。
三、精确计数:别再估算,用 tiktoken 数 token
认清"token 数估不准"之后,正确的做法只有一个:凡是涉及成本、上下文的地方,都用 tokenizer 真正数一遍,而不是拿字符数去乘系数。把它封装成一个随手能调的函数:
import tiktoken
# 缓存 encoding 对象:它的加载有开销,不该每次调用都重建
_enc_cache = {}
def get_encoding(model="gpt-4o-mini"):
if model not in _enc_cache:
try:
_enc_cache[model] = tiktoken.encoding_for_model(model)
except KeyError:
# 模型名不认识时,退回到一个通用 encoding
_enc_cache[model] = tiktoken.get_encoding("cl100k_base")
return _enc_cache[model]
def count_tokens(text, model="gpt-4o-mini"):
"""精确数出一段文本的 token 数 —— 这才是唯一可靠的'长度'。"""
return len(get_encoding(model).encode(text))
但还有一个容易被忽略的细节:调 Chat 接口时,你传的是一个 messages 列表,不是一段纯文本。每条 message 的 role、消息之间的分隔,模型内部都会用一些特殊 token 包裹起来——这部分开销也要算进去,否则你数出来的 token 数会偏小:
def count_message_tokens(messages, model="gpt-4o-mini"):
"""估算一组 chat messages 的总 token 数,含 role 与分隔的固定开销。"""
enc = get_encoding(model)
# 每条消息有固定的结构性开销(包裹 role/content 的特殊 token)
tokens_per_message = 3
total = 0
for msg in messages:
total += tokens_per_message
for key, value in msg.items():
total += len(enc.encode(value)) # role 和 content 都要数
total += 3 # 整个回复也有起始开销
return total
messages = [
{"role": "system", "content": "你是一个有帮助的助手。"},
{"role": "user", "content": "用一句话解释什么是 token。"},
]
print(count_message_tokens(messages), "个 token")
这里的认知要点是:"数 token"这件事,要变成你代码里的一个基础设施,而不是一个临时算法。任何一处你过去用 len(text) 来判断"长不长""贵不贵"的地方,都应该换成 count_tokens。它的成本极低——tiktoken 是本地纯计算,不需要联网、不花钱;它换来的,是你对成本和上下文的所有判断,第一次站在了真实的刻度上。还要记住:chat 接口的 token 不只是 content 的 token,role 和消息结构本身也占数——把这部分固定开销忘掉,你的预算又会悄悄差一截。能精确数了,下一步就是用这个数,去管好两件最要紧的事:上下文和成本。
四、上下文预算:窗口和 max_tokens 都是 token 单位
开头第二个问题——"按字数没超 8K,却报上下文超限"——根子就在把 token 单位的限制当成了字符单位。这里要先分清两个常被搞混的概念。上下文窗口(context window):是模型一次能处理的 token 总量上限——而且这个上限是"输入 + 输出"一起算的。max_tokens:是你给这次生成的"输出"设的 token 上限。两者的关系是:输入 token + max_tokens,不能超过上下文窗口。所以拼 prompt 时,不能把窗口占满,必须给输出留出空间:
# 上下文预算:输入 + 输出 必须留在窗口之内
def plan_context(messages, model="gpt-4o-mini",
context_window=128000, reserve_for_output=2000):
"""规划一次调用的 token 预算,确保输入留够输出空间。"""
input_tokens = count_message_tokens(messages, model)
# 留给输出的空间 = 窗口 - 已用输入,但不超过我们想要的上限
budget_for_output = context_window - input_tokens
if budget_for_output < 256:
raise ValueError(
f"输入已占 {input_tokens} token,窗口 {context_window} 几乎被占满,"
f"必须先压缩输入")
safe_max_tokens = min(reserve_for_output, budget_for_output)
return {
"input_tokens": input_tokens,
"max_tokens": safe_max_tokens, # 安全地传给接口的 max_tokens
"remaining": budget_for_output,
}
当输入本身就快把窗口占满时,光靠"留余量"已经不够,得主动把输入压缩进预算——比如对一段超长文本,按 token 数截到一个安全长度:
# 把一段超长文本,精确截断到指定的 token 数以内
def truncate_to_tokens(text, max_tokens, model="gpt-4o-mini"):
"""按 token 截断:在 token 边界上切,绝不切碎一个 token。"""
enc = get_encoding(model)
ids = enc.encode(text)
if len(ids) <= max_tokens:
return text
# 关键:在 token ID 列表上切片,再 decode 回文本
truncated_ids = ids[:max_tokens]
return enc.decode(truncated_ids)
# 因为是在 token 边界切的,decode 出来的文本天然完整,不会有半个字。
这里的认知要点是:上下文窗口不是"输入的额度",是"输入和输出共用的总额度"。这是最容易踩的一个坑——你把 prompt 拼到刚好贴着窗口上限,以为天衣无缝,结果模型连一个字的回复都吐不出来,因为没有 token 额度留给它了。所以规划上下文,脑子里要装的是一道减法:窗口总量,减去输入,剩下的才是输出能用的;反过来,你想要多长的输出,就得从窗口里先把那块预留出来,输入只能用剩下的。一切都按 token 算,一切都在一个总盘子里。上下文规划好了,另一件被 token 决定的大事是钱——这就要说到成本。
五、成本估算:按 token 算账,输入输出还不同价
开头第一个问题——"账单翻倍"——根子是成本估算从一开始就用错了单位。大模型的计费,清一色是按 token 算的,而且有两个必须知道的细节。第一,输入 token 和输出 token,通常是两个不同的单价——输出往往比输入贵好几倍。第二,中文文本的 token 数,往往明显高于字数,你按字数估,就会系统性低估。把成本估算认真做一遍:
# 成本估算:输入、输出分开计价,单位一律是 token
# 单价示意(美元/百万 token),实际以服务商最新价目为准
PRICING = {
"gpt-4o-mini": {"input": 0.15, "output": 0.60},
"gpt-4o": {"input": 2.50, "output": 10.00},
}
def estimate_call_cost(messages, expected_output_tokens,
model="gpt-4o-mini"):
"""估算一次调用的成本:输入按 message token,输出按预期 token。"""
input_tokens = count_message_tokens(messages, model)
price = PRICING[model]
input_cost = input_tokens / 1_000_000 * price["input"]
output_cost = expected_output_tokens / 1_000_000 * price["output"]
return {
"input_tokens": input_tokens,
"output_tokens": expected_output_tokens,
"input_cost": round(input_cost, 6),
"output_cost": round(output_cost, 6),
"total_cost": round(input_cost + output_cost, 6),
}
真正要估一整个月的账单时,还要乘上调用量,并且用真实样本去数,而不是拿一个想当然的平均字数:
# 月度成本估算:务必用"真实样本"数 token,而不是拍一个平均值
def estimate_monthly_cost(sample_messages_list,
avg_output_tokens,
calls_per_month,
model="gpt-4o-mini"):
"""用一批真实请求样本,估算月度总成本。"""
# 对真实样本逐个数 token,取平均 —— 这是关键,别凭感觉填数
sample_input_tokens = [
count_message_tokens(m, model) for m in sample_messages_list
]
avg_input = sum(sample_input_tokens) / len(sample_input_tokens)
price = PRICING[model]
per_call = (avg_input / 1_000_000 * price["input"]
+ avg_output_tokens / 1_000_000 * price["output"])
return {
"avg_input_tokens": round(avg_input, 1),
"cost_per_call": round(per_call, 6),
"monthly_cost": round(per_call * calls_per_month, 2),
}
这里的认知要点是:成本估算错得最离谱的两种做法,一是用字数当 token 数,二是输入输出一个价。前者让你低估,因为中文 token 比字数多;后者也让你低估,因为输出单价往往是输入的好几倍,而一个"短输入、长输出"的任务(比如写文章),成本的大头其实在输出。要把账估准,只有一条路:拿一批真实的请求样本,用 tokenizer 把输入 token 老老实实数出来,输出 token 用真实观测的平均值,再分别乘以各自的单价。任何一个环节用"估"代替"数",最后的数字就会在账单上给你补回来。主干都讲完了,最后是几个真正用起来才会撞见的工程坑。
六、工程坑:模型差异、安全截断与特殊 token
五块设计之外,还有几个工程坑,不处理就会让你要么数错、要么切坏、要么被特殊 token 反咬一口。坑 1:不同模型用不同的 tokenizer,数 token 必须对应模型。同一段文本,用 GPT-4o 的分词器和用更老模型的分词器,数出来的 token 数可能不一样;别的厂商(Claude、Gemini、国产模型)更是各有各的分词器。所以 count_tokens 必须带上模型参数,不能全局写死一个:
import tiktoken
# 不同模型 -> 不同 encoding,数 token 前要先对应清楚
def show_encoding(model):
try:
enc = tiktoken.encoding_for_model(model)
return f"{model:>16} -> {enc.name}"
except KeyError:
return f"{model:>16} -> 未知,需用厂商自己的分词器"
for m in ["gpt-4o-mini", "gpt-4", "gpt-3.5-turbo"]:
print(show_encoding(m))
# gpt-4o-mini -> o200k_base
# gpt-4 -> cl100k_base —— 注意:和 4o 系列不是同一个
# gpt-3.5-turbo -> cl100k_base
# 提醒:tiktoken 只覆盖 OpenAI 系。Claude/Gemini 等要用各自官方的计数方式。
坑 2:截断长文本,一定要按 token 切,不能按字符切。开头第四个问题——截断出乱码——就是按字符硬切的恶果。一个 token 可能对应多个字节,你按字符(甚至按字节)拦腰一刀,很可能切碎一个 token,decode 出来就是乱码。正确做法在第四节的 truncate_to_tokens 里已经给了:在 token ID 列表上切片。这里再强调反面写法错在哪:
# 反面:按字符数截断 —— 可能从一个多字节 token 中间切断
def bad_truncate(text, max_chars=1000):
return text[:max_chars] # 危险:切点可能落在一个 token 内部
# 正确:永远在 token 边界上操作(见第四节 truncate_to_tokens)
# 口诀:涉及"长度"就想 token,涉及"切割"就在 token ID 列表上切。
坑 3:warn 当心特殊 token,别把用户输入当控制符。tokenizer 里有一类特殊 token(如表示对话开始/结束的标记)。如果你直接 encode 一段可能含有这类标记字面量的用户输入,某些 encode 选项下它会被当成"特殊 token"而非普通文本,轻则数错、重则出现安全问题。数普通文本时,要显式禁止把特殊 token 解析出来:
# 数"用户输入"这种不可信文本时,禁止它被解析成特殊 token
def count_user_text(text, model="gpt-4o-mini"):
enc = get_encoding(model)
# disallowed_special=() :把所有特殊 token 当普通文本处理,更安全
return len(enc.encode(text, disallowed_special=()))
坑 4:别为了省 token 把 prompt 压得不成样子。知道 token 要花钱后,有人会走向另一个极端:疯狂删字、用缩写、去标点。但 prompt 写清楚带来的质量提升,通常远比省下的那几个 token 值钱。该精简的是冗余和废话,不是把指令砍到模型看不懂。坑 5:流式输出按 token 计费,也要实时累计。流式响应是一个 token 一个 token 推回来的,要统计这次输出花了多少,累计收到的 chunk 数即可,接口返回的 usage 字段也会给出权威的 token 用量,以它为准对账。
关键概念速查
| 概念 / 手段 | 说明 |
|---|---|
| token | tokenizer 切出的子词单元,模型计量的最小单位,非字非词 |
| tokenizer | 把文本切成 token 并映射成整数 ID 的分词器 |
| BPE | 字节对编码,统计高频字符片段合并成 token 的算法 |
| tiktoken | OpenAI 的本地分词库,可精确数 token,不联网不花钱 |
| 上下文窗口 | 一次调用能处理的 token 总量,输入与输出共用 |
| max_tokens | 本次生成的输出 token 上限,需从窗口里预留 |
| 输入/输出计价 | 两者通常不同价,输出单价往往是输入的数倍 |
| 按 token 截断 | 在 token ID 列表上切片,避免切碎多字节 token |
| 特殊 token | 标记对话起止等的保留 token,数用户输入时应禁用解析 |
| encoding 差异 | 不同模型对应不同 encoding,计数必须对应模型 |
避坑清单
- token 不是字数,字符数和 token 数没有固定换算比例。
- 中文一个字常占 1~2+ 个 token,英文一个常见词常只占 1 个。
- 涉及成本与上下文的地方,用 tiktoken 精确数,别拿字数估。
- chat 接口要算 role 和消息结构的固定 token 开销,别只数 content。
- 上下文窗口是输入加输出共用的总额度,拼 prompt 必须留输出空间。
- 截断长文本在 token ID 列表上切,按字符切会切出乱码。
- 成本按 token 算,且输入输出分开计价,输出通常贵得多。
- 估月度账单用真实样本数 token,别凭平均字数拍脑袋。
- 不同模型分词器不同,count_tokens 要带模型参数。
- 数不可信的用户输入时禁用特殊 token 解析,防数错与安全问题。
总结
回头看那串"账单翻倍、没超字数却上下文超限、中英文长度差一截、截断出乱码"的问题,以及我后来在 token 上接连踩的坑,最该记住的不是某一个换算系数,而是我动手前那个想当然的判断——"token 就是字数,数数字符就能估算一切"。这句话错在它把一个"模型内部的计量单位",当成了一个"人眼可见的字符"。我以为我看到多少字,模型就读到多少字。可我忽略了一件事:我和模型之间,隔着一道我看不见的 tokenizer。我数的是字符,模型算的是 token——它的账单、它的上下文窗口、它的每一个长度限制,说的都是 token 那门语言。我用字符去和它对话,从一开始就用错了货币。
所以用对大模型的 token,真正的工程量不在"调一下接口"那几行代码上。那几行,谁都会写。真正的工程量,在于你要承认"模型按 token 计量一切、token 和字符没有固定换算",并据此把"数 token"变成代码里的基础设施:你要估成本,就用 tokenizer 数真实样本、输入输出分开计价;你要拼 prompt,就按 token 规划上下文预算、给输出留够空间;你要截断长文本,就在 token 边界上切、绝不按字符硬切;你要支持多个模型,就让计数对应各自的分词器;你要数用户输入,就禁掉特殊 token 的解析。这篇文章的几节,其实就是顺着这条线展开的:先想清楚"token 就是字数"为什么错,再讲 tokenizer 在做什么、怎么精确数、上下文怎么按 token 规划、成本怎么按 token 算,最后是模型差异、安全截断、特殊 token 这几个把 token 搞扎实的工程细节。
你会发现,大模型的 token,和现实里"出国旅行时的汇率"完全相通。你揣着人民币出了国,可当地超市的标价、餐厅的账单、酒店的房费,全是用当地货币标的。一个不懂汇率的人会怎样?他看到标价"100",就以为是 100 块人民币,大手大脚地花,回国一对账单才发现花的是两倍的钱(这就是拿字数估成本);他按人民币的面值去估"这些钱够不够付房费",到了柜台才发现差一大截(这就是按字数估上下文);他在欧元区和日元区之间穿梭,却用同一个汇率换算,自然处处算错(这就是不同模型用不同分词器)。而一个懂汇率的人怎么做?他进了一个国家,先把汇率搞清楚,看到标价就在心里换算成自己熟悉的货币(这就是用 tokenizer 精确数 token);他知道每个国家汇率不同,该换算就换算,绝不拿一个比例走天下(这就是计数对应模型)。同样一笔钱、同样要花,可前者一路被汇率坑得稀里糊涂,后者每一笔都花得心里有数——差别不在钱多钱少,只在他认不认"这里通行的是另一种货币"这件事。
最后想说,大模型的 token 算没算对,差距永远不会在"本地测几篇、看着都挺好"时暴露——本地你挑的样本太少,恰好偏差不大、也没逼近上下文上限,误差被掩盖了,你会觉得"数数有多少字"已经够用。它只在真实的、文档成千上万篇地涌进来、账单按月结算、上下文被真实流量逼到上限的线上环境里才显形。那时候它会用最直接的方式给你结账:做不好,你的账单会是预估的两倍,你的调用会因为上下文超限而成片报错,你的截断会吐出一串乱码;而做对了,你的成本估算和账单严丝合缝,你的 prompt 稳稳待在窗口之内、给输出留足了空间,你的截断每一刀都切在 token 边界上。所以别等"月底账单吓你一跳"找上门,在你写下每一处和"长度""价格"有关的代码时就该想清楚:这里说的到底是字符,还是 token——它数对单位了吗、它给输出留余量了吗、它按 token 边界切了吗、它对应的是哪个模型的分词器,这一道道工序,我是不是都换算成 token 那门货币去想过了?这些问题有了答案,你写下的才不只是一段"本地能跑"的调用,而是一套成本可控、上下文可靠、经得起真实流量考验的大模型应用。
—— 别看了 · 2026