KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

02. Token 怎样被可靠测量? — keel 龙骨

上一章得到 127 个动态预算,但这个数字只有在计数可信时才有意义。支付服务诊断里出现了中文工单、英文日志、JSON 工具参数和工具 schema;如果统一使用“字符数除以四”,误差可能刚好把一次请求推过边界。

上一章得到 127 个动态预算,但这个数字只有在计数可信时才有意义。支付服务诊断里出现了中文工单、英文日志、JSON 工具参数和工具 schema;如果统一使用“字符数除以四”,误差可能刚好把一次请求推过边界。

Token counting 看起来只是工具函数,实际上是预算、限流、计费、压缩触发和容量告警共同依赖的测量基础。测量错误后,后面的优先级设计再精致也没有意义。

1. 为什么字符数不能直接换算 token

下面三段文本长度接近,token 分布却可能完全不同:

支付服务仍然超时
payment service timeout
{"incident_id":"INC-42","include_logs":true}

差异来自 tokenizer 词表、语言、空格、标点、代码、JSON 转义和模型消息模板。同一段文本在不同模型版本上也可能得到不同数量。多模态输入、缓存 token 和 reasoning token 还可能使用额外计量规则。

因此要区分两种计数:

字符估算可以存在,但必须在名称、事件和文档中明确标为 estimate,不能伪装成供应商 token 事实。

2. 计数对象不是一段字符串

一次工具型消息至少包含:

{
  "role": "assistant",
  "content": "",
  "tool_calls": [{
    "id": "run-42:step-3:call-0",
    "name": "search_incident",
    "arguments": {"query": "payment"}
  }]
}

如果只统计 content,结果会是 0,但供应商仍要处理 role、call ID、工具名、参数 JSON 和消息包装。系统提示、工具定义和响应格式也可能在每一轮重复发送。

本课程把计数接口收敛为三个动作:

class TokenCounter(Protocol):
    def count_text(self, text: str) -> int: ...
    def count_message(self, message: Message) -> int: ...
    def count_messages(self, messages: Sequence[Message]) -> int: ...

适配器负责把内部 Message 转换为目标请求,再按该模型的规则计数。这个小接口固定的是责任,不是假装所有供应商格式相同。

3. 看懂教学计数器

项目中的 CharTokenCounter 使用 ceil(len(text) / chars_per_token) + message_overhead,并把工具调用稳定序列化为 JSON:

tool_text = json.dumps(
    [{"id": call.call_id, "name": call.name, "arguments": call.arguments}
     for call in message.tool_calls],
    ensure_ascii=False,
    sort_keys=True,
)

sort_keys=True 只是为了让教学测试可重复。这个实现能证明“工具参数不能漏算”和“计数器可替换”,不能证明某个真实模型的 token 数。

运行预算示例:

python courses/foundation/context-engineering/course/project/examples/01_measure_and_budget.py

输出中的 system/tool schema: 16 15 来自同一个计数器。若把工具描述写得更长,tool_schema_budget 会立即增大,动态材料随之减少。

4. 正确的测量顺序

不要先把所有材料拼成一个巨大 prompt,再在末尾猜长度。推荐逐层报告:

目标模型与 tokenizer 版本
  -> 系统规则
  -> 本轮实际工具 schema
  -> 当前任务
  -> 动态候选逐项计数
  -> 输出预留和余量
  -> 最终请求预检

装配报告至少能够解释每类开销:

{
  "counter": "provider-tokenizer@model-version",
  "estimated_input": 6120,
  "by_kind": {
    "system": 430,
    "tool_schema": 910,
    "current_task": 180,
    "evidence": 2600,
    "history": 2000
  },
  "input_budget": 7000,
  "remaining": 880
}

发生丢弃或超限时,复盘者才能知道空间被谁使用,而不只看到 context too long。

5. 预检估算和实际 usage 必须对账

发送前只能得到估算;请求结束后,供应商可能返回实际 input、output 和 reasoning usage。项目中的 UsageReconciler 把两者放进同一报告:

report = UsageReconciler.reconcile(
    estimated_input=90,
    actual_input=100,
    output_tokens=20,
    reasoning_tokens=5,
    window_limit=128,
)

结果是:

estimation_error = 10
total_actual = 125
within_window = True

单次误差不能直接决定余量。应在真实请求上统计绝对误差和相对误差的 P50、P95、最大值,并按模型版本、语言、工具数量和多模态类型分组。安全余量来自这批数据,而不是永远固定 10%。

OpenAI token counting 指南展示了按完整 Responses 输入形态计数的方式;其他供应商需要使用各自 tokenizer 或计数接口。课程不把某一家接口抽象成所有模型的统一事实。

6. 五个常见计数失败

  1. 只计算正文:忽略 role、name、tool call、JSON 和工具 schema,系统性低估。
  2. tokenizer 和模型不匹配:数值看似精确,实际模型版本错误。
  3. 计数失败返回 0:把“无法测量”伪装成“完全不占空间”。
  4. 忽略供应商包装:SDK 或服务端模板导致估算偏差。
  5. 复用旧误差:压缩、工具集合和模型变化后,上一轮误差不再适用。

计数器异常时,安全策略应拒绝发送、切换保守估算器或缩小任务。吞掉异常继续调用,会让预算保护形同虚设。

7. 失败注入

做三个实验:

  1. 让 count_message() 故意只计算 content,构造空正文 tool call,确认测试能发现低估;
  2. 让计数器抛出异常,验证 Harness 不会继续发送请求;
  3. 输入 estimated=100, actual=120, output=20, window=128,确认 within_window 为 false。

第三个实验提醒我们:供应商最终接受了请求,不代表应用预算正确;实际总量超出配置边界仍要进入告警和策略校准。

8. 生产替换清单

9. 本章验收

完成后你应能解释:为什么同样 1000 个字符不能使用同一个 token 系数;为什么空 content 的工具调用仍然占空间;为什么 tokenizer 不可用时不能返回 0;以及安全余量应该如何由真实误差数据产生。

下一章:预算应该给谁?

进入 keel 阅读