Skip to content

Tokenizer 与上下文窗口原理

这一章的目标不是让你记几个名词,而是让你能把"为什么这次上线突然更贵、更慢、更容易截断、更容易 JSON 失败"解释清楚。很多团队把 token 当计费单位,但在测试里,token 更像输入分辨率、上下文货币和稳定性上限的集合体。

教学导读

**定位:**这一章解决"为什么 token 会同时影响成本、延迟、截断、结构化输出和多轮记忆"的底层问题。 **前置依赖:**建议至少已经理解 Prompt、RAG、结构化输出和多轮会话的基本测试场景。 **适用场景:**Prompt 评测、JSON 抽取、Tool Calling、RAG 拼接、长上下文问答、多轮客服 Bot。 **学完产出:**你应该能手工拆一个请求的 token 预算,并把"为什么这次上线更贵更慢更容易截断"解释成可执行的测试方案。

先立一个判断。

对传统测试来说,输入长度更多是"边界值"的问题;对大模型测试来说,输入长度会直接改变模型看到世界的方式。你给模型的每一段文字、每一个工具 schema、每一轮历史消息,都在和其他内容竞争上下文窗口。竞争的结果不是"有或没有",而是"谁被压缩、谁被截断、谁更容易被忽略"。

01. Token 到底是什么

Token 不是字,也不是词,更不是字符。它更像模型词表里的"最小可识别碎片"。英文常见词可能一个 token 就够,中文一个字有时一个 token,有时多个字组合成一个 token,代码、URL、emoji、特殊符号常常会把 token 数迅速抬高。

字符视角

你看到的是"这个输入只有 300 个字"。

Tokenizer 视角

模型看到的是"这里有 700 个 token,且 schema 和历史消息已经占掉 500 个"。

计费视角

供应商按 token 计费,所以改一版 prompt 不只影响结果,也影响成本。

测试视角

token 数变化会影响延迟、截断、结构化输出成功率、函数调用稳定性和 RAG 命中质量。

1.1 为什么"字数差不多"却 token 差很多

输入类型表面长度token 风险原因
普通中文句子常见字可能较稳定,但专有名词、混合符号会抬升 token
中英混合 + 代码标点、路径、下划线、驼峰命名会被切得更碎
JSON Schema很高字段名、嵌套层级、引号、括号共同拉高 token 数
多轮历史消息很高不仅有内容,还有 role、tool 调用记录、系统说明

1.2 测试岗位最常见的误区

误区一:token 只是计费指标。

实际它同时还是延迟指标、截断风险指标、上下文污染指标和回归不稳定放大器。一个提示词改动如果把输入 token 从 1400 拉到 2600,你看到的通常不只是"更贵",而是首 token 慢、历史更容易被挤掉、JSON 更容易不闭合、RAG 上下文更容易被稀释。

1.3 从信息论看 Token 的本质

从信息论的角度,tokenizer 的目标可以用最小描述长度(Minimum Description Length, MDL)来理解。给定语料 C 和词表 V,总编码长度为: L(C, V) = |V| × log|V| + Σ_{t∈C} -log P(t|V) 第一项是词表本身的存储代价——词表越大,每个条目需要更多 bit 来编码;第二项是语料用这个词表编码后的总 bit 数。BPE 的 merge 操作本质上是在这两项之间做平衡:每一次 merge 增加 |V| 一个条目(增大第一项),但把高频 pair 变成单个 token,减小第二项。当边际收益(第二项下降)不再超过边际成本(第一项上升)时,merge 停止。 这解释了为什么现代大模型的词表通常在 32k-150k 之间——太小则编码效率低(同样的文本需要更多 token),太大则 embedding 矩阵膨胀且低频 token 的 embedding 学不充分。

02. 手撕 BPE:为什么一个词会被切碎

BPE(Byte Pair Encoding)可以粗略理解成"从字符开始,不断把训练语料里高频共同出现的片段合并成更大的单元"。它不是在追求语言学上的"词",而是在追求统计上高频、压缩效果好、对模型训练友好的切分方式。 一个极简 BPE merge 例子 c u s t o m e r

合并高频对:c + u → cu → st → st → omer → omer

cu st omer

2.1 真正值得测试人员关注的不是算法细节,而是结果特性

  • 高频片段更可能被合并成大 token,所以常见表述更省 token。
  • 冷门术语、随机字符串、日志片段、base64、长 URL 更容易被切碎。
  • 同样的意思,写法不同,token 数可能明显不同。
  • 你在系统里加入一段特别长的 schema 或工具说明,会吞掉本该给业务输入的上下文空间。

2.2 手算一个简单的 merge 过程

假设语料里高频对依次是 ("退","货")("系统","提示")("json","schema")。那么:

原始输入:退 / 货 / 流 / 程 / json / schema / 很 / 长
第一轮 merge:退货 / 流 / 程 / json / schema / 很 / 长
第二轮 merge:退货 / 流 / 程 / jsonschema / 很 / 长

结论:
1. 常见业务词"退货"会更紧凑。
2. 技术词"json schema"是否能被紧凑表示,要看词表是否收录高频合并。
3. 自定义字段名、随机订单号通常没有这种好处,token 更碎。

2.3 BPE 训练算法的完整伪代码

下面是 BPE 训练阶段的精确过程。理解它能帮助你预判哪些输入容易"token 膨胀":

python
def train_bpe(corpus: list[str], vocab_size: int):
    # 第一步:初始化词表为所有单字节(UTF-8 byte)
    vocab = {bytes([i]) for i in range(256)}
    # 将语料拆成 byte 序列
    splits = [list(word.encode("utf-8")) for word in corpus]

    while len(vocab) < vocab_size:
        # 第二步:统计所有相邻 pair 的出现频次
        pair_freq = Counter()
        for seq in splits:
            for i in range(len(seq) - 1):
                pair_freq[(seq[i], seq[i+1])] += 1

        if not pair_freq:
            break

        # 第三步:找到频次最高的 pair
        best_pair = max(pair_freq, key=pair_freq.get)

        # 第四步:合并这个 pair,生成新 token
        new_token = best_pair[0] + best_pair[1]
        vocab.add(new_token)

        # 第五步:在所有序列中执行替换
        for seq in splits:
            i = 0
            while i < len(seq) - 1:
                if (seq[i], seq[i+1]) == best_pair:
                    seq[i] = new_token
                    del seq[i+1]
                else:
                    i += 1

    return vocab, merge_rules

测试启发。

当你在评测集中设计边界场景时,不能只看"文本字数"。应该专门准备几类 token 不友好的输入:超长链接、带表情的用户提问、日志片段、路径、SQL 片段、base64、口语化错别字、中英混排。它们更容易暴露上下文预算和结构化输出的脆弱性。

02.3 四种 Tokenizer 算法完整推导与对比

现代 LLM 使用的分词算法可以归纳为四大家族。它们的核心差异在于:从哪里开始(字符 vs 词)、怎么选 token(频率 vs 概率)、怎么处理歧义(贪心 vs 全局最优)。理解这些差异对测试至关重要——不同算法对同一段文本会切出不同数量和粒度的 token,直接影响成本和截断行为。

自底向上

BPE 和 WordPiece 都从最小单元(字节或字符)出发,不断向上合并。

自顶向下

Unigram 从一个超大词表开始,逐步裁剪低价值 token。

预处理统一

SentencePiece 把整个输入当 raw byte stream,不依赖语言特定的空格分词。

BPE(Byte Pair Encoding)

前面已经手撕过基本流程。这里补充关键的数学性质:每一步 merge 选择的 pair (a, b) 满足: (a*, b*) = argmax_{(a,b)} freq(a, b) BPE 是纯粹的频率贪心算法。它的优势是实现简单、可复现、训练快;劣势是每一步的贪心选择不保证全局最优。GPT 系列、LLaMA、Mistral 均使用 BPE 变体。

WordPiece

WordPiece 由 Google 在 BERT 中使用,和 BPE 的核心区别在于合并准则。BPE 选频次最高的 pair;WordPiece 选的是使语言模型似然增益最大的 pair: score(a, b) = freq(ab) / (freq(a) × freq(b)) 这个公式本质上是互信息(Pointwise Mutual Information)的变体。当两个片段 a 和 b 总是一起出现而很少单独出现时,score 最高。这意味着 WordPiece 更倾向于合并语义上紧密绑定的片段,而不仅仅是统计高频的片段。 对测试的影响:同一段文本在 BERT tokenizer(WordPiece)和 GPT tokenizer(BPE)下的 token 数可能相差 10-30%。如果你的系统同时使用 BERT 做 embedding 和 GPT 做生成,token 计数必须分别统计。

Unigram Language Model

Unigram 采用完全不同的策略——自顶向下。它从一个大词表开始(通常包含所有出现过的子串,初始大小可能是百万级),然后逐步移除对语料编码代价影响最小的 token。 给定词表 V 和语料 C,Unigram 定义每个 token t 的概率为 P(t),一个词 w 的所有可能分词方式 S(w) 中,最优分词为: w* = argmax_{s∈S(w)} Π_{t∈s} P(t) 取 log: w* = argmax_{s∈S(w)} Σ_{t∈s} log P(t) 训练时使用 EM 算法迭代:E 步用 Viterbi 找每个词的最优分词,M 步重新估计 P(t),然后裁剪掉移除后 loss 增加最小的 token,直到词表缩小到目标大小。 Unigram 的独特优势在于它天然支持多种分词候选的概率采样,这在某些生成任务中可以引入有益的随机性(subword regularization)。T5、mBART 使用 Unigram。

SentencePiece:语言无关的统一框架

SentencePiece 不是一种新的分词算法,而是一个工程框架,可以在内部使用 BPE 或 Unigram。它的核心贡献是:

  • **无预分词:**不依赖空格来划分词边界,将输入视为 raw Unicode 流。这对中文、日文等无空格语言至关重要。
  • **可逆性:**空格被编码为特殊字符 ▁(U+2581),保证 detokenize 后能完美还原原始文本。
  • **字节回退(byte fallback):**任何 Unicode 字符即使不在词表中,也能通过 UTF-8 字节序列表示,永远不会出现 UNK。

四种算法的工程对比

维度BPEWordPieceUnigramSentencePiece
构建方向自底向上自底向上自顶向下框架(包装 BPE/Unigram)
合并准则频次最高似然增益最大EM + 裁剪取决于内部算法
UNK 处理byte fallback##前缀拆分概率回退byte fallback
代表模型GPT/LLaMA/MistralBERT/DistilBERTT5/mBART/ALBERTLLaMA/Gemma
中文友好度中(需要好的预分词)高(原生支持)
训练速度
分词确定性确定确定可随机采样取决于内部算法

测试落地建议。

在做跨模型对比测试时,务必注意不同模型使用的 tokenizer 家族不同。同一段 prompt 在 GPT-4(BPE, cl100k_base)和 Gemini(SentencePiece)下的 token 数可能差异 15-40%。你的 token budget 不能跨模型复用,必须分别测量。

02.5 为什么代码、URL、emoji 特别容易把 token 数打爆

对测试人员来说,这一节很实用,因为线上最容易低估的就是"看起来没多长"的技术型输入。日志、JSON、URL、文件路径、SQL、base64、emoji、Markdown 表格,都会让 token 数快速膨胀。

输入类型为什么容易膨胀测试风险
URL / 文件路径斜杠、点、问号、参数键值会被切成很多碎片history 或 tool 参数里很容易隐藏高 token 成本
JSON / 日志引号、括号、字段名重复、时间戳都增加分词碎片结构化抽取与日志分析场景成本和截断风险更高
代码片段驼峰、下划线、符号、缩进、泛型符号都不友好代码助手、SQL 分析容易首包变慢
emoji / 稀有符号Unicode 表达复杂,经常不是一个字符一个 token社交产品、UGC 评论场景要特别测
一个直观例子:
用户输入 A:请帮我总结这段退款规则
用户输入 B:请帮我总结这段退款规则,并分析这个 JSON:
{"bizCode":"refund_2026","trace_id":"8abf-9911-xx","payload":{"items":[...]}}

两者字数差距看起来不夸张,
但 B 的 token 往往会明显更高,
因为字段名、引号、括号、下划线、trace id 都会被切得更碎。

02.6 tiktoken 与 HuggingFace tokenizers 源码剖析

理解 tokenizer 的实现细节能帮助你定位"为什么线上和离线计数不一致"等诡异问题。这里剖析两个最常用的库。

tiktoken 架构:Rust 核心 + Python 绑定

tiktoken 是 OpenAI 开源的 BPE tokenizer 实现。它的性能秘密在于核心编解码逻辑用 Rust 编写,通过 PyO3 提供 Python 绑定。关键代码路径:

# Python 入口层 (tiktoken/core.py)
class Encoding:
    def encode(self, text: str) -> list[int]:
        # 调用 Rust 实现的 _tiktoken.CoreBPE
        return self._core_bpe.encode(text)

# Rust 核心层 (src/lib.rs) 简化逻辑
fn _encode_native(&self, text: &str) -> Vec<u32> {
    let mut tokens = Vec::new();
    // 第一步:用正则按预分词模式切分
    // GPT-4 的 cl100k_base 使用的正则:
    // (?i:'s|'t|'re|'ve|'m|'ll|'d)
    // |[^\r\n\p{L}\p{N}]?\p{L}+
    // |\p{N}{1,3}
    // | ?[^\s\p{L}\p{N}]+[\r\n]*
    // |\s*[\r\n]  |\s+(?!\S)  |\s+
    for piece in regex.find_iter(text) {
        // 第二步:在 merge 表中查找最优切分
        // 使用 BPE merge ranks 做贪心匹配
        match self.encoder.get(piece.as_bytes()) {
            Some(&token) => tokens.push(token),  // 完整匹配
            None => {
                // 第三步:逐步应用 merge 规则
                tokens.extend(byte_pair_merge(
                    piece.as_bytes(),
                    &self.encoder
                ));
            }
        }
    }
    tokens
}

关键设计决策的测试影响。

**预分词正则:**tiktoken 在执行 BPE 之前先用正则把文本切成"片段"。这意味着 BPE merge 永远不会跨越正则边界。例如 's 会被单独切出来而不会和前面的词合并。这解释了为什么 it's(2 tokens)和 its(1 token)的 token 数不同。 特殊 token 处理:<|endoftext|> 等特殊 token 在 encode 时有单独路径,默认的 encode() 不允许特殊 token 出现在普通文本中——这是一个安全边界,防止 prompt injection 通过特殊 token 操纵模型行为。

HuggingFace tokenizers 库关键路径

HuggingFace 的 tokenizers 库同样用 Rust 编写,设计为一个通用的 tokenizer pipeline,核心流程是五个可插拔阶段:

Normalizer → PreTokenizer → Model (BPE/WP/Unigram) → PostProcessor → Decoder

阶段职责测试关注点
NormalizerUnicode 归一化(NFC/NFD/NFKC)、大小写转换、去重音不同归一化策略下同一文本的 token 数可能不同;中文全角/半角转换
PreTokenizer按空白、标点等规则做初步切分中文无空格语言是否正确处理;代码中的特殊符号切分粒度
Model核心分词算法(BPE/WordPiece/Unigram)merge 规则的确定性、vocab 大小对 OOV 率的影响
PostProcessor添加 [CLS]、[SEP] 等特殊 token特殊 token 占用的 budget 是否被正确计入
Decoder将 token ID 序列还原为文本编解码往返一致性:encode(decode(ids)) == ids
# 验证 tokenizer 编解码一致性的测试脚本
from tokenizers import Tokenizer

tokenizer = Tokenizer.from_pretrained("bert-base-chinese")

test_cases = [
    "正常中文句子",
    "Hello World 混合",
    "emoji: 🎉🔥👨‍👩‍👧‍👦",
    "特殊符号: ≠ ≤ ∞ ® ™",
    "代码: def foo(bar: dict[str, Any]) -> None:",
    "",  # 空字符串
    " " * 100,  # 纯空格
]

for text in test_cases:
    encoded = tokenizer.encode(text)
    decoded = tokenizer.decode(encoded.ids)
    roundtrip = tokenizer.encode(decoded)
    assert encoded.ids == roundtrip.ids, \
        f"往返不一致: '{text}' → {encoded.ids} → '{decoded}' → {roundtrip.ids}"
    print(f"✓ '{text[:30]}...' → {len(encoded.ids)} tokens")

02.7 多语言 Tokenization 的 Fertility 问题

Fertility(繁殖率)是衡量一种语言在特定 tokenizer 下的编码效率的核心指标。定义为: fertility(lang) = token_count(text) / word_count(text) 英文文本的 fertility 通常在 1.2-1.5 之间(即平均每个英文单词需要 1.2-1.5 个 token),而中文可能高达 1.5-2.5(每个汉字需要 1.5-2.5 个 token),阿拉伯语甚至更高。这种差异直接导致:

成本不公平

同样 1000 个"词"的文本,中文用户可能支付英文用户 1.5-2 倍的费用,因为中文被拆成更多 token。

上下文容量不对等

128k token 的窗口,英文约能放 10 万个单词,中文只能放 5-7 万个汉字。同样的业务逻辑,中文 prompt 需要的 token 更多。

延迟差异

同一段话中文生成需要输出更多 token,TTFT 和总延迟都会高于英文。

截断位置不可预测

一个 UTF-8 编码的汉字可能跨越多个 byte token,如果在中间截断会产生乱码或不完整字符。

实测数据:同一段话在不同语言下的 token 差异

# 用 tiktoken 实测对比
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")

texts = {
    "英文": "The customer requested a full refund for order number 12345.",
    "中文": "客户要求对订单号12345进行全额退款。",
    "日文": "お客様は注文番号12345の全額返金を要求しました。",
    "阿拉伯语": "طلب العميل استرداد كامل للطلب رقم 12345.",
    "韩文": "고객이 주문 번호 12345에 대한 전액 환불을 요청했습니다.",
}

for lang, text in texts.items():
    tokens = enc.encode(text)
    ratio = len(tokens) / len(text)
    print(f"{lang}: {len(text)}字符 → {len(tokens)} tokens (ratio={ratio:.2f})")

# 典型输出:
# 英文:    59字符 → 13 tokens (ratio=0.22)
# 中文:    18字符 → 15 tokens (ratio=0.83)
# 日文:    23字符 → 22 tokens (ratio=0.96)
# 阿拉伯语: 42字符 → 24 tokens (ratio=0.57)
# 韩文:    28字符 → 26 tokens (ratio=0.93)

测试实践:多语言场景必须分语种做 budget 测试。

如果你的产品同时服务中英日韩用户,绝对不能只用英文 prompt 去验证 token budget。建议为每种主要语言分别建立 token 基线,并在回归测试中覆盖"最不友好"的语言(通常是日文和韩文)。特别注意:中日韩混合输入(如用户消息包含日文商品名 + 中文描述 + 英文型号)的 token 数往往比任何单一语言都高。

03. 上下文窗口为什么永远不够用

很多人以为"模型支持 128k 上下文"就等于问题解决了。企业真实场景里,窗口是被不同组件持续蚕食的:

system prompt → 工具 schema → 历史消息 → RAG 上下文 → 当前问题 → 预留输出

组成部分常见占用为什么危险
System Prompt300-1500 token改版本最频繁,却最容易被忽视成本影响
Tool / MCP Schema500-5000 token字段一多,业务输入可用空间立刻缩水
History随轮数飙升多轮一长,模型对早期信息的利用率下降
RAG Context1000-12000 token召回越多不一定越好,噪声也在占预算
Output Reserve200-2000 token不给输出预留空间,模型就更可能截断或格式不完整

3.1 一个企业请求的上下文预算示例

模型最大窗口:32,768 token
system prompt:1,200
tool schema:4,800
对话历史:6,500
RAG context:8,000
当前用户问题:600
输出预留:2,000

当前总预算:23,100
剩余安全空间:9,668

如果再追加 10 条检索片段,每条 700 token:
新增成本 = 7,000 token
剩余安全空间 = 2,668 token

这时风险:
1. 历史消息可能被截断
2. 输出空间不够,JSON 容易坏
3. 长上下文导致延迟显著升高

3.2 为什么"塞更多上下文"常常让结果更差

  • 更多片段意味着更高噪声,模型要在更多候选信息里分配注意力。
  • 如果检索片段之间互相矛盾,模型可能混合拼接出看似合理的答案。
  • 当工具 schema 很长时,真正的用户问题在总上下文中的权重会被相对稀释。
  • 长窗口并不等于长距离依赖始终稳定。越长的上下文,越需要测试"真正用上了没有"。
  • 研究表明模型在上下文中间位置的信息利用率最低("Lost in the Middle" 效应),关键信息应该放在开头或结尾。

03.3 RoPE 位置编码的完整数学推导

理解位置编码对测试的意义在于:它决定了模型"能看多远"的物理极限。当你测试长上下文场景时,截断和遗忘问题的根源往往在位置编码的外推能力上。

为什么需要位置编码

Transformer 的自注意力机制本身是置换不变的(permutation invariant)——打乱输入顺序,注意力分数不变。位置编码的作用是把"第几个 token"的信息注入模型,让模型知道谁在前谁在后。

RoPE 的核心思想:用旋转矩阵编码位置

RoPE(Rotary Position Embedding)的核心想法是:把位置信息编码为向量空间中的旋转角度。对于位置 m 处的 query 向量 q 和位置 n 处的 key 向量 k,RoPE 希望注意力分数只依赖于相对距离 m-n。 具体做法是:将 d 维的 q/k 向量两两分组,每组 2 维,对第 i 组应用一个旋转矩阵: R(θ_i, m) = [cos(mθ_i) -sin(mθ_i)] [sin(mθ_i) cos(mθ_i)]

其中 θ_i = 1 / 10000^(2i/d), i = 0, 1, ..., d/2 - 1 对完整的 d 维向量,RoPE 变换为: f(x, m) = R_Θ,m · x

其中 R_Θ,m = diag(R(θ_0, m), R(θ_1, m), ..., R(θ_{d/2-1}, m))

为什么旋转能编码相对位置

两个经过 RoPE 变换的向量的内积具有一个关键性质: ⟨f(q, m), f(k, n)⟩ = ⟨R_Θ,m · q, R_Θ,n · k⟩ = q^T · R_Θ,m^T · R_Θ,n · k = q^T · R_Θ,(n-m) · k

由于旋转矩阵的正交性:R^T_m · R_n = R_{n-m} 注意力分数只取决于 (n-m)——即相对距离,与绝对位置无关。这正是我们想要的性质。

基频 θ 与上下文长度的关系

θ_i 的取值控制了每一组维度的"旋转频率"。低频维度(大的 i)旋转慢,能感知远距离位置差异;高频维度(小的 i)旋转快,擅长区分近距离 token。base 值(默认 10000)决定了频率的分布范围。 当位置 m 超出训练时见过的最大值时,旋转角度 mθ_i 可能超出训练分布,导致注意力分数异常——这就是外推失败的根源。

对测试的直接影响。

如果你的业务场景需要处理超出模型训练长度的输入(比如用一个训练长度 4k 的模型处理 8k 输入),即使模型架构允许接受更长输入,位置编码的外推失败会导致"远处的信息看不到"——表现为模型对前面的指令失忆、回答只基于最近的几百个 token。测试时应该在不同位置放置关键信息,验证模型是否真的能利用远距离上下文。

03.4 长上下文方案对比:ALiBi / YaRN / NTK-aware

为了让模型支持更长的上下文,业界提出了多种位置编码扩展方案。它们的核心差异在于如何解决"训练时没见过这么远的位置"这个问题。

ALiBi(Attention with Linear Biases)

ALiBi 完全不使用位置编码,而是在注意力分数上加一个线性偏置: attention(q_i, k_j) = q_i · k_j^T - m · |i - j|

其中 m 是每个头特有的斜率,按 2^(-8/n_heads) 的等比数列递减 ALiBi 的思路简洁:距离越远,惩罚越大,自然衰减注意力。它天然支持外推到任意长度,但缺点是注意力严格随距离递减,对"远处有关键信息"的场景不友好。

位置内插(Position Interpolation, PI)

最直觉的方案:训练长度是 4k,想推理 16k,就把位置缩小 4 倍。原来的 position 0-16383 被映射到 0-4095.75: m' = m × (L_train / L_target)

缩放因子 s = L_train / L_target 优点是简单、只需少量微调;缺点是高频信息被压缩,近距离分辨率下降。

NTK-aware Interpolation

NTK-aware 方案不均匀缩放——低频维度(负责远距离)缩放多,高频维度(负责近距离)保持不变: θ'_i = θ_i / (α^(2i/(d-2)))

其中 α = (L_target / L_train)^(d/(d-2))

效果:高频维度 θ'≈θ(近距离精度保留),低频维度被大幅缩放(远距离容量扩展)

YaRN(Yet another RoPE extensioN)

YaRN 是目前效果最好的方案之一,它结合了 NTK 缩放、注意力缩放因子和温度修正:

  • **NTK 频率缩放:**和 NTK-aware 类似的非均匀缩放
  • **注意力缩放:**乘以 sqrt(1/s) 补偿因长度扩展导致的 softmax 分布变化
  • **分组处理:**将维度分为"不缩放区"、"线性内插区"和"NTK 缩放区"三组

方案对比表

方案是否需要微调近距离精度远距离能力推理开销代表模型
ALiBi理论无限极低BLOOM/MPT
PI少量有损s 倍扩展LLaMA-PI
NTK-aware否/少量Code LLaMA
YaRN少量很好很好Mistral/Qwen
原生长训练大量最好最好GPT-4/Claude

测试关注点。

当供应商宣称"支持 200k 上下文"时,你需要追问:是原生训练支持还是通过 PI/YaRN 扩展的?如果是扩展方案,在接近上限长度时应该专门做信息检索测试(如"大海捞针" needle-in-a-haystack),验证远距离信息是否真的能被利用。不同扩展方案在不同距离上的衰减曲线差异很大。

03.5 KV Cache 与上下文窗口的显存关系

KV Cache 是 Transformer 推理的核心优化:在自回归生成时,已经计算过的 Key 和 Value 矩阵被缓存,每个新 token 只需计算自己的 Q 并与所有缓存的 K/V 做注意力。但 KV Cache 的显存消耗与上下文长度线性增长,是限制实际可用上下文长度的物理瓶颈。

KV Cache 显存计算公式

KV Cache 大小 = 2 × n_layers × n_kv_heads × d_head × seq_len × bytes_per_param

对 LLaMA-2-7B (FP16): n_layers = 32, n_kv_heads = 32, d_head = 128, bytes = 2

4k 上下文: 2 × 32 × 32 × 128 × 4096 × 2 = 2 GB 32k 上下文: 2 × 32 × 32 × 128 × 32768 × 2 = 16 GB 128k 上下文: 2 × 32 × 32 × 128 × 131072 × 2 = 64 GB

对 LLaMA-3-70B (FP16, GQA 8 kv_heads): n_layers = 80, n_kv_heads = 8, d_head = 128, bytes = 2

8k 上下文: 2 × 80 × 8 × 128 × 8192 × 2 = 2.6 GB 128k 上下文: 2 × 80 × 8 × 128 × 131072 × 2 = 41 GB

GQA/MQA 如何压缩 KV Cache

传统 Multi-Head Attention(MHA)中每个注意力头都有自己的 K/V,KV Cache 与 n_heads 成正比。Grouped Query Attention(GQA)让多个 query head 共享同一组 K/V head,从而成倍降低 KV Cache。这是 LLaMA-3、Mistral 等模型能支持长上下文的关键工程决策。

注意力类型KV Head 数压缩比代表模型
MHA= query headsGPT-3, LLaMA-1
GQAquery heads / group_size4-8×LLaMA-2/3, Mistral
MQA1= n_heads ×PaLM, Falcon

为什么测试人员需要关心 KV Cache。

KV Cache 大小直接决定了:(1) 同一张 GPU 能并发多少请求——每个请求都独占自己的 KV Cache;(2) 当服务器 GPU 显存不足时,后到的请求可能被排队甚至 OOM,表现为延迟飙升或 5xx 错误;(3) 在压测场景下,你需要区分"模型计算慢"和"KV Cache 打满导致排队"两种延迟来源。监控指标:关注推理服务的 kv_cache_utilizationbatch_size 指标。

04. Token Budget 怎么设计才像企业方案

成熟团队不会只看"超不超窗口",而是会给每一类内容设预算上限。预算的意义不是节省几个 token,而是防止某个模块无限膨胀,把整个系统拖慢拖坏。

提示词预算

给 system prompt 设上限,例如不超过 1200 token,每次 prompt 变更都跑成本回归。

检索预算

限制 RAG top-k、chunk 长度和总拼接上限,防止"召回越多越安全"的错觉。

输出预算

结构化输出、长摘要、多轮 Agent 都要预留足够输出空间,否则容易尾部截断。

4.1 一个推荐的预算表

模块预算策略测试动作
System Prompt固定上限 + 版本对比每次改动记录 token 增量和通过率变化
History保留最近 N 轮 + 摘要压缩测试总结质量和记忆丢失风险
RAG Context总 token 上限 + 分段优先级测试 top-k 从 3 到 10 的效果差异
Tools按场景按需加载 schema测试"全量工具"与"按需工具"性能差异
Output基于任务类型预留JSON、长文生成、多轮任务分别跑截断测试

04.5 一个企业可直接落地的 Budget Worksheet

如果你要把 token budget 变成团队通用规则,最好的方式不是口头约定,而是给每个请求模板做一张"预算 worksheet"。下面是一个你可以直接照搬的表。 请求预算拆解图

最大窗口 32k → system 1.2k → tools 4.8k → history 6.5k → RAG 8k → 输出预留 2k

字段预算上限实测值是否超标备注
system prompt1200980正常
tool schema30004650需要按需裁剪工具
history50004200可接受
RAG context60007200top-k 和 chunk 需要下调
输出预留15001000风险JSON 场景容易截断

04.6 Token Budget 优化的工程实践:Streaming 与 Chunking

当单次请求无法在一个上下文窗口内完成时,工程上有两大类解法:流式处理(Streaming)和分块处理(Chunking)。这两种策略直接影响测试用例的设计。

Streaming(流式输出)

Streaming 不减少总 token 消耗,但改善用户感知延迟。Server-Sent Events(SSE)是最常见的协议:

# Streaming 模式下的 token 监控脚本
from openai import OpenAI
import time

client = OpenAI()
start = time.time()
first_token_time = None
total_tokens = 0

stream = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "请详细分析..."}],
    stream=True,
    max_tokens=2000
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        if first_token_time is None:
            first_token_time = time.time() - start
        total_tokens += 1

elapsed = time.time() - start
print(f"TTFT: {first_token_time:.2f}s")
print(f"总 token: {total_tokens}")
print(f"吞吐: {total_tokens/elapsed:.1f} tokens/s")
print(f"总耗时: {elapsed:.2f}s")

Streaming 测试要点。

  • **TTFT(Time to First Token):**流式下最核心的延迟指标。输入 token 越多,TTFT 越高,因为模型需要先处理完所有输入才能开始生成。
  • **流中断:**网络抖动或服务超时可能导致流中断,客户端收到的是不完整的 JSON 或 Markdown。测试应专门模拟流中断场景。
  • **token 粒度闪烁:**中文 token 可能对应不完整的 UTF-8 字节,某些实现会出现短暂乱码。

Chunking(分块处理)

当输入文本超过上下文窗口时,需要将文本切块后分次处理,再合并结果。关键决策点:

策略做法优势风险
固定大小切块按 token 数等分简单可控可能在句子中间切断,破坏语义
语义切块按段落/句子边界切分保持语义完整块大小不均匀,可能超窗口
滑动窗口块间有 overlap避免边界信息丢失重叠部分的 token 被重复消耗
Map-Reduce先分块摘要,再汇总适合长文档总结中间摘要可能丢信息
# 带 overlap 的智能 chunking 实现
import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

def smart_chunk(text: str, max_tokens: int = 3000, overlap: int = 200):
    sentences = text.replace("。", "。\n").replace(".", ".\n").split("\n")
    chunks = []
    current_chunk = []
    current_tokens = 0

    for sent in sentences:
        sent = sent.strip()
        if not sent:
            continue
        sent_tokens = len(enc.encode(sent))

        if current_tokens + sent_tokens > max_tokens and current_chunk:
            chunks.append("".join(current_chunk))
            # 保留最后几句作为 overlap
            overlap_tokens = 0
            overlap_sents = []
            for s in reversed(current_chunk):
                t = len(enc.encode(s))
                if overlap_tokens + t > overlap:
                    break
                overlap_sents.insert(0, s)
                overlap_tokens += t
            current_chunk = overlap_sents
            current_tokens = overlap_tokens

        current_chunk.append(sent)
        current_tokens += sent_tokens

    if current_chunk:
        chunks.append("".join(current_chunk))

    return chunks

05. 结构化输出为什么在长输入下更容易坏

结构化输出失败,很多时候不是模型"不会 JSON",而是上下文预算、schema 复杂度、输出保留空间共同叠加造成的。

一个典型故障链。

工具 schema 很长 → 当前请求还带了多段上下文 → 模型可用输出空间缩水 → 生成到一半被截断 → 最终 JSON 缺右括号或字段缺失。表面看像格式错误,本质上是 token budget 管控失败。

5.1 测试时要专门覆盖的 4 类场景

  1. 长 schema + 长上下文 + 长输出三者同时出现。
  2. 多工具注册,但当前只会用到一个工具。
  3. 同一个任务,在"短指令版 schema"和"详细版 schema"下的成功率差异。
  4. 输出接近上限时,是否出现字段缺失、数组被截断、转义错误。

5.2 最小测试脚本骨架

python
from openai import OpenAI

client = OpenAI()

def run_case(system_prompt, user_input, schema, max_output_tokens):
    return client.responses.create(
        model="gpt-4.1",
        input=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_input}
        ],
        text={
            "format": {
                "type": "json_schema",
                "name": "answer_schema",
                "schema": schema
            }
        },
        max_output_tokens=max_output_tokens
    )

# 真实测试里你要对比:
# 1. 不同 schema 长度
# 2. 不同 max_output_tokens
# 3. 不同 history 长度
# 4. 是否出现 JSON 不完整或字段空缺

05.3 Constrained Decoding 原理与 JSON Mode 约束

当 API 设置 response_format: {type: "json_schema"} 时,背后运行的不只是"希望模型输出 JSON",而是一套受约束的解码机制(Constrained Decoding)。理解它的原理能帮你诊断"为什么 JSON mode 有时反而更慢"。

基于有限状态自动机的 token 过滤

Constrained Decoding 的核心思想是:在每一步 token 生成时,根据当前已生成的内容和目标 schema,构建一个合法 token 掩码(mask),只允许模型从合法的 token 中采样。

JSON Schema → 构建 CFG/正则 → 转化为 FSA → 每步计算合法 token mask → mask 应用到 logits

具体过程:

  1. 将 JSON Schema 转换为上下文无关文法(Context-Free Grammar, CFG)或正则表达式
  2. 将 CFG/正则编译为有限状态自动机(Finite State Automaton, FSA)
  3. 在每一步生成时,FSA 根据当前状态计算所有合法的下一个 token(可能是几十到几千个),将不合法 token 的 logit 设为 -∞
  4. 模型只能从合法候选中采样,保证输出严格符合 schema

Constrained Decoding 的代价

代价类型原因测试影响
延迟增加每步需要额外计算 FSA 状态和 token maskTTFT 和每 token 延迟都可能升高 5-15%
质量可能下降强制约束可能让模型选择了概率次优但语法合法的 token字段值的语义准确率可能略低于无约束模式
schema 越复杂越慢FSA 状态空间与 schema 复杂度正相关嵌套 5 层的 schema 比扁平 schema 慢很多
枚举型字段加速enum 约束大幅缩小候选空间,FSA 状态少多用 enum 少用 freeform string 能提速

优化建议与测试策略。

(1) 尽量扁平化 schema,减少嵌套层级;(2) 用 enum 替代自由文本字段;(3) 将大 schema 拆分为多个小的结构化调用;(4) 对比测试:同一任务在 json_schema mode 和 text mode + 后处理解析两种方案下的延迟和准确率差异。在某些场景下,text mode + robust parser 可能是更好的选择。

05.5 最小 Token 计数脚本:别再靠感觉估

如果团队里还在用"这段提示词看起来不长"来判断成本和风险,建议直接把计数脚本放进开发流程里。下面给一个最小示例,重点不是库名,而是方法:把 system、history、schema、context 分别计数。

python
import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

def count_tokens(text: str) -> int:
    return len(enc.encode(text))

system_prompt = "你是一名企业客服助理..."
tool_schema = '{"type":"object","properties":{"order_id":{"type":"string"}}}'
history = "用户:我昨天问过退款...\n助手:..."
context = "退款规则:七天无理由..."

parts = {
    "system": system_prompt,
    "tool_schema": tool_schema,
    "history": history,
    "context": context,
}

total = 0
for name, text in parts.items():
    n = count_tokens(text)
    total += n
    print(f"{name}: {n}")

print("total:", total)

进阶:集成到 CI/CD 的 token 回归检测。

把 token 计数做成自动化检查,在 PR 合并前自动对比 system prompt 和 tool schema 的 token 增量。当增量超过阈值(如 10%)时自动标记为需要 review。

# CI 集成示例:prompt 变更的 token 回归检测
import tiktoken
import json
import sys

enc = tiktoken.get_encoding("cl100k_base")

def load_prompt(path: str) -> str:
    with open(path) as f:
        return f.read()

def check_token_regression(old_path: str, new_path: str, threshold: float = 0.1):
    old_text = load_prompt(old_path)
    new_text = load_prompt(new_path)
    old_tokens = len(enc.encode(old_text))
    new_tokens = len(enc.encode(new_text))
    delta = (new_tokens - old_tokens) / old_tokens

    report = {
        "old_tokens": old_tokens,
        "new_tokens": new_tokens,
        "delta_pct": f"{delta*100:.1f}%",
        "status": "PASS" if abs(delta) <= threshold else "FAIL"
    }
    print(json.dumps(report, indent=2))

    if abs(delta) > threshold:
        print(f"Token 增量 {delta*100:.1f}% 超过阈值 {threshold*100}%", file=sys.stderr)
        sys.exit(1)

# 用法: python token_check.py prompts/v1.txt prompts/v2.txt
if __name__ == "__main__":
    check_token_regression(sys.argv[1], sys.argv[2])

06. 企业里最常见的 5 类 token 故障

故障类型表面现象底层原因修复方向
提示词升级后费用暴涨单次请求成本翻倍system prompt 和 tool schema 同时膨胀拆分公共指令、按需加载工具
多轮对话失忆早期约束失效历史被截断或注意力利用率下降滚动摘要 + 关键记忆提炼
JSON 偶发失败同 case 有时过有时不过输出预算边缘化,复杂场景下截断缩 schema、加输出预留、缩上下文
RAG 召回一多就答非所问明明给了更多上下文却更差噪声片段挤占 token 与注意力削减 top-k、做 rerank、控总 token
Agent 工具调用慢TTFT 变差全量工具 schema 被塞入上下文场景化注册工具

6.1 故障定位的三步法

当线上出现 token 相关故障时,推荐以下定位流程:

  1. **拆解请求:**把实际发送给 API 的完整 payload 拿出来,分别计算 system / tools / history / context / output 各部分的 token 数。
  2. **对比基线:**和正常工作时的 token 分布做对比。通常某个部分会有显著膨胀(最常见的是 history 或 RAG context)。
  3. **复现边界:**找到导致故障的 token 阈值,固定其他变量,只改变可疑部分的长度,二分查找临界点。
# 故障定位脚本:解析实际请求的 token 分布
import tiktoken
import json

enc = tiktoken.get_encoding("cl100k_base")

def analyze_request(payload: dict):
    breakdown = {}
    total = 0

    for msg in payload.get("messages", []):
        role = msg["role"]
        content = msg.get("content", "")
        if isinstance(content, list):
            content = json.dumps(content)
        tokens = len(enc.encode(str(content)))
        breakdown.setdefault(role, 0)
        breakdown[role] += tokens
        total += tokens

    if "tools" in payload:
        tool_tokens = len(enc.encode(json.dumps(payload["tools"])))
        breakdown["tools"] = tool_tokens
        total += tool_tokens

    breakdown["total"] = total
    breakdown["max_tokens"] = payload.get("max_tokens", "unset")

    for k, v in breakdown.items():
        print(f"  {k}: {v}")

    return breakdown

07. 测试设计清单:这一章学完后应该怎么测

建议把 token 测试从"附加项"升级成"固定回归项"。

  1. 为核心提示词建立 token 基线,每次变更记录增减量。
  2. 对长上下文任务建立预算表,明确 system/history/RAG/output 各自上限。
  3. 准备一组 token 不友好的真实输入:URL、日志、乱码、emoji、SQL、base64、长 JSON。
  4. 对结构化输出单独做"接近上限"的截断测试。
  5. 对多轮会话做记忆衰减测试,确认关键指令能保留多少轮。
  6. 把 token、费用、TTFT、输出完整性放到同一张趋势图里观察。
  7. 对不同语言做 fertility 基线测试,确保多语言场景的 budget 覆盖。
  8. 对 KV Cache 做压力测试,观察高并发下的延迟退化曲线。

07.5 Tokenizer 完整测试用例设计

一个成熟的 tokenizer 测试套件应该覆盖以下维度。这些测试用例不仅验证 tokenizer 本身的正确性,更重要的是发现业务场景中的 token 风险。

边界 Case 测试矩阵

类别测试输入验证目标预期行为
空值"" , " " , "\n\n"空输入不崩溃返回空列表或仅含特殊 token
超长单行100k 字符无换行不 OOM、不超时正常分词,token 数与字符数合理比例
纯符号!@#$%^&*(){}[]特殊符号不产生 UNK每个符号至少对应一个 token
Unicode 边界U+0000, U+FFFF, U+10FFFF, 代理对不崩溃、不产生非法序列byte fallback 正确处理
混合编码中文+日文+韩文+阿拉伯文+emoji多脚本共存不互相干扰各语言 fertility 在合理范围

Emoji 专项测试

Emoji 是 tokenizer 的重灾区。单个"可见字符"在 Unicode 层面可能由多个 code point 组成:

# Emoji token 测试用例
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")

emoji_cases = {
    "简单 emoji":      "😀",           # 1 code point
    "肤色修饰":        "👍🏽",          # 2 code points (base + modifier)
    "ZWJ 家庭":       "👨‍👩‍👧‍👦",  # 7 code points (4 人 + 3 个 ZWJ)
    "国旗":           "🇯🇵",          # 2 regional indicator
    "keycap":         "1️⃣",           # 3 code points
    "连续 10 个 emoji": "🎉🔥💯🚀✨🎯🏆💡🎮🎨",
}

for name, emoji in emoji_cases.items():
    tokens = enc.encode(emoji)
    codepoints = len(emoji.encode("utf-32-le")) // 4
    print(f"{name}: {repr(emoji)}")
    print(f"  code points: {codepoints}, tokens: {len(tokens)}")
    print(f"  token IDs: {tokens}")
    print()

代码分词测试

# 代码片段的 token 效率测试
code_cases = {
    "Python 函数": "def calculate_total_price(items: list[dict]) -> float:",
    "Java 泛型":   "Map<String, List<Optional<Integer>>> result = new HashMap<>();",
    "SQL 查询":    "SELECT u.name, COUNT(*) FROM users u JOIN orders o ON u.id=o.user_id GROUP BY u.name HAVING COUNT(*)>5;",
    "正则表达式":   r"^(?:(?:\+|00)86)?1[3-9]\d{9}$",
    "JSON 嵌套":   '{"a":{"b":{"c":{"d":{"e":"deep"}}}}}',
    "Base64":      "SGVsbG8gV29ybGQhIFRoaXMgaXMgYSB0ZXN0Lg==",
    "文件路径":     "/usr/local/lib/python3.11/site-packages/tokenizers/__init__.py",
    "URL 参数":    "https://api.example.com/v2/search?q=hello+world&lang=zh&page=1&size=20&sort=relevance",
}

for name, code in code_cases.items():
    tokens = enc.encode(code)
    ratio = len(tokens) / len(code)
    print(f"{name}: {len(code)}字符 → {len(tokens)} tokens (ratio={ratio:.2f})")

往返一致性测试(Round-trip Consistency)

任何 tokenizer 都应该满足 decode(encode(text)) ≈ text。注意某些 normalizer 可能改变空白字符,所以不一定是严格相等。

# 往返一致性批量测试
import random
import string

def generate_adversarial_inputs(n=100):
    inputs = []
    for _ in range(n):
        # 随机混合中英文、符号、空白
        chars = []
        for __ in range(random.randint(1, 500)):
            r = random.random()
            if r < 0.3:
                chars.append(random.choice(string.ascii_letters + string.digits))
            elif r < 0.5:
                chars.append(chr(random.randint(0x4e00, 0x9fff)))  # CJK
            elif r < 0.7:
                chars.append(random.choice("!@#$%^&*(){}[]<>:;'\",./\\|`~"))
            elif r < 0.85:
                chars.append(random.choice([" ", "\t", "\n", "\r"]))
            else:
                chars.append(chr(random.randint(0x1f600, 0x1f64f)))  # emoji
        inputs.append("".join(chars))
    return inputs

test_inputs = generate_adversarial_inputs(1000)
failures = []

for text in test_inputs:
    try:
        encoded = enc.encode(text, allowed_special="all")
        decoded = enc.decode(encoded)
        re_encoded = enc.encode(decoded, allowed_special="all")
        if encoded != re_encoded:
            failures.append({"text": text[:50], "reason": "re-encode mismatch"})
    except Exception as e:
        failures.append({"text": text[:50], "reason": str(e)})

print(f"测试 {len(test_inputs)} 条, 失败 {len(failures)} 条")
for f in failures[:5]:
    print(f"  {f}")

性能基准测试

Tokenizer 性能直接影响推理链路延迟。

在高 QPS 场景下,tokenizer 的编码速度可能成为瓶颈。建议用以下维度建立性能基线:(1) 短文本(<100 字符)的单次编码耗时应 <0.1ms;(2) 长文本(>10k 字符)的编码吞吐应 >1MB/s;(3) 批量编码 1000 条应 <1s。如果使用 Python 实现的 tokenizer(而非 Rust binding),性能可能差 10-50 倍。

08. 练习题

8.1 思考题

  1. 为什么"把 schema 写详细一点"有可能提高正确率,也有可能降低稳定性?
  2. 为什么同样 500 字的两段文本,在 token 上可能差别很大?
  3. 为什么长 history 不一定让多轮体验更好?
  4. BPE 和 Unigram 在处理"从未见过的新词"时有什么行为差异?哪种更适合代码场景?
  5. 为什么 GQA 能大幅降低 KV Cache 而对模型质量影响有限?
  6. Constrained Decoding 在什么情况下反而会降低输出质量?

8.2 手算题

假设一个请求窗口上限是 16,384 token,system 占 900,history 占 4,200,RAG context 占 6,000,当前问题占 300,输出至少要留 1,200,请问:

  1. 当前剩余安全空间是多少?
  2. 如果工具 schema 再增加 2,500 token,会先影响哪个部分?
  3. 如果必须保留输出空间,你会优先压缩 schema、history 还是 RAG context?为什么?

8.3 KV Cache 计算题

一个模型有 40 层、32 个注意力头(其中 KV head 用 GQA 缩减为 8 个)、每个头维度 128、使用 FP16。请计算:

  1. 单个请求在 4096 token 上下文下的 KV Cache 大小是多少 MB?
  2. 如果 GPU 有 24GB 显存,模型权重占 14GB,最多能同时服务多少个 4096 token 的请求?
  3. 如果将上下文扩展到 32768 token,最多能同时服务多少个请求?

8.4 参考答案要点

  • **8.2.1:**总已占用是 900 + 4200 + 6000 + 300 + 1200 = 12600,因此剩余安全空间是 3784 token。
  • **8.2.2:**如果 schema 再增加 2500 token,最先被挤压的往往是输出保留空间和优先级较低的 history / RAG 片段。
  • **8.2.3:**企业方案通常先压缩可改写的 schema,再压缩低价值 history,最后才动高价值 RAG 证据,因为证据最直接影响正确率。
  • **8.3.1:**KV Cache = 2 × 40 × 8 × 128 × 4096 × 2 bytes = 2 × 40 × 8 × 128 × 4096 × 2 = 671,088,640 bytes ≈ 640 MB。
  • **8.3.2:**可用显存 = 24 - 14 = 10 GB = 10,240 MB。每请求 640 MB,最多 10240/640 = 16 个并发。
  • **8.3.3:**32768 token 的 KV Cache = 640 × 8 = 5120 MB ≈ 5 GB,最多 10240/5120 = 2 个并发。这就是为什么长上下文场景吞吐量骤降。

8.5 编码实战题

用 tiktoken 或 HuggingFace tokenizers 完成以下任务:

  1. 写一个函数,输入任意文本,输出该文本在 cl100k_base 和 o200k_base 两种编码下的 token 数差异。
  2. 构造一段中英混合文本,使得 cl100k_base 下的 token 数恰好比 o200k_base 多 50% 以上。
  3. 写一个 token budget 检查器:接收完整的 API 请求 payload,检测各组件的 token 占比并生成预警报告。
# 实战题参考框架
import tiktoken

def compare_encodings(text: str):
    enc_100k = tiktoken.get_encoding("cl100k_base")
    enc_200k = tiktoken.get_encoding("o200k_base")

    tokens_100k = len(enc_100k.encode(text))
    tokens_200k = len(enc_200k.encode(text))
    diff_pct = (tokens_100k - tokens_200k) / tokens_200k * 100

    return {
        "text_preview": text[:80],
        "cl100k_tokens": tokens_100k,
        "o200k_tokens": tokens_200k,
        "diff_pct": f"{diff_pct:+.1f}%"
    }

test_texts = [
    "Hello, how are you today?",
    "你好,今天天气怎么样?",
    "def foo(bar: dict[str, list[int]]) -> Optional[bool]:",
    "👨‍👩‍👧‍👦 Family emoji test 🎉",
    "SELECT * FROM users WHERE created_at > '2026-01-01' ORDER BY id DESC LIMIT 100;",
]

for t in test_texts:
    result = compare_encodings(t)
    print(result)

8.6 自测标准

完成这一页后,你应该能做到:

  • 用自己的话解释 token、BPE、上下文预算三者关系。
  • 说清楚 BPE、WordPiece、Unigram、SentencePiece 四种算法的核心差异及其对 token 数的影响。
  • 看到一个长 prompt 时,能主动拆成 system / history / tools / RAG / output 预算。
  • 把 JSON 输出失败、延迟上升、历史失忆和 token budget 联系起来。
  • 解释 RoPE 位置编码的基本思想,以及长上下文方案(PI/NTK/YaRN)的取舍。
  • 计算给定模型配置下的 KV Cache 大小,评估并发容量。
  • 设计一套包含边界 case、多语言、emoji、代码、往返一致性的完整 tokenizer 测试套件。
  • 设计一套最小可落地的 token 回归测试,并集成到 CI/CD 流程。