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. 五个常见计数失败
- 只计算正文:忽略 role、name、tool call、JSON 和工具 schema,系统性低估。
- tokenizer 和模型不匹配:数值看似精确,实际模型版本错误。
- 计数失败返回 0:把“无法测量”伪装成“完全不占空间”。
- 忽略供应商包装:SDK 或服务端模板导致估算偏差。
- 复用旧误差:压缩、工具集合和模型变化后,上一轮误差不再适用。
计数器异常时,安全策略应拒绝发送、切换保守估算器或缩小任务。吞掉异常继续调用,会让预算保护形同虚设。
7. 失败注入
做三个实验:
- 让
count_message()故意只计算content,构造空正文 tool call,确认测试能发现低估; - 让计数器抛出异常,验证 Harness 不会继续发送请求;
- 输入
estimated=100, actual=120, output=20, window=128,确认within_window为 false。
第三个实验提醒我们:供应商最终接受了请求,不代表应用预算正确;实际总量超出配置边界仍要进入告警和策略校准。
8. 生产替换清单
- 计数器绑定模型和 tokenizer 版本;
- 统计完整消息、工具 schema、附件和响应格式;
- 记录估算值、实际值和误差分布;
- 模型或 SDK 升级后重跑容量回归;
- 计数服务不可用时有明确拒绝或降级策略;
- 日志只保存必要统计与来源 ID,不泄露完整敏感 prompt。
9. 本章验收
完成后你应能解释:为什么同样 1000 个字符不能使用同一个 token 系数;为什么空 content 的工具调用仍然占空间;为什么 tokenizer 不可用时不能返回 0;以及安全余量应该如何由真实误差数据产生。