LLM Token 原理
理解 token 是理解一切 LLM 优化的起点。
什么是 Token
LLM 不直接处理文字,而是处理 token —— 一种介于字符和单词之间的文本单元。
以 Hello, world! 为例,GPT 系列模型的 tokenizer 会将其拆分为:
| Token | 文本 |
|---|---|
| 15339 | Hello |
| 11 | , |
| 1917 | world |
| 0 | ! |
4 个 token,而不是 2 个"单词"或 13 个"字符"。
Tokenization 过程
主流 LLM 使用 BPE(Byte Pair Encoding) 算法:
将语料 "low lower newest widest" 拆分为单个字符作为初始 token。
这意味着常见词(the、function)是一个 token,而罕见词或中文会被拆成多个 token。英文平均约 4 字符/token,中文约 1.5 字符/token。
不同模型的差异
各模型家族使用不同的 tokenizer,同一段文字的 token 数可能差异 20-30%。这也是为什么 token 估算(如 RTK 的 len / 4)只能是近似值。
上下文窗口
上下文窗口(context window)是 LLM 单次对话能处理的 token 总量上限。
| 模型 | 窗口大小 |
|---|---|
| GPT-4o | 128K |
| Claude Opus 4 | 200K |
| Claude Sonnet 4 | 200K |
输入 vs 输出 token:输入 token(prompt)和输出 token(completion)共享窗口,但定价不同。输入通常是输出价格的 1/3 到 1/5,但占用的窗口空间完全相同。
窗口溢出:当输入超过窗口限制时,模型(或客户端)会截断最早的消息。在 agentic 场景下(如 Claude Code),这意味着工具调用结果和早期上下文会丢失——模型可能"忘记"之前做过什么。
为什么要缩减 Token
成本
Token 直接对应 API 费用。假设一个开发者每天通过 Claude Code 产生 50 万输入 token:
对团队来说,节省按人头线性增长。
速度
LLM 推理分两个阶段:
质量
这是最容易被忽视、但可能最重要的原因:
"Lost in the Middle" —— 研究表明,LLM 对上下文中间部分的内容关注度显著低于开头和结尾。当上下文中充满无关信息(如 git log 的 merge commit、Signed-off-by trailer),模型更容易忽略关键信息。
颜色深浅表示模型对该位置的注意力强度。开头和结尾的信息更容易被模型关注,中间部分容易被“遗忘”。
减少噪声 = 提升信噪比 = 更准确的输出。
缩减策略分类
在输入到达 LLM 之前裁剪无关内容
RTK规则化压缩,去除重复信息
opencode DCP 插件用小模型生成摘要,压缩后给大模型
LLM-based summarization从系统设计层面减少单次 token 用量
RAG / 滑动窗口 / 分层记忆预过滤
在系统命令输出到达 LLM 之前,根据预定义规则裁剪。不调用 LLM,零额外成本,延迟极低。
代表项目:RTK —— 拦截 git log、ls、cargo build 等 40+ 命令的输出,去掉冗余信息。
压缩/去重
分析对话历史,识别重复的工具调用结果并去重或压缩。同样基于规则,零 LLM 成本。
代表项目:opencode DCP 插件 —— 作为 coding agent 的中间件,分析对话历史中重复的工具调用结果并自动压缩。
摘要
用一个小型/快速的模型对长文本生成摘要,再把摘要喂给主模型。有额外 LLM 调用成本,但主模型的 token 用量大幅减少。
常见于:长文档问答、多轮对话记忆压缩。
架构级
从系统设计层面避免一次性加载大量 token:
- RAG:只检索相关片段而非全文
- 滑动窗口:只保留最近 N 轮对话
- 分层记忆:短期记忆(完整)+ 长期记忆(摘要)
Prompt Cache:复用而非缩减
除了减少 token 数量,还有一种互补策略:让重复的 token 更便宜。
什么是 Prompt Cache
大模型推理分为两个阶段:
- Prefill —— 处理输入的全部 token,生成每一层的 Key-Value(KV)状态。这是推理中最耗计算的部分,延迟与输入 token 数线性相关。
- Decode —— 基于 KV 状态逐 token 生成输出。
Prompt cache 的核心思想:缓存 prefill 阶段产生的 KV 状态。后续请求如果拥有相同的 prompt 前缀,可以直接复用已有的 KV cache,跳过 prefill 计算。
这些 KV 状态存储在 GPU 显存(或少数厂商的磁盘)中。GPU 显存是昂贵的稀缺资源,因此缓存不能无限保留——每条缓存都有一个 TTL(Time To Live),过期后被驱逐。主流厂商的默认 TTL 从 5 分钟(Anthropic)到数小时(DeepSeek)不等。
直觉上的矛盾
Token 缩减工具会修改发送给 LLM 的内容——压缩、去重、裁剪。而 prompt cache 要求前缀 100% 一致才能命中。
这看起来是矛盾的:每次修改上下文,都会导致 cache miss,似乎让 cache 失去意义。
为什么长期来看仍然划算
实际情况中,一次典型的 agentic 会话(如 Claude Code 处理一个开发任务)可能包含 50-200 轮交互。分析 cache 在整个会话生命周期中的行为:
1. 静态前缀占比高
一次会话中,system prompt + CLAUDE.md + 工具定义等不变的前缀通常占 10K-50K token。这部分每一轮都完全相同,是 cache 的稳定命中区。即使后续上下文被修改,前缀的 cache 命中仍然有效。
2. 缩减后的 cache 更高效
假设原始上下文 100K token,RTK 压缩到 40K:
- 未压缩:100K token 全价,即使 cache 命中 50%,仍为 50K 全价 + 50K 半价 = 75K 等价
- 压缩后:40K token,cache 命中 50%,为 20K 全价 + 20K 半价 = 30K 等价
缩减让 cache miss 的代价也变小了。
3. Cache miss 不是全有全无
即使后半段上下文发生变化,前缀部分的 KV 状态仍被复用。修改通常发生在上下文末尾(最新的工具调用结果),大部分 cache 仍然有效。
4. 成本算术
以 Anthropic Claude 为例(90% cache 读取折扣):
| 场景 | 每轮 token | Cache 命中率* | 等价成本 |
|---|---|---|---|
| 无优化 | 100K | 70% | 37K 等价 |
| 预过滤压缩 60% | 40K | 70% | 14.8K 等价 |
| 仅预过滤无 cache | 40K | 0% | 40K 等价 |
*表中 70% 命中率为假设值,实际取决于会话模式和 TTL 配置。90% 折扣基于 Anthropic 2026 年定价。
预过滤 + cache 叠加(14.8K)比单独使用任一方案都便宜。缩减降低基数,cache 降低单价,两者相乘。
与 Token 缩减的关系
总结:Token 缩减和 Prompt Cache 解决的是不同维度的问题:
- 缩减解决"发送了不需要的 token"——信噪比问题
- Cache解决"重复计算相同的 token"——冗余计算问题
两者不冲突,而是相乘关系。最优策略:先缩减(降低基数),再让 cache 发挥作用(降低单价)。
下一步
理解了原理,可以深入具体的实现探索:
- RTK:预过滤的 Rust 实现 —— 如何用 CLI 代理在毫秒级完成输出过滤