Token 計量:決策用重放,展示用投影
兩套計量各幹各的,壓縮決策從不信 UI 上那個數。核心原始碼:packages/llm/token-meter/src/usage-projection.ts。
projectedTokens 那條公式為什麼回答下一次請求的大小;以及佔用率百分比的非原子性為什麼是設計決策,連文件都替它寫好了辯護詞。
先玩再講。左表是重放實測 measure(),壓縮決策讀它。右表是 UI 投影 projectedTokens,狀態行顯示它。一場對話逐步推進:大工具輸出、壓縮、換模型、新請求。你會看到兩表大部分時候貼得很近,然後在換模型那一步公開分叉,還理直氣壯。
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 日誌記錄。分子分母各寫各的,從不湊成一次原子觀測。
光有 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, …) 兜住下界。兩個來源欄位缺一個,對應的輸出乾脆不出現。
view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
...contextWindow === undefined ? {} : { contextWindow },
...pressureTokens === undefined ? {} : { pressureTokens },
...pressureTokens === undefined || sampledSurfaceTokens === undefined
? {}
: { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) },
}),
packages/llm/token-meter/src/usage-projection.ts,核對日期 2026-08-13。程式碼塊保留原始碼原文。兩個邊界條件順著這段程式碼就能推出來。其一,provider 不回 usage:投影側 pressureTokens 一直缺席,第二、三個展開項都不出現,UI 乾脆不顯示佔用率(Agent Note 明確:只有壓力與容量都已知時才顯示佔用率);決策側不受影響,measure() 退化為 estimated 啟發式錨,照常給數。其二,佔用率的非原子分叉,文件的辯護詞在演示第 6 步已經彈出來過,出處是 Agent Note 第 29 到 35 行,標題就叫「上下文佔用率是近似值,而這正是決策本身」。
專門開了一個純函式 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。
子 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 把兩個消費方徹底分開,然後在文件裡承認展示那份就是近似值。
手推一次分叉現場
壓縮剛落盤,surface 從 92k 縮到 8k,還沒有任何新請求;使用者緊接著把模型從 100k 視窗切到 200k 視窗。此刻回答四個數:UI 狀態行顯示的分子和分母各是多少、來自哪個欄位;壓縮決策方如果此刻調 measure(),拿到的總量大約是多少、錨點是哪一類。最後用 Agent Note 第 35 行那句原文,解釋這兩組數為什麼允許不相等、真需要精確值的消費方該怎麼辦。