DeepSeek Harness · 上下文工程

Token 计量:决策用重放,展示用投影

两套计量各干各的,压缩决策从不信 UI 上那个数。核心源码:packages/llm/token-meter/src/usage-projection.ts

课程目标读完你能说清三件事:给压缩决策用的 token 数和给 UI 显示的 token 数为什么必须是两套;projectedTokens 那条公式为什么回答下一次请求的大小;以及占用率百分比的非原子性为什么是设计决策,连文档都替它写好了辩护词。
交互演示 · 双仪表台

先玩再讲。左表是重放实测 measure(),压缩决策读它。右表是 UI 投影 projectedTokens,状态行显示它。一场对话逐步推进:大工具输出、压缩、换模型、新请求。你会看到两表大部分时候贴得很近,然后在换模型那一步公开分叉,还理直气壮。

重放实测 · measure()决策用 · 压缩要不要动手,看这个数
0 0%
阈值刻度 80k · 分母 100k(决策方在自己边界现场解析)
UI 投影 · projectedTokens展示用 · 状态行上的占用率,是这个数
等第一个 usage 样本
分母 contextWindow 100k(来自最新一条 request/context 记录)
这种非原子性是有意为之,不是缺陷。确实需要同一边界精确数字的消费方,应在自己的请求边界调用 ctx.tokenMeter.measure(),那里两个值同时可得,而不是读取该投影。.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md 第 35 行,原文引用
点播放开始,或滚动到此处自动播放。
为什么是两套 · 两个问题,两种代价

先给结论:这两个数回答的问题不一样。压缩决策要回答此刻这个会话如果发请求会有多大,答案必须准,可以贵。UI 状态行只要给用户看个占用率,答案必须便宜、持久、重连后立刻能显示,准到小数点没有意义。DSH 干脆各修一条路,谁也不迁就谁。

决策用的数要准,可以贵。
展示用的数要便宜,可以糙。

决策这条路叫重放。ctx.tokenMeter.measure() 每次被调用,都把持久日志的当前尾部折叠成一份不可变快照:最近一次成功请求的 provider usage 若能匹配当前请求信封、且总量不低于它的完整启发式锚点,就拿来当锚;surface(模型可见表面)此后的增减用有符号 delta 重新定价;没有可复用的锚就整体按固定启发式定价。

代价也明明白白:每次调用 O(surface),所以只有压缩这样的决策方在自己的请求边界调它。

出处:docs/subsystems/token-meter.zh.md 对 baseline 两种取值(usage 锚 / estimated 启发式锚)的定义。

展示这条路叫投影。它是普通的持久会话投影状态,只有两个各自后者胜的字段:pressureTokens,最近一次请求报告的提示词侧规模,口径是输入加缓存读写、不含输出(usage-projection.ts 第 70 到 72 行);还有 contextWindow,来自最新一条 request/context 日志记录。分子分母各写各的,从不凑成一次原子观测。

持久会话日志 usage 样本 · surface 增减 request/context 记录 measure() 重放 锚点 + 有符号 delta · 每次 O(surface) 调用时现算,答下一次请求多大 投影 contextPressure pressureTokens · contextWindow 各自后者胜 O(1) 状态 · 重启重连后立即可用 压缩决策 阈值比较 · 溢出恢复 Web / TUI 状态行 占用率百分比 · 参考数字 准,但贵 糙,但便宜持久
教学化结构图:同一份持久日志分出两条计量路径,各自服务一个消费方,互不串门。
投影的巧思 · 答下一次,不答上一次

光有 pressureTokens 有个尴尬:它只在请求报 usage 时更新,Turn 流式期间一动不动,更看不见压缩。压缩替换了一大段 surface,状态行上的数却纹丝不动,用户会以为压缩没干活。所以 fold(折叠函数)顺手带一份 surface 的运行总量,公布的是样本加上此后 surface 的有符号变动。源码注释把意图写得很直白:occupancy answers for the next request rather than the last one,占用率回答下一次请求的大小(usage-projection.ts 第 150 到 161 行注释)。演示第 5 步就是这个效果:压缩刚落盘、一个新请求都没发,投影已经掉下来了。

还有一个时序细节:usage 样本在同一事件加入 surface 之前盖章(stamped BEFORE),所以 assistant/message 锚定的是它自己那次请求看到的 surface,增量的起点不会错位。

分子分母不构成原子对

换模型时,新 contextWindow 立刻生效,pressureTokens 还是上一个路由的旧样本。占用率此刻是近似值,直到下一个请求报 usage。文档明说这是取舍,不当 bug 处理。

pressureTokens 只算提示词侧

口径是 inputTokens 加缓存读写,不含输出。它描述的是发出去的请求有多大,和计费总量 tokenUsage 是两个投影单元,别混。

决策路径不读投影

Agent Note 原话:harness 中没有任何环节依据占用率百分比做决策,压缩直接读取 measure()。UI 上那个数再漂亮,也进不了决策函数的参数表。

关键证据 · 投影公式原文

整个投影的对外视图就这 7 行。第三个展开项是全课的题眼:pressureTokens + surfaceTokens - sampledSurfaceTokens,样本加上取样之后 surface 的净变动,再用 Math.max(0, …) 兜住下界。两个来源字段缺一个,对应的输出干脆不出现。

packages/llm/token-meter/src/usage-projection.ts第 198 至 204 行
  view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
    ...contextWindow === undefined ? {} : { contextWindow },
    ...pressureTokens === undefined ? {} : { pressureTokens },
    ...pressureTokens === undefined || sampledSurfaceTokens === undefined
      ? {}
      : { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) },
  }),
源码快照说明:依据本地仓库 deepseek-harness-master,核对文件 packages/llm/token-meter/src/usage-projection.ts,核对日期 2026-08-13。代码块保留源码原文。

两个边界条件顺着这段代码就能推出来。其一,provider 不回 usage:投影侧 pressureTokens 一直缺席,第二、三个展开项都不出现,UI 干脆不显示占用率(Agent Note 明确:只有压力与容量都已知时才显示占用率);决策侧不受影响,measure() 退化为 estimated 启发式锚,照常给数。其二,占用率的非原子分叉,文档的辩护词在演示第 6 步已经弹出来过,出处是 Agent Note 第 29 到 35 行,标题就叫「上下文占用率是近似值,而这正是决策本身」。

横向对比 · 一套数的活法和两套数的活法
Grok Build:一个数走天下

专门开了一个纯函数 crate 当唯一口径:bytes/4 启发式加派生显示运算,/context、/session-info、auto-compact 门、preflight 溢出检查和每个客户端渲染器全用它。决策和展示天生一个数,永远不打架。

代价是决策也只能用粗启发式:4 字节算 1 个 token(BYTES_PER_TOKEN = 4),图片一张记 765。该 crate 的模块注释自述为 bytes/4 启发式与派生显示运算的唯一事实来源。阈值判定用 u32 整数百分比加饱和乘法,细节与源码证据见站内已核对的 Token 使用率与阈值边界。provider 真实 usage 在这条口径里不参与占用率计算。

出处:grok-build-main crates/codegen/xai-token-estimation/src/lib.rs 第 3 至 19 行,核对日期 2026-08-13。

Claude Code:展示口径自己也要防坑

子 Agent 进度里的 token 计数分两个字段存:input_tokens 是 API 返回的逐轮累计值,只保留最新一份;output_tokens 是逐轮增量,累加。两个都直接相加就会把 input 重复计好几遍,显示虚高(书稿第 6 章引 restored-src ProgressTracker,study/chapters/06-task-system.md 第 169 至 181 行)。

压缩决策的阈值则用一组缓冲区常量表达,autocompact 预留 13000、手动 /compact 预留 3000(书稿第 3 章引 autoCompact.ts)。方向上同样是展示与决策各有口径,只是在已核对的公开材料里,未见这种明确让投影回答下一次请求的显式公式。

对比下来,三家其实答了同一道题:占用率这个数给谁用、错了谁买单。Grok 选了永远一致的粗数,Claude Code 给展示计数打了防重复的补丁,DSH 把两个消费方彻底分开,然后在文档里承认展示那份就是近似值。

课堂练习
01

手推一次分叉现场

压缩刚落盘,surface 从 92k 缩到 8k,还没有任何新请求;用户紧接着把模型从 100k 窗口切到 200k 窗口。此刻回答四个数:UI 状态行显示的分子和分母各是多少、来自哪个字段;压缩决策方如果此刻调 measure(),拿到的总量大约是多少、锚点是哪一类。最后用 Agent Note 第 35 行那句原文,解释这两组数为什么允许不相等、真需要精确值的消费方该怎么办。

Takeaway:决策用重放,measure() 在自己的边界现算,准而贵;展示用投影,pressureTokens 加 surface 净变动,糙而便宜持久。两个数可以不一样,占用率的非原子性是写进文档的设计决策。看到 UI 上的百分比,记住没有任何决策读它。