想象你是一家智能积木玩具厂的仓库调度员。厂里的机器臂笨得很,它不认识“今天天气不错”这句话,只认积木编号:1743、321、86……所以每次接到客户订单,你得先把整句话翻译成一张“积木编号清单”,机器臂才能开工。这个翻译动作,就是大模型里的“分词”(tokenization)。 为了让装配更快,厂里的设计师们发明了一套“标准积木规格表”:把经常拼在一起的小积木预先粘成一块大积木,比如把 h、e、l、l、o 五个小粒粘成一块现成的“hello“。反复粘合成千上万次,最终沉淀出一只装满了十万多块精选积木的大箱子——这只箱子就是 GPT-4 的 cl100k_base 词表。负责按规格表快速拆单、准确报数的那个”智能拆单员”,就是我们今天的主角:Tiktoken 库。 这篇文章会带你走进这座积木厂:先看为什么要分词,再看 BPE 算法怎么反复粘合字节对,接着了解字节级设计的巧思、正则预分词这把“剪刀”、词表的完整训练流程,最后认识 cl100k_base 词表和 Tiktoken 库,并学会用它估算 API 成本。

岗位关联

对 LLM 应用开发者和数据工程师来说,理解 Tiktoken 与 BPE 是硬通货:它能让你精确预估 API 账单、优化 prompt 长度、设计高效的 RAG 与数据清洗管道。读完这篇,你将不再把 token 当黑盒,而是能亲手算出每一句话的“积木件数”。

目录
  1. 可视化演示
  2. 分词与词表
  3. BPE合并算法
  4. 字节级BPE
  5. 正则预分词
  6. 词表训练流程
  7. cl100k_base词表
  8. Tiktoken库
  9. 成本优化
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

分词与词表

想象你是智能积木玩具厂的仓库调度员。机器臂不认“今天天气不错”,只认积木编号:1743、321、86……每接到客户订单,你得先把句子翻译成一张“积木编号清单”。这个翻译动作,在 AI 世界叫分词(tokenization),而清单上的每一项,就是一个 token。

在 GPT 这类大模型眼里,根本不存在“字符”这个概念,它只吃整数 ID。所有 token 的集合叫作词表(vocabulary),词表有多大,模型就能认多少种积木件。GPT-4 的词表有十万多件,而 Tiktoken 正是官方配发的“拆单工具”。一个 token 可能是整个单词(hello)、半个单词(hell)、一个标点,甚至一个空格——拆得越合理,模型理解得越省力。

开发者最关心的实操问题是“这句话会消耗多少 token”。经验上,英文大约每 4 个字符折合 1 个 token,中文则约 1 个字接近 1 个 token。这个数字直接决定 API 账单,所以拆单前先粗算一下很有必要。

就像调度员扫一眼订单页数就能猜出大概用多少积木件,工程师看一眼 prompt 字数也能估出 token 消耗。当然,真正的精确数,还要交给 Tiktoken 这个专业拆单员来算——这正是后面几节要讲的事。

ntokens≈⌈Lchars4⌉n_{\text{tokens}} \approx \left\lceil \frac{L_{\text{chars}}}{4} \right\rceil
这个公式表示:用文本的字符总数除以 4,再向上取整,就能大致估出 token 数量。它只用于快速估算,精确值必须交给 Tiktoken 计算。
  1. 分子 L_chars$L_{\text{chars}}$ 是文本的字符个数,英文字母、数字、空格都算一个字符
  2. 分母常量 4英文语料的经验统计值,平均约 4 个字符对应 1 个 token
  3. 向上取整符号$\left\lceil \cdot \right\rceil$ 表示结果不是整数就进一位,比如 9 个字符算出 2.25,实际按 3 个 token 计

BPE合并算法

积木厂仓库最初只有 256 种最基础的“小方粒”。设计师发现,每次装配 “hello“ 都要先找 h、e、l、l、o 五粒,太慢了。于是他们守在流水线边统计:哪两粒最常被拼在一起?l 和 l 总黏着,先粘成 ”ll“;h 和 e 也常出现,粘成 ”he“。粘完再统计,ll 和 o 又常一起,粘成 ”llo“。几次之后,仓库里多了一块现成的 ”hello” 大件。

这套“反复粘合最常见相邻对”的思路,就是 BPE(Byte Pair Encoding,字节对编码)算法,GPT 系列分词的核心。BPE 属于子词分词法:它不把单词当不可分割的整体,也不把文本拆到只剩字母,而是从最小元素出发,自底向上合成高频子词。常见词如 “the“ 很快变成整件,生僻词如 ”loww“ 则可拆成 ”low” + "w",永远不会因为词表里没有而失败。

训练时每轮只做两件事:统计当前所有相邻对的出现频率,把频率最高的一对合并成新符号并加入词表。反复迭代几万次,满屏字母的文本会被逐渐覆盖上“单词级大件”,模型的输入序列也随之变短。

设计师每粘合一粒大积木,流水线上的装配动作就少一步;BPE 每合并一次,模型处理这段文本的 token 数就少一个。语料里高频出现的词,最终都会变成“整装件”。

f(a,b)=∑w∈Dcount(ab,w),V(k+1)=V(k)∪{arg⁡max⁡p,qf(p,q)}f(a,b) = \sum_{w \in \mathcal{D}} \mathrm{count}(ab, w), \qquad V^{(k+1)} = V^{(k)} \cup \{ \arg\max_{p,q} f(p,q) \}
左边是在统计相邻字节对 $(a,b)$ 在语料里的总次数;右边表示每轮把出现频率最高的一对合并成一个新符号,加进词表 $V$。
  1. 频率统计 f(a,b)$f(a,b)$ 表示相邻对 $(a,b)$ 在所有训练文本里共同出现的总次数,$\mathcal{D}$ 是全部训练片段
  2. 计数函数 count$\mathrm{count}(ab, w)$ 计算片段 $w$ 内部出现相邻对 $ab$ 的次数,再对所有片段求和
  3. 挑选最大值 argmax$\arg\max_{p,q} f(p,q)$ 表示从所有候选相邻对里找出频率最高的一对
  4. 词表更新 V$V^{(k+1)}$ 是第 $k+1$ 轮的词表,等于上一轮词表并上新符号 $\{ab\}$,词表每轮多一项

字节级BPE

仓库现在接到世界各地的订单:阿拉伯文、日文,还有夹杂 🚀 的表情包。如果每种语言的每个字符都做成一种小方粒,仓库会爆炸。设计师一狠心:干脆把最底层规格统一成 256 种“标准微粒”(字节),任何文字都能拆成这些微粒的排列。仓库从此不怕生僻字。

这些“标准微粒”就是 UTF-8 编码后的字节。英文 a 占 1 字节,中文“你”占 3 字节,emoji 占 4 字节。BPE 不再作用在字符上,而是作用在这串字节上。初始词表就是 256 个字节(取值 0~255),之后合并出的每个新符号,本质是若干字节的组合。

为什么非要字节级?因为 Unicode 字符有上百万个,字符级词表再大也会遇到 OOV(out-of-vocabulary,词表外词)。字节级词表从 256 出发,理论上任何 UTF-8 文本都能被拆解,永远不会出现“这个词没见过”。这个设计同时解决了多语言与未知词两大难题,也保证了 decode 可以无损还原原文。

有了 256 种标准微粒,设计师的世界清净了——不管客户用什么语言下订单,原料都能拆到最底层,再按统计规律重新聚合成“大件”。这就是字节级 BPE 让 GPT 系列“通吃”全球语言的根本原因。

UTF-8:  s↦[b1,b2,…,bm],bi∈{0,1,…,255}\text{UTF-8}: \; s \mapsto [b_1, b_2, \dots, b_m], \quad b_i \in \{0, 1, \dots, 255\}
这表示任意文本 $s$ 经过 UTF-8 编码后,变成一串字节 $b_1$ 到 $b_m$,每个字节都是 0 到 255 之间的整数。BPE 就在这串字节上做合并。
  1. 文本映射为字节序列$s \mapsto [b_1, b_2, \dots, b_m]$ 表示一个字符或多个字符被编码成一串字节,比如“你”变成 3 个字节
  2. 字节取值范围$b_i \in \{0, 1, \dots, 255\}$ 说明每个字节只有 256 种取值,这就是“256 种标准微粒”的由来
  3. 序列长度 m$m$ 随文本内容变化,英文短、中文更长,因为不同字符占 1~4 个字节不等

正则预分词

设计师没敢直接对整段话做统计——那会乱套。“hello world“ 里如果 h 和 e 隔着空格也能硬粘,产线会疯掉。发货部老师傅给出建议:先用一把剪刀按语言习惯把句子剪开。这把剪刀碰到单词、数字、标点、空格的结合处就下刀,剪出的每个片段都是”可独立装配的单元”。

这把剪刀就是正则预分词(pre-tokenization)。GPT-2 训练词表时采用的规则效果大致是:连续字母归成一段,标点单独成段,空格保留成片段前缀。比如 “hello world!“ 会被切成 [”hello”, " world", “!“]。注意第二个片段前面带着空格,这是刻意保留的——模型需要知道 ”world” 是在句首还是句中。

预分词最大的价值是把 BPE 的搜索空间大大缩小。BPE 只在每个片段内部寻找可合并的相邻对,跨片段永不合并。空格、标点的边界信息因此被完整保留,数字也常常整段留下。这让大模型对代码、编号、URL 的切分质量明显提升,GPT-4 的分词器延续了同样的思路。

先剪后粘,节奏清晰。就像仓库师傅先把大件订单按“包装盒”分开,再在盒子里做积木优化——既不跨盒混装,也不漏掉任何零件。预分词就是 BPE 正式开工前的“分盒工序”。

R(s)={s1,s2,…,sk},s=concat(s1,s2,…,sk)\mathcal{R}(s) = \{ s_1, s_2, \dots, s_k \}, \quad s = \mathrm{concat}(s_1, s_2, \dots, s_k)
这表示正则预分词器 $\mathcal{R}$ 把文本 $s$ 切成若干片段 $s_1$ 到 $s_k$,这些片段按顺序拼起来还必须恰好还原成 $s$,一个字都不能丢。
  1. 预分词映射 R(s)$\mathcal{R}(s)$ 是正则规则作用在文本上的结果,输出一个片段集合
  2. 无损拼接条件$s = \mathrm{concat}(s_1, \dots, s_k)$ 是所有片段首尾相连后必须等于原文,保证拆分不增不减

词表训练流程

现在把设计师的完整工作流程摆上台面:先收集海量历史订单——网页、书籍、代码,这叫语料;然后用剪刀预分词;接着从 256 个字节出发,一轮轮统计、合并;直到词表达到目标规模,比如 10 万件。整个过程几乎自动化,不需要人为干预。

具体来说,训练 BPE 词表只需五步:准备语料 → 正则预分词 → 初始化字节级词表 → 循环执行合并(每轮加一项)→ 收敛输出词表。每一步都非常机械。OpenAI 当年就是用类似流程,在超大语料上跑出了 cl100k_base 词表。训练好的结果包含两部分:词表里的符号串,以及每个符号的 rank(合并顺序优先级,越小表示越靠近底层字节)。

词表大小和合并次数是同一个硬币的两面:基础表 256 项,每合并一次词表多一项,因此总的合并次数 M 恰好等于最终词表大小减 256。cl100k_base 的容量 100,256,就来自约 10 万次合并。

这批“规格表”一旦定型,就变成所有产线共享的资产。仓库不再每次重新发明积木,而是直接拿着规格表拆单。理解了训练流程,你就知道词表不是天上掉下来的,而是海量语料里“长”出来的。

M=∣Vfinal∣−∣Vbase∣=∣Vfinal∣−256M = |V_{\text{final}}| - |V_{\text{base}}| = |V_{\text{final}}| - 256
合并总次数等于最终词表大小减去初始字节表大小。初始永远是 256 个字节,所以词表越大,意味着训练时执行的合并轮次越多。
  1. 最终词表大小$|V_{\text{final}}|$ 是训练结束后词表里符号的总个数,比如 cl100k_base 的 100,256
  2. 基础字节表$|V_{\text{base}}|$ 恒为 256,对应 256 种 UTF-8 字节微粒
  3. 合并次数 M$M$ 就是训练过程中执行的“粘合”总次数,也是词表净增长量

cl100k_base词表

经过千锤百炼,GPT-4 工厂选定了一款“豪华积木箱”,型号 cl100k_base。cl 是 class(系列)的缩写,100k 指约 10 万容量。GPT-4、GPT-3.5-turbo 的 API 都配发这款箱子。它的总容量是 100,256:256 个基础字节,加上约 10 万个由 BPE 合并而来的子词与整词。

除了普通 token,箱子里还有一批“管理件”——特殊 token。比如 <|endoftext|> 标记文本结束,<|fim_prefix|>、<|fim_middle|>、<|fim_suffix|> 用来支撑代码补全的填充式任务(FIM,Fill-In-the-Middle)。它们和普通 token 一样占用编号,encode 时被单独识别,不会和普通字符混淆。

为什么 GPT-4 选 10 万容量而不是 5 万?词表越大,单个 token 携带的信息越多,模型处理同样文本要走的步数越少,但最后一层 softmax 的计算负担也越大。10 万是在“表达力”与“算力成本”之间折中的结果。实际效果是:英文常见词大多一整个 token,中文按字节合并出常用字词,少见内容退化为多字节组合。

这款积木箱就是 GPT-4 系列仓库的标准配置。作为开发者,你不必自己训练词表,直接用官方成品即可;理解它的内部构造,才能在估算 token 时心中有数,也知道那些带尖括号的“管理件”是干什么用的。

∣Vcl100k∣=100,256|V_{\text{cl100k}}| = 100{,}256
cl100k_base 词表的容量精确值是 100,256。中间的逗号只是千位分隔符,方便阅读,数值就是十万零二百五十六。
  1. 词表符号 V$V_{\text{cl100k}}$ 表示 cl100k_base 这套词表(积木箱)的全部符号集合
  2. 容量 100256$100{,}256$ 是词表总项数,即 256 个基础字节 + 约 100,000 次合并新增的符号
  3. 模型含义任何输入文本最终都会被映射成这 100,256 个编号中的若干个,不会越界

Tiktoken库

现在请出真正的“智能拆单员”——Tiktoken,OpenAI 开源的快速分词库。它自带 cl100k_base 等词表的规格书(rank 表),给定一段文本,能瞬间输出 token ID 列表;反过来给 ID 列表,也能一字不差还原文本。装上它,你就拥有了和 OpenAI API 完全一致的分词口径。

工程上它做了三件事:一是预编译正则,把文本切成片段;二是在每个片段内部查 BPE 合并表,按 rank 从底层到高层贪心匹配出 token;三是维护一张“符号串 → rank”的字典,rank 越小表示它越是基础的字节或越早合并的基元。算法主体用 Rust 实现,速度远快于纯 Python 的 BPE 实现。

使用极其简单:tiktoken.get_encoding("cl100k_base") 拿到编码器,enc.encode(text) 返回 id 列表,enc.decode(ids) 还原原文。要处理特殊 token 就用 encode_with_special_tokens。对 GPT-4 系列模型,还可以用 encoding_for_model("gpt-4") 自动匹配词表。开发者几乎不需要关心底层细节。

这位拆单员最大的美德是“可靠”:拆得再碎,装回去一个零件不少。无论是提交给 API 的 prompt,还是接收到的补全结果,都能精确换算成 token 数,让账单清清楚楚。

decode(encode(s))=s\mathrm{decode}(\mathrm{encode}(s)) = s
这表示“先编码再解码”必然还原原始文本。字节级 BPE 保证了分词过程无损,这也是 Tiktoken 敢把自身作为计费基准的底气。
  1. 编码函数 encode$\mathrm{encode}(s)$ 把文本 $s$ 变成一串 token ID,即“拆单员”把句子拆成积木编号
  2. 解码函数 decode$\mathrm{decode}(\cdot)$ 把 token ID 序列拼回字符串,即按编号把积木原样装回
  3. 恒等式还原等式表示对任意合法文本,两者复合后等于原文,说明分词可逆、信息零丢失

成本优化

作为厂里调度员,每次接单先让拆单员算件数,因为财务部是按件收钱的。API 的费用公式简单粗暴:token 总数除以 1000,向上取整,再乘单价。一句话 prompt 可能只要 20 个 token,一份长文档摘要却可能吃掉 4000 个 token。

实践中有两个常用经验:英文 1 token ≈ 4 字符 ≈ 0.75 个单词;中文 1 个汉字约 0.6~1.5 个 token,保守估算按 1~2 个算。但经验只是预判,真正开发时必须用 Tiktoken 精确统计——prompt 模板、历史消息、function 定义都会占用上下文,漏算任何一项都会让成本评估失真。

控制成本的核心手段包括:精简 prompt、用摘要代替长文本、设置最大响应 token、对用户输入限流。在 RAG(检索增强生成)场景里,检索回来的文档块要按 token 预算动态截断,给“生成答案”留足空间;批处理任务则先统计全部样本的 token 分布,再决定分几批跑、要不要压缩。

拆单员的账本,就是产品盈亏的晴雨表。会估 token,你就能在功能上线前算出成本红线,而不是等账单来了才拍大腿。

cost=⌈ntokens1000⌉×P\text{cost} = \left\lceil \frac{n_{\text{tokens}}}{1000} \right\rceil \times P
API 费用 = 实际 token 数除以 1000 后向上取整,再乘以每千 token 的单价。比如 1,500 token 会按 2,000 计费。
  1. 总 token 数 n_tokens$n_{\text{tokens}}$ 是一次请求的完整 token 总数,包含 prompt 与模型生成的回答
  2. 千 token 取整$\left\lceil \frac{n_{\text{tokens}}}{1000} \right\rceil$ 把 token 数折算成“千 token”单位,不足一千按一千算
  3. 单价 P$P$ 是模型每 1000 个 token 的美元价格,不同模型差别很大
  4. 费用 cost$\text{cost}$ 就是这次请求的美元成本,向上取整的惩罚让短请求也要付“起步价”

AI 应用场景

API 成本预估与配额管理

阶段:推理部署

解决的问题:开发 LLM 应用时无法预测用户输入长度,账单容易失控。传统做法是等月底看账单再补救,而用 Tiktoken 可以在每次 API 调用前精确统计 prompt 的 token 数,乘上单价做实时预算;也能对单用户每日累计 token 设上限,超限自动降级。

具体案例:某客服机器人项目上线前,用 Tiktoken 跑历史 10 万条真实用户问题,得出平均每条 86 个 token、P99 为 240 个 token,据此把单次调用的 token 上限设为 600,成本下降约 35%。

RAG 长文本上下文适配

阶段:数据预处理与检索

解决的问题:RAG 检索回的文档可能超过模型上下文窗口(如 8k 或 128k 限制)。按字符截断容易切断语义,而用 Tiktoken 对每个文档块计算 token 数,可按预算精确拼接 prompt,超限部分降级为摘要或丢弃。

具体案例:企业知识库问答场景中,文档块长短不一。工程师用 Tiktoken 批量分词后按 token 分布分桶存储,检索阶段动态调整条数与长度,把最终 prompt 稳定压在 3,000 token 以内,回答质量提升且不再报上下文超限错误。

微调语料的质量评估与清洗

阶段:数据质量分析

解决的问题:模型微调语料里混着大量重复、超长、噪音文本。直接看字符数不够直观,用 Tiktoken 计算每条样本的 token 数,结合重复度做过滤——token 数超过阈值的样本往往是未清洗的整页网页;近似重复的文本也可按 token 分布去重。

具体案例:数据工程师处理 500 万条 SFT 语料时,先用 Tiktoken 统计 token 分布,发现 12% 的样本超过 4,096 token,进一步排查出是爬虫抓取的整页 HTML。清洗后有效语料 token 总量下降 20%,训练效率明显提升。

Python 演示

python51 行
import refrom collections import Counter  def apply_merge(symbols, pair):    """把列表里所有等于 pair 的相邻两项合并成一项"""    out, i = [], 0    while i < len(symbols):        if i < len(symbols) - 1 and (symbols[i], symbols[i + 1]) == pair:            out.append(symbols[i] + symbols[i + 1])            i += 2        else:            out.append(symbols[i])            i += 1    return out  def train_bpe(corpus, num_merges=10):    """用正则预分词 + BPE 合并,返回合并规则与词表"""    pieces = re.findall(r'\w+|\s+', corpus)          # 预分词: 单词与空白分开    word_freq = Counter(pieces)                      # 统计每个片段出现次数    word_splits = {w: list(w) for w in word_freq}    # 每个片段先拆成最细字符    merges = []    for _ in range(num_merges):        pair_freq = Counter()        for w, freq in word_freq.items():            parts = word_splits[w]            for i in range(len(parts) - 1):                pair_freq[(parts[i], parts[i + 1])] += freq        if not pair_freq:            break        best = max(pair_freq, key=pair_freq.get)     # 找到最高频相邻对        merges.append(best)        for w in word_splits:            word_splits[w] = apply_merge(word_splits[w], best)    vocab = sorted({s for parts in word_splits.values() for s in parts})    return merges, vocab  def estimate_tokens(text, chars_per_token=4):    """按 4 字符一个 token 向上取整估算"""    return (len(text) + chars_per_token - 1) // chars_per_token  if __name__ == '__main__':    corpus = ('low ' * 200 + 'hello world ' * 200).strip()    merges, vocab = train_bpe(corpus, num_merges=8)    print('前 8 次合并:', merges)    print('最终词表示例:', vocab[:20])    sample = 'hello world low low'    print(f'估算 token 数: {estimate_tokens(sample)}')
目的:用约 60 行实现一个简化版 BPE 分词器,包含正则预分词、字节对统计合并与词表输出,并附一个 4 字符/token 的快速估算函数,帮你在本地理解 Tiktoken 的底层机制。
  1. pieces = re.findall(r'\w+|\s+', corpus)用正则预分词,把连续单词与空白切成独立片段,对应正文的“剪刀”
  2. word_freq = Counter(pieces)统计每个片段出现次数,后续给相邻对计数加权
  3. word_splits = {w: list(w) for w in word_freq}把每个片段拆成最细字符列表,这就是“256 基础件”的雏形
  4. pair_freq[(parts[i], parts[i + 1])] += freq给相邻对计数,关键是用片段频次加权
  5. best = max(pair_freq, key=pair_freq.get)每轮挑出最高频字节对,它就是本轮的“胶水”
  6. word_splits[w] = apply_merge(word_splits[w], best)把所有片段里出现的这对字节全部粘合成新符号
  7. vocab = sorted({s for parts in word_splits.values() for s in parts})汇总所有符号得到最终词表

常见误区

误区我以为分词就是把句子按空格切成单词,token 数量约等于单词数量。写 prompt 时数了下有 20 个词,就觉得只要 20 个 token。

正解BPE 会把常见单词合成一个 token,把不常见单词拆成子词,空格还常常被保留成“ world“这样的带前缀 token。比如 ”hello world“ 在 cl100k_base 里可能是 2 个 token,而 ”supercalifragilistic“ 会被拆成好几个。就像积木箱里既有”你好“整件,也有”我“和”你”小件,不能按空格数猜件数。

为何常见:人习惯按空格分词思考,而子词分词是数据驱动的最优拆法,二者天然不同。

误区我觉得既然文本被拆成了碎片再转成数字,信息一定有损,decode 可能还原不出原文,尤其是生僻字或表情符号。

正解字节级 BPE 保证了 decode(encode(s)) == s 恒成立。UTF-8 字节是“标准微粒”,任何文本拆到字节层都能原样拼回。就像积木拆成零件,只要编号齐全,一定能装回原来的模型。这也是 OpenAI 敢按 token 计费的信任基础。

为何常见:很多旧分词器确实有损或存在未登录词,用户把历史经验套到了字节级 BPE 上。

误区BPE 是从英文语料训练出来的,遇到中文、日文、阿拉伯文肯定会乱套,生成的 token 没意义。

正解字节级 BPE 的底座是 256 个 UTF-8 字节,英文、中文、emoji 在底层都是字节的组合。中文常用字经过合并后会形成独立 token,生僻字则退化为多个字节 token,但永远不会失败。GPT-4 的中文能力就建立在 cl100k_base 对中文的字节级覆盖之上。

为何常见:人们看到词表里大量英文子词,就误以为分词器不懂别的语言,没意识到字节层已经统一了所有文字。

误区我拿 `len(text) // 4` 算 token 数,觉得这就是 API 账单上的数字,从没怀疑过。

正解4 字符一 token 只是英文经验的粗略平均值。中文大约一字一 token,数字串、代码、空格密集的文本偏差很大。精确 token 数必须用 Tiktoken 的 encode 实测,经验公式只适合快速粗估。就像按字数猜运费,猜得再准也不如称重器。

为何常见:官方文档给出过 4 字符的近似值,初学者容易把“近似”当成“精确”。

面试题

Q1. Tiktoken 使用的核心分词算法是什么?

单选题 · easy

  • A.WordPiece 算法
  • B.BPE 字节对编码← 正确答案
  • C.SentencePiece 算法
  • D.按空格直接切分

答案:B。Tiktoken 正是 OpenAI 对 BPE 的高效工程实现。WordPiece 是 BERT 系常用的,SentencePiece 是 T5/LLaMA 系常用的,而直接按空格切分会丢失子词信息。用积木比喻,Tiktoken 拿的就是 BPE 规格表来拆单。

Q2. cl100k_base 词表的容量大约是?

单选题 · easy

  • A.大约 5 万个
  • B.约 10 万个← 正确答案
  • C.仅有 256 个
  • D.超过 100 万个

答案:B。cl100k_base 的精确容量是 100,256,其中 256 是基础字节表,剩下约 10 万来自 BPE 合并。5 万是许多早期模型的规模,256 只是字节基础表,100 万则远高于实际。记住"100k"这个系列名即可。

Q3. BPE 训练时,每一轮合并操作的依据是什么?

单选题 · mid

  • A.随机选一对相邻字节
  • B.出现频率最高的相邻字节对← 正确答案
  • C.词典中排序最靠前的词
  • D.长度最短的字节对

答案:B。BPE 每轮统计所有相邻对的频率,选最高的那对合并进词表。随机、按排序或按长度都不符合 BPE 的贪心统计本质。就像积木设计师只关心“哪两粒最常被拼在一起”,而不是哪粒最短。

Q4. 使用字节级 BPE 而不是字符级 BPE 的好处有哪些?

多选题 · mid

  • A.可以处理任意语言的文本← 正确答案
  • B.避免遇到未知词的情况← 正确答案
  • C.词表一定会变得更小
  • D.保证解码阶段无损还原← 正确答案

答案:ABD。字节级以 256 个 UTF-8 字节为底座,任何语言、任何符号都能编码,天然消灭 OOV,且 decode 可无损还原。但词表大小由合并次数决定,与是否字节级无关,所以 C 错。这就像统一了 256 种标准微粒后,仓库从不怕生僻原料。

Q5. GPT-2 分词流程中,预分词(pre-tokenize)的主要作用是什么?

单选题 · mid

  • A.按 BPE 合并规则直接切分文本
  • B.先用正则把文本切成片段再做 BPE← 正确答案
  • C.把文本编码成 UTF-8 字节序列
  • D.把 token id 映射回字符串

答案:B。预分词是 BPE 之前的一道工序,用正则按空格、标点、字母数字边界切分,让 BPE 只在片段内部合并。直接按 BPE 切是编码阶段的事,UTF-8 编码是另一层,id 映射回字符串是 decode。就像装箱前先分盒,再在盒内做优化。

Q6. 一段英文文本大约每多少个字符对应 1 个 token?

单选题 · mid

  • A.大约 1 个字符
  • B.大约 4 个字符← 正确答案
  • C.大约 10 个字符
  • D.大约 64 个字符

答案:B。经验统计表明,英文里 1 个 token 平均对应约 4 个字符,或者说 0.75 个英文单词。1 个字符一 token 过于碎片化,10 个和 64 个又明显高估了单词的信息密度。这个系数常用于 prompt 的快速成本粗估。

Q7. 关于特殊 token(如 <|endoftext|>),下列说法正确的是?

多选题 · mid

  • A.它们同样占用词表中的编号← 正确答案
  • B.普通文本中永远不可能出现
  • C.用于标记文本开始或结束等边界← 正确答案
  • D.解码时会被直接忽略丢弃

答案:AC。特殊 token 与普通 token 共享编号空间,A 对;它们承担边界标记作用,C 对。B 太绝对,编码器只是优先识别它们;D 错,decode 时它们依然会还原成对应字符串。就像积木箱里的“管理件”,平时不参与拼装,但管着箱子的起止标记。

Q8. 某 API 按每 1000 token 收费 0.002 美元,一次请求消耗 4,200 token,费用是多少?

单选题 · easy

  • A.0.008 美元
  • B.0.010 美元← 正确答案
  • C.0.012 美元
  • D.0.016 美元

答案:B。4,200 token 除以 1000 得 4.2,向上取整为 5,再乘 0.002 得 0.010 美元。这正是成本公式 $\lceil n/1000 \rceil \times P$ 的运用。选 A 的忘了向上取整,选 C、D 则是把 4.2 直接乘或错算。

Q9. 关于 Tiktoken 中 encode 与 decode 的关系,正确的是?

单选题 · hard

  • A.解码可能会丢失标点符号
  • B.任意合法文本都能无损还原← 正确答案
  • C.编码仅接受英文输入
  • D.解码输出的是词向量表示

答案:B。字节级 BPE 使 decode(encode(s)) == s 恒成立,标点、空格、emoji 都能还原,B 对。A 会丢失信息所以错;C 忽略多语言支持;D 混淆了 token id 与 embedding——解码输出的是字符串,不是向量。拆得再碎也能原样装回,这正是 Tiktoken 可靠之处。

Q10. 关于 token 数估算与成本控制,正确的说法有?

多选题 · hard

  • A.英文约四字符对应一个 token← 正确答案
  • B.中文约一个字对应一个 token← 正确答案
  • C.经验估算可以完全替代精确计算
  • D.prompt 越长消耗 token 越多← 正确答案

答案:ABD。A 和 B 都是行业公认的经验系数,D 是 API 计费的基本逻辑,三者都对。C 错,经验估算只能用于粗估,开发环境必须用 Tiktoken 精确统计,否则 prompt 模板、历史消息等隐性消耗会让账单失控。