Token 词元到底是什么?从原理到 Claude Code 的工程实践

LLM 如何看待文字,以及一个生产级 AI Agent 如何精确掌控上下文预算

April 3, 2026·5 min read·Yimin
#LLM#Token#Claude Code#上下文管理#AI Agent

你以为 token 就是"一个词"?实际上,一个中文字可能是 1-3 个 token,一个 emoji 可能吃掉 5 个 token,而 }}}} 却只算 1 个 token。理解 token 是用好 LLM 的第一步。

🎯 为什么你需要关心 Token?

如果你在用 ChatGPT、Claude 或任何 LLM API,token 与你息息相关:

维度Token 的影响
💰 费用API 按 token 计费,input token 和 output token 分开定价
📏 上下文窗口模型能"记住"的内容有上限,超了就会丢失信息或报错
⏱️ 延迟token 越多,推理越慢
🧠 质量上下文塞太满,模型注意力分散,回答质量下降
对于开发 AI Agent 的人来说,token 管理是核心架构问题 —— 你的 agent 在多轮对话中不断积累消息,不管理 token 预算,迟早会撞上下文墙。

🧠 Token 到底是什么?

不是字,不是词,是"子词片段"

Token 是 tokenizer (分词器) 把文本切出来的最小单元。现代 LLM 普遍使用 BPE (Byte Pair Encoding) 算法,核心思路是:

  1. 从单个字节开始
  2. 统计训练语料中哪些相邻字节对出现频率最高
  3. 把最频繁的字节对合并成一个新 token
  4. 反复迭代,直到词表达到预设大小 (通常 50k-200k) 结果就是:高频词被整个收编,低频词被拆成碎片。

直观感受

英文:
  "Hello world"      → ["Hello", " world"]          = 2 tokens
  "tokenization"     → ["token", "ization"]          = 2 tokens
  "anthropomorphize"  → ["anthrop", "omorph", "ize"] = 3 tokens
中文:
  "你好"             → ["你", "好"]                   = 2 tokens
  "人工智能"          → ["人工", "智能"]               = 2 tokens
  "中华人民共和国"     → ["中华", "人民", "共和国"]      = 3 tokens
代码:
  "console.log"      → ["console", ".", "log"]       = 3 tokens
  "}}}"              → ["}}}"]                        = 1 token
  "    "  (4空格)    → ["    "]                       = 1 token
特殊:
  "🎉"              → [多个字节 token]                ≈ 3-5 tokens

经验法则

语言大致比例
英文1 token ≈ 4 个字符 ≈ 0.75 个单词
中文1 token ≈ 1.5-2 个字符
代码1 token ≈ 3-4 个字符 (取决于语言)
JSON1 token ≈ 2 个字符 (大量 {, }, :, , 各占 1 token)

🏛️ API 返回的 Token 计数

当你调用 LLM API 时,response 里会附带 usage 字段。以 Anthropic API 为例:

{
  "usage": {
    "input_tokens": 15234,
    "output_tokens": 892,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 12000
  }
}
字段含义
input_tokens本次请求发送的全部内容的 token 数 (system prompt + tools + 所有 messages)
output_tokens模型本次生成的 token 数
cache_creation_input_tokens本次新写入缓存的 token 数
cache_read_input_tokens本次从缓存读取的 token 数
关键认知: input_tokens 不是"本轮新增",而是完整上下文。你发了 10 轮对话,第 10 次请求的 input_tokens 包含了前面所有 9 轮的消息。
这也意味着:
第 1 轮: input_tokens = 500   (system + 用户第一条消息)
第 2 轮: input_tokens = 1800  (system + 前两轮消息)
第 3 轮: input_tokens = 4200  (system + 前三轮消息)
...
input_tokens 是单调递增的 (除非你主动删了消息)

🔍 Claude Code 是如何计算 Token 的?

Claude Code 是 Anthropic 官方的 AI Coding Agent,它在多轮对话中需要精确掌控上下文预算。我阅读了它的源码,发现它的 token 计算策略非常务实。

第一层: 上下文窗口总大小 —— 查表

"模型最多能容纳多少 token?" 这个问题不需要运行时探测,Claude Code 用一个多级查找链:

优先级 (高 → 低):
1. 环境变量 CLAUDE_CODE_MAX_CONTEXT_TOKENS  (人工覆盖)
2. 模型名后缀 [1m]                          (显式 1M 上下文)
3. Model Capabilities API 缓存               (调 API 查询,缓存到本地)
4. Beta header 判断                          (是否开启了 1M beta)
5. 硬编码默认值 200,000

大多数情况下走第 3 步: Claude Code 启动时调用 Anthropic 的 Models List API,拿到每个模型的 max_input_tokens,缓存到 ~/.claude/cache/model-capabilities.json

第二层: 当前已用量 —— "精确基准 + 粗估增量"

这是最精妙的部分。Claude Code 不自己累加每轮的 token,而是用一个混合策略:

当前上下文占用 = 最近一次 API 响应的 usage (精确值)
              + 该响应之后新增消息的粗估 (近似值)

为什么这样设计?

  • 不累加: 如果你把每轮的 output_tokens 累加,第 2 轮的 input_tokens 已经包含了第 1 轮的 output,累加就重复计算了
  • 不纯粗估: 纯用字符数 / 4 估算整个对话,误差会随对话变长而放大
  • 取长补短: API 返回的 usage 是 server 端精确计算的结果,以它为锚点;只对"API 返回后到下一次 API 调用前"这个小窗口内的新消息做粗估,误差很小 用时序图来看:
                 API 调用 1         API 调用 2         API 调用 3
                    │                   │                   │
时间 ──────────────▶│                   │                   │
                    │                   │                   │
                    ▼                   ▼                   ▼
              usage 返回           usage 返回           usage 返回
              (精确锚点 A)         (精确锚点 B)         (精确锚点 C)
                    │                   │                   │
                    │  用户输入          │  tool 结果        │
                    │  tool 结果        │  用户输入          │
                    │  ← 这些做粗估 →   │  ← 这些做粗估 →   │
                    │                   │                   │
              估算 = A + 粗估     估算 = B + 粗估     估算 = C + 粗估

第三层: 粗估算法 —— 字符数除以常数

对于还没送去 API 的新消息,Claude Code 的粗估极其简单:

tokens ≈ 字符数 / 4    (通用文本)
tokens ≈ 字符数 / 2    (JSON,因为大量单字符 token)
tokens ≈ 2000          (图片,固定估值)

分 block 类型处理:

  • text → 文本长度 / 4
  • tool_use → 工具名 + JSON.stringify(input) 的长度 / 4
  • tool_result → 递归估算内容
  • thinking → thinking 文本长度 / 4
  • image / document → 固定 2000
  • 其他 block → JSON.stringify 后 / 4

第四层: 计算时机 —— 不是 Streaming 中,是 Stream 结束后

usage 字段在 stream 的 message_stop 事件中返回,不是 streaming 过程中可用的。所以 Claude Code 的计算时机是:

Agent Loop 每次迭代:
  1. [循环顶部] 用上一轮的 usage 锚点 + 新消息粗估 → 得到当前 token 估算
  2. [判断] 是否超过 auto compact 阈值?
  3. [调用 API] 发送请求,streaming 接收响应
  4. [stream 结束] 拿到新的 usage → 更新锚点
  5. [执行 tool] tool 结果追加到 messages
  6. → 回到步骤 1

⚙️ 阈值与预算管理

知道了"总大小"和"当前用量",就可以设阈值了。Claude Code 的计算:

有效上下文 = context_window - max(model_max_output, 20000)
                              ↑ 预留给模型输出的空间
auto compact 阈值 = 有效上下文 - 13000
                                  ↑ 额外 buffer,避免刚好压线

以 200k 模型为例:

context_window          = 200,000
预留输出空间             = 20,000
有效上下文              = 180,000
auto compact buffer    = 13,000
────────────────────────────────
auto compact 阈值      = 167,000  (≈ 83.5%)

当估算 token 数超过 167,000 时,Claude Code 就会自动触发压缩 —— 调 LLM 生成对话摘要,替换掉大部分历史消息。 实际状态划分:

0%          ████████████████████████████░░░░░░░░░░░░░░  83.5%   97%  100%
            │← 正常区域 →│                │← warning →│  │err│  │block│
                          auto compact ──→              ↑       ↑
                                              手动提醒  强制提醒

⚠️ 并行 Tool Call 的边界问题

Claude Code 源码中有一段很有意思的处理。当模型一次返回多个 tool_use 时,streaming 代码会把每个 tool_use 拆成独立的 assistant message (共享同一个 message.id),tool_result 交错插入:

messages 数组实际布局:
  assistant(id=A, tool_use_1)   ← usage 在这条
  user(tool_result_1)
  assistant(id=A, tool_use_2)   ← 同一个 API 响应
  user(tool_result_2)
  assistant(id=A, tool_use_3)   ← 同一个 API 响应
  user(tool_result_3)

如果只从最后一条 assistant(id=A) 开始粗估,就只会估到 tool_result_3,漏掉了 tool_result_1tool_result_2。 Claude Code 的解法: 找到有 usage 的 assistant message 后,回溯到同一 message.id 的第一条,从那里开始粗估,确保中间交错的 tool_result 都被覆盖。

📝 总结

问题答案
Token 是什么?BPE 分词器切出的子词片段,不等于字也不等于词
1 token 约多少字符?英文 ≈ 4 字符,中文 ≈ 1.5-2 字符,JSON ≈ 2 字符
上下文总大小哪来的?查表 (API 缓存 + 硬编码默认值 + 环境变量覆盖)
当前用了多少 token?最近一次 API 返回的 usage (精确) + 新消息字符数粗估
为什么不累加?每次 input_tokens 已包含全部历史,累加会重复计算
什么时候算?Stream 结束后拿到 usage,在 agent loop 每轮开头做判断
超了怎么办?超过阈值触发 auto compact (调 LLM 做摘要替换历史消息)
如果你在开发自己的 AI Agent,token 管理的核心就三件事:
  1. 记录每次 API 返回的 usage —— 这是你最准确的数据源
  2. 对新增消息做轻量粗估 —— 字符数 / 4 就够用
  3. 设合理阈值,留足 buffer —— 别等撞墙了才处理