前言
上一篇文章讲了 JSON mode + Pydantic + Token 计算三层防线。这篇把 Token 计算这条防线拆开——不只是”算成本”,而是”怎么算、为什么算不对、怎么越算越准”。
一、为什么要管上下文
每轮对话,system prompt + 历史消息 + 用户输入都得打包发给模型。20 轮对话后,这个消息包轻松超过 10 万 token。不加管理的话:模型直接拒答或静默截断;每轮都重复计费同样的 system prompt,纯浪费。
我的 e2e 实测:构造 80 轮假对话,输入 143953 token,trim 裁到 101538,成本 ¥0.20。没有 TokenBudget,80 轮对话直接爆 128K 窗口。
二、三次迭代:从 /4 到 json.dumps
第一次:字符 /4
够用吗?够——trim 只需要知道”大概超了没”,数量级对就行。但做精确计费时,中文 /4 严重低估 2-9 倍。
第二次:tiktoken 精确计数
精度大幅提升,但漏了东西——role 标记、引号、花括号。模型实际收到的是序列化后的完整 JSON,不是纯 content。
第三次:json.dumps 全量序列化
384 条消息的 JSON 结构开销大约是 1800 token——content-only 完全忽略。
三种方案的实测对比(80 轮假对话):
| 方案 | input token | 误差 |
|---|---|---|
| /4 | 240,553 | 英文重复字符严重高估 |
| tiktoken content-only | 142,137 | 漏了 JSON 结构开销 |
| tiktoken json.dumps | 143,953 | 最接近真实 |
三次迭代从”能用就行”到”知道自己差在哪”到”精确到 JSON 序列化字节级”。
三、TokenBudget:三个方法的职责边界
count:计算消息 token 数。trim 触发判断用 /4 够用,成本精确计费上 tiktoken。到后续上云端 API 时必须全量切——差 2 倍的 prompt token 就是差 2 倍的月度账单。
is_within_budget:max_output 参数是容易被忽视的关键——输入 90K,预留 30K 给模型回复,加起来超预算线——也算超。返回 (bool, 当前用量, 剩余额度) 三元组,调用方不需要再调一次 count。
trim:三条规则。保留 system 消息(丢了等于 agent 失忆)。user/assistant 从最新开始保留(对话场景里最新消息才是当前问题的上下文)。system 自己就超了预算——不抛异常,静默返回。
四、一个修了四遍的 bug
trim 循环里的”先检查后追加”,我一共改了四版才修对:
第一版:按消息条数切片——直接废稿。
第二版:追加后检查,多一条才停——超预算。
第三版:追加前检查,但查的是追加前的总量,不是”加了这条之后的总量”——仍然超一条。
第四版:先累加、再判断、后追加。这三步的顺序不能换。
举一反三:LRU 缓存淘汰、流控窗口、分页预加载——所有”加不加”的循环决策,陷阱都一样:你检查的是”现在的总量”还是”加上它之后的总量”。
五、Ollama 不告诉我用了多少 token
非流式调用 Ollama,拿到的 usage 长这样:
1CompletionUsage(completion_tokens=0, prompt_tokens=0, total_tokens=0)结构体有,数据没有——Ollama 的 OpenAI 兼容层不填 usage。
解法:RouterClient 里加零值检测,fallback 到字符估算。不要假设任何 provider 会给你正确的 usage——凡是外部数据,先校验非零。
六、前端视角的全景映射
| 概念 | 前端 | Agent |
|---|---|---|
| context window | V8 heap limit | 模型上下文上限 |
| token count | bundle analyzer | TokenBudget.count |
| trim | code splitting / lazy load | 丢弃旧消息 |
| 预算线 | Lighthouse budget | budget_ratio=0.8 |
| usage 缺失 | PerformanceObserver 不可用 | Ollama 返回全零 → fallback |
| 先检查后追加 | Array.push 前检查容量 | 循环内先累加再追加 |
结论
前端优化的本质是”在用户设备上省资源”,Agent 优化的本质是”在模型推理上省 token”。逻辑一样:先测量、再预算、后裁剪。
从 /4 到 json.dumps,每一步误差缩小都不只是数值更准——是越来越接近”模型真正看到了什么”。上下文管理的终点不是”省了多少钱”,是”你知道每轮对话的 token 从哪来、到哪去、为什么值这个价”