模型不认识字符。在文本进入模型之前,tokenizer(分词器)先把它切成一串 token(词元),再把每个 token 映射成词表里的整数 ID。模型全部的输入、输出、计费、上下文长度限制,统统以 token 为单位——token 是 LLM 系统里的基本计量单位,就像字节之于存储。
为什么不直接用字符或单词?两条路都走不通。用单词做词表:词表会爆炸(每个词形变化都是新词),而且永远会遇到词表外的新词(OOV,out-of-vocabulary)——用户打个错字模型就瞎了。用字符:词表很小且没有 OOV,但序列长度暴涨,模型要花很多算力才能理解「一个词」。
BPE(Byte Pair Encoding,字节对编码)是这两者之间的妥协,也是今天主流模型的默认选择。它的训练过程朴素得出奇:从单个字节开始,反复统计语料里最高频的相邻符号对,把它合并成一个新符号,重复几万次,得到一张固定大小的词表。结果是高频内容被压成一个 token,低频内容退化成碎片——常见英文单词占 1 个 token,生僻词被拆成几块,而任何字节序列都能被编码,永远不会 OOV。
BPE 的合并规则是从训练语料里统计出来的。如果语料以英文为主,那么英文的常见词组会被合并成单个 token,而中文只能拿到较少的合并机会——很多汉字甚至要用多个字节级 token 才能拼出来。同一份内容翻译成不同语言,token 数可以差出好几倍。
这个差异不是审美问题,它直接换算成三件事:① 成本——按 token 计费,token 多则账单高;② 延迟——输入 token 多则 prefill 慢,输出 token 多则总生成时间长;③ 有效上下文——同样的 128K 窗口,能塞进去的实际内容更少。这就是为什么中文场景下「上下文不够用」和「成本超预算」总是同时发生。
不要背比例,要去测。「中文 1 个字约等于几个 token」这类经验值随 tokenizer 版本变化很大,新一代模型的多语言 tokenizer 通常明显改善了 CJK 的压缩率。正确做法是用你实际调用的那个模型的 tokenizer 跑一遍你自己的真实语料,得到属于你的比例,再拿它做估算——这个数字应该属于工具层,而不是记在脑子里的常识。
context window(上下文窗口)是模型单次能处理的最大 token 数。它常被理解成一个简单的容量上限,但真正影响工程决策的是——把窗口填满的代价随长度增长得比线性更快。
推理分两个阶段。prefill(预填充)一次性处理全部输入 token,其中 self-attention 需要计算 token 两两之间的关系,计算量随输入长度呈二次方增长;decode(解码)逐个吐出输出 token,每步只算一个新 token,但要读取全部已有的 KV cache,而 KV cache 随长度线性增长(见 1.2)。
还有一件容易被忽略的事:窗口大不等于全窗口都好用。模型在超长上下文中定位关键信息的能力会随位置和长度衰减,这个现象在 3.4 节 详谈。所以「窗口够大就把所有资料都塞进去」既贵、又慢、还可能更差。
Petrov 等人系统性地测量了同一段内容翻译成不同语言后的 token 数差异。结论比大多数人的直觉更极端:
论文指出这种差异会直接传导为三重不平等:访问商业模型服务的成本、处理时间与延迟,以及能塞进上下文的内容量——恰好就是上文那三件事。对工程决策的启示很直接:如果你的产品服务多语言用户,成本模型和上下文预算必须按语言分别测算,用英文语料估出来的单位成本会系统性地低估非英语场景。
先确认 tokenizer 版本:同一家厂商不同代际的模型 tokenizer 可能不同,跨厂商更是完全不同。永远用你实际要调用的那个模型的 tokenizer 计数,不要用别家的结果外推。
开源模型:transformers 的 AutoTokenizer.from_pretrained(...) 直接加载模型自带 tokenizer,len(tok.encode(text)) 即 token 数,这是最可靠的口径。
商业 API:优先使用厂商官方提供的计数接口或官方 tokenizer 库(如 OpenAI 的 tiktoken);部分厂商提供 count-tokens 端点,可在发请求前精确计费预估。不要用「字数 ÷ 4」这类经验公式做正式的成本核算,只能用于量级判断。
建立你自己的基线:拿 1000 条真实线上语料跑一遍,记录 token/字符 比值、P50 与 P95 输入长度。这三个数是 2.4 容量规划 的直接输入,值得固化成一个定期重跑的脚本。
先声明假设(这一步比算术重要):设中文 1 字 ≈ 1.3 token——这个数必须用你实际模型的 tokenizer 实测得到,此处仅为演示。
关键陷阱是多轮对话的输入是累积的:第 k 轮的输入 = system prompt + 前 k−1 轮的全部问答 + 本轮提问。
system ≈ 800 × 1.3 ≈ 1040 token;每轮新增 ≈ (60 + 200) × 1.3 ≈ 338 token。
第 k 轮输入 ≈ 1040 + 338 × (k−1) + 78。6 轮输入合计 ≈ 6×1040 + 338×(0+1+2+3+4+5) + 6×78 ≈ 6240 + 5070 + 468 ≈ 11,800 输入 token;输出合计 ≈ 6 × 260 × 1.3 ≈ 2,030 输出 token。
值得记住的结论:输入 token 是输出的 5 倍以上,且其中 6240 是同一段 system prompt 被重复计费了 6 次。这正是 2.2 Prompt Caching 存在的理由——多轮对话场景里,前缀缓存能命中的部分恰好是占比最大的那部分。
因为模型看到的不是字母,是 token。一个单词可能整体是一个 token,模型内部并没有「这个 token 由哪些字符组成」的显式结构——要回答字母级问题,它得从训练数据中间接学到拼写知识,这远不如直接看字符可靠。
数字同理:多位数字的切分方式取决于 tokenizer,同一个数值在不同上下文里可能被切成不同的 token 组合,位值对齐关系因此被破坏。工程启示:字符级操作(计数、反转、精确截断)和精确算术应该交给代码工具做,而不是交给模型(见 4.1 Tool Use)。
三件事同时发生,而且它们看起来像三个不相关的故障:
① 成本超预算——中文相同信息量需要更多 token,实际单请求成本高于预估。② 延迟变差——输入更长,prefill 计算量按二次方增长,TTFT 涨幅超过 token 数涨幅。③ 截断 bug——如果限流或截断阈值是按 token 数设的,中文用户会在「更少的实际内容」处就被截断,表现为「中文回答莫名其妙断掉」。
修法不是把阈值调大了事,而是按语言分别建立成本与长度基线,并把截断逻辑改成基于语义边界而非固定 token 数。
这是一道考察成本意识的题,标准答案是「窗口是能力上限,不是使用建议」。三个层面:
成本层面:塞满 128K 的每一次调用都要为全部输入 token 付费和付延迟,而 prefill 的 attention 计算量随长度二次增长;RAG 只把相关的几千 token 送进去,成本差出一到两个数量级。
质量层面:塞进去 ≠ 用得上,长上下文中的信息定位能力会衰减,无关内容还会形成干扰(详见 3.4)。
架构层面:知识库通常远大于任何窗口,且需要增量更新、权限过滤、引用溯源——这些是检索层的职责,不会因为窗口变大而消失。
但也要给出长上下文确实更优的场景:单份文档需要全局理解(如整篇合同的一致性审查)、文档量小且固定、或对首次接入速度的要求高于成本——这时切分与检索反而是自找麻烦。