16 KiB
Grok Bot 的 token 計數與 context window 用量,以及重建 harness 的對應做法
对象:/Applications/Grok Bot.app(package name sand,Electron 40.1.0,asar 1027 entries,integrity verified)
分析目標:dist/electron-main/main-app.cjs、dist/local-exec-daemon/main.cjs、dist/node-agent-coordinator/main.cjs
相關 finding:fnd_5、fnd_6、fnd_7、fnd_9、fnd_12、fnd_16
§0 標記規則(先讀這節)
本文每條陳述都標明等級:
- 觀察:在目標檔案裡直接讀到的欄位名、常數、字面值。一律附掃描範圍——「掃了哪個檔、找了哪些關鍵字」。「沒找到」只等於「在該範圍、用該關鍵字沒找到」,不等於「不存在」。
- 推論:從觀察算出來/歸納出來的結論(例:記帳在伺服器端)。
- 推測:最合理的猜測,沒有直接證據。
- 作者設定:重建 harness(
app/)裡我自己選的數字,與 Grok Bot 無關。不要把這些數字當成 Grok Bot 的事實。
引證方式:本文引 finding id(fnd_*),因為已入庫 finding 的 evidence 連結在入庫時已綁定並驗證。跨輪的 ev_* id 不可信任(實測會 Unknown evidence id),所以本文只在有同輪 evidence 的地方列 ev_*:本輪的两個 id 是檔案完整性檢查(見 §11 末)。
§1 核心結論:Grok Bot 客戶端沒有 token 計數器;記帳在伺服器端
推論(依據下列觀察 + fnd_5/fnd_6)。
觀察(範圍:dist/electron-main/main-app.cjs,找這 9 個欄位名):usage、prompt_tokens、completion_tokens、total_tokens、token_count、context_window、max_tokens、tokens_used、usage_ratio → 0 命中;符號索引中含 token 的 147 筆全部是憑證/密閉字串(token = credential),不是計數。
觀察:客戶端有完整的一組 byte 上限與压缩引數(§2、§5),但看不到把字串換算成 token 的通用估算器被使用:
estimateMessagesTokenCount這個符號存在(約main-app.cjs@2743065 附近),但它的算術在觀察到的位址不可見(minified、未展開)。→ 有名字、無實現細節。
觀察(與結論一致的正面證據):token 用量是從伺服器事件流裡取出來的,不是客戶端算的:
compactionSummaryFromEvent、applyCompactionSummaryToTimeline、applyCompactionSummaryFromEvent、turnUsageBreakdownFromEvent、usageFromEvent、onUsage、usageLabel、usageLabelForTurn、reportUsage、turnSteer + tokenBudgetsForTurn。
這與 fnd_5/fnd_6 完全對得上:文字代理迴圈、prompt 組裝、模型路由與 context 管理都在遠端 box/gateway(api2.cursor.sh 系),客戶端只是顯示者與工具執行者(fnd_7)。
範圍限制:只掃了上述欄位名,且 fnd_9 已記錄字串表被截斷。所以嚴格的說法是:「在 main-app.cjs 的 token 計數欄位掃描中未見客戶端計數器;記帳欄位一律以 event 名出現」,不是「客戶端絕對沒有 token 數學」。
§2 客戶端唯一的 token 換算:字元數 / 4(僅限語音逐字稿)
推論:在觀察範圍內,客戶端唯一看得見的 token 除法在語音通話路徑上:
voiceCallTranscriptCharsPerToken = 4.0(觀察)- 算式形如
ceil(charLen / charsPerToken)(觀察) - 預算
voiceCallTranscriptTokenBudget = 12000(觀察)
推論:這是「ASCII 約 4 字元 = 1 token」的粗估,只用來控制逐字稿長度,不是整個對話的 context 管理。
作者設定(重建做法):我的 harness 不內嵌 tokenizer(零相依,不帶 gpt-tokenizer/js-tiktoken)。規則:ASCII byte → 1 token,非 ASCII byte → 0.5 token。這是我選的,Grok Bot 只用 4.0 字元/token 於語音逐字稿。
§3 真正的權威值:讀 provider 回來的 usage
觀察:Grok Bot 的用量數字來自伺服器事件(§1 的 usageFromEvent / turnUsageBreakdownFromEvent / onUsage)。
作者設定(重建做法):不自己數,讀 provider 回傳的 usage。OpenAI-compatible 串流的最後一個 SSE chunk 帶 usage.prompt_tokens / usage.completion_tokens / usage.total_tokens(請求側要發 stream_options: {"include_usage": true})。規則:
- 每個 LLM 事件流結束時取最後一個帶
usage的 chunk 當權威值; - 佔用率 =
usage.total_tokens / contextWindow,寫入事件流(tokens_used+source: "provider"); - provider 沒給
usage時才退回本地估計,並在事件上標est.,UI 顯示~; - 估計器與權威值並存,永不互相覆蓋——這樣才知道估計誤差。
§4 每工具的輸出上限:用 byte 和行列卡,不用 token
觀察(範圍:dist/local-exec-daemon/main.cjs,fnd_7 的工具實作):
| 工具 | 上限欄位 | 值 |
|---|---|---|
read_file |
maxBytes |
預設 131072(128 KiB) |
readTerminalFile(背景 shell 輸出) |
maxBytes |
65536(64 KiB) |
ripgrep |
maxResults |
200 |
ripgrep |
timeoutMs |
20000 |
推論:Grok Bot 控制 context 膨脹的主要手段是在工具邊界卡 byte 與條數,讓單次工具結果不可能撐爆 context,而不是把工具結果送去數 token。
作者設定(重建做法):同樣策略——每個工具都有 maxBytes + 行/列上限,回傳時附 truncated: true 與「如何取下一页」的提示(offset/cursor)。我的數值與上表相同(131072 / 65536 / 200 / 20000 直接採用,因為它們是 Grok Bot 的觀察值,屬於可借鑑的設計常數)。
§5 壓縮觸發點:Grok Bot 用「比例」,而且是 70 %
觀察(範圍:dist/electron-main/main-app.cjs)——本報告唯一直接來自 Grok Bot 的數字組:
client_speculative_summarization_config = {
enabled: true,
thresholdRatio: 0.70,
toleranceRatio: 0.05,
modelId: "grok-4.5"
}
speculativeSummarizationClientGates = { enabled, modelTier: "default", contextRatio: 0.70 }
推論:
thresholdRatio: 0.70= context 用量到 70 % 觸發(推論:欄位語意由名字與contextRatio同名值一致支持,非文件證明)。toleranceRatio: 0.05= 5 % 容差 → 觸發後压到 65 % 以下(0.70 - 0.05)。0.70 與 0.05 是觀察;「压到 65 %」是把容差讀成遲滯目標的推論。modelId: "grok-4.5"= 做摘要的模型由客戶端設定送出去(與 fnd_12 的「客戶端擁有設定、推送到 box」同形)。- 「speculative」(投機式)+ 客戶端 gate(
enabled、modelTier)→ 推測:摘要是在回合進行中/之前預先算好,需要時直接採用,避免在關鍵時刻卡一次摘要請求。這是對命名的解讀,無行為證據。
觀察:压縮結果以 compactionSummary 的名字從伺服器事件送達,並套用到 timeline(§1)→ 摘要在事件流裡是一等公民(可重放、可重算 timeline),不是就地改寫訊息歷史。
作者設定(重建做法):採用同一對引數——觸發 70 %,目標 65 %(遲滯,避免在臨界點反覆壓縮)。這是我從 Grok Bot 借鑑的唯一一組數值;其他比例都是我的選擇。
§6 壓什麼:位元組上限講得很清楚
觀察(範圍:dist/electron-main/main-app.cjs):
| 欄位 | 值 | 附註 |
|---|---|---|
summarizationInputTokenLimit |
360000 | 碼內註解寫「bytes (ASCII=1 token)」 |
summarizationInputByteLimit |
360000(預設 300000) | |
summarizationInputBytes |
120000 | |
summarizationInputTokenLimitCap |
0 = 停用 | 上限的上限 |
summarizationOutputCap |
6000 | 摘要輸出 |
MAX_SUMMARY_BYTES |
100 × 1024 | |
summarizeMessages |
函式名 |
推論:
- 全鏈路都以 byte 為單位;「token」在客戶端只是「ASCII byte」的別名(註解自己寫了 ASCII=1 token)。
- 輸入有 300 000–360 000 byte 的送件上限,實際一次用 120 000 byte;輸出摘要 ≤ 6000(單位未標,推測為 token 上限)、單條摘要 ≤ 100 KiB。
- 因為摘要由伺服器產生(§1),這些限制更像是客戶端決定送多少歷史去摘要。
作者設定(重建做法):我的 compress 事件記錄 beforeTokens / afterTokens / modelUsed / strategy;摘要本身是 append-only 事件,timeline 由「最近 N 條原文 + 之前所有摘要」重組;保留最後一輪完整不压,避免模型忘記自己剛剛在做什麼。
§7 context 爆了怎麼辦:413 的分類與一次重試
觀察(fnd_9,範圍 main-app.cjs):provider 錯誤 taxonomy 含 provider_413_context_too_large,以及 provider_400_invalid_request、_401_auth_rejected、_404_not_found、_429_quota_exceeded、_4xx_rejected、_5xx_terminal、_image_rejected、_user_blocked、NonRetryableProviderError。
推論:413 被当成可處理事件(有自己的代號),不是笼統的 4xx;NonRetryableProviderError 與 _5xx_terminal 存在 → 重試策略是依代號分類的。
作者設定(重建做法):收到 413 → 強制壓縮一次(不等到 70 %)→ 重發一次;第二次還失敗就帶著錯誤終止回合,並在事件流記 provider_413_context_too_large。429 讀 retry-after;4xx 不重試(對應 fnd_9 的 non-retryable 分類)。
§8 別把「配額」讀成「context window」
觀察(範圍:dist/electron-main/main-app.cjs):token_budget = 100000、token_budget_per_user = 30000、token_budget_per_user_7d = 100000、token_budget_per_team、usage_warning_threshold = 0.8、usage_limit_floored。
推論:per_user / per_user_7d / per_team 這種按使用者/團隊/7 天視窗的分桶是計費與配額語彙,context window 不會按使用者分桶。usage_warning_threshold = 0.8 是「用量到 80 % 要示警」的配額示警。
结论:Grok Bot 的 context window 大小不在客戶端(找不到 context_window,§1)。客戶端看到的 token_budget* 是配額。
作者設定(重建做法):兩個數字必須分開。--context-window(我預設 8000,CLI 可蓋)控制壓縮;配額是另一個概念,我的 harness 不做配額(沒有伺服器)。usage_warning_threshold = 0.8 這個「80 % 示警」我沿用為 CLI 警示條的閾值(借用其形式,非借用其語意)。
§9 用量要能對到回合
觀察(範圍:dist/electron-main/main-app.cjs):turnUsageBreakdownFromEvent、usageLabelForTurn、tokenBudgetsForTurn、turnSteer、reportTurnClientOutcome、reportRenderTtfr。
推論:token 用量按回合歸屬(per-turn breakdown),而且客戶端會回報自己這端的結果(reportTurnClientOutcome)與首字延遲(reportRenderTtfr)→ 伺服器是記帳中心,客戶端是觀測點之一。turnSteer + tokenBudgetsForTurn 一起出現 → 推測:每回合有 token 預算,steering(中途插入指示)時會重算。
作者設定(重建做法):事件流裡每條 llm.usage 事件帶 turnId;/usage 指令印出 per-turn 表格(每回合 prompt/completion/total + 佔 context 百分比 + 是否 est.)。我的 harness 不做遠端上報。
§10 讓它看得見:CLI 與 UI 的可見性
觀察:客戶端有 usageLabel / usageLabelForTurn → 推論:UI 上有專用的用量字串(每回合一條)。
作者設定(重建做法):
- CLI 每個回合後印一行
tokens 1234/8000 (15%), provider 值直接顯示,估計值前綴~。 - 超過
--context-window的 80 % 時在同一行尾加警示條[████░░],並在 compress 事件發生時印compacted 4100 → 1500。 --print模式结束時印總計。
§11 數字來源對照表(最重要的一節)
| 數字 | 屬於誰 | 出處與標記 |
|---|---|---|
0.70(thresholdRatio)、0.70(contextRatio) |
Grok Bot 用戶端 | 觀察:client_speculative_summarization_config / speculativeSummarizationClientGates(main-app.cjs)。這是本報告唯一直接來自 Grok Bot 的比例數字。 |
0.05(toleranceRatio) |
Grok Bot 用戶端 | 觀察:同上欄位。 |
modelId: "grok-4.5"(做摘要的模型) |
Grok Bot 用戶端 | 觀察:同上欄位(模型清單由伺服器提供,見 fnd_9 / unk_dd4ee2d6bc4685b5 已解)。 |
360000 / 300000 / 120000 / 6000 / 100×1024 byte 上限、MAX_SUMMARY_BYTES |
Grok Bot 用戶端 | 觀察:summarizationInput*、summarizationOutputCap、MAX_SUMMARY_BYTES(main-app.cjs)。單位是 byte(檔內註解:ASCII=1 token)。 |
| 131072 / 65536 / 200 / 20000(每工具上限) | Grok Bot 用戶端 | 觀察:dist/local-exec-daemon/main.cjs(fnd_7)。 |
| 4.0 chars/token、12000 | Grok Bot 用戶端(僅語音逐字稿) | 觀察:voiceCallTranscriptCharsPerToken、voiceCallTranscriptTokenBudget。 |
| 100000 / 30000 / 100000 / 0.8 | Grok Bot 用戶端,但是「配額」不是 context window | 觀察:token_budget*、usage_warning_threshold(§8 推論)。 |
| 65 %(压縮目標) | 推論 + 作者設定 | 由 0.70 − 0.05 得出的推論;作為我 harness 的固定目標值则是作者設定。Grok Bot 碼內沒有「65 %」這個字面值。 |
| 72 000、100 000(重建檔內出現的 context 尺寸範例) | 作者設定 | 我舉的例子,與 Grok Bot 無關。 |
8000(--context-window 預設) |
作者設定 | 我的 CLI 預設值。Grok Bot 客戶端沒有 context window 數字(§1 掃描 0 命中)。 |
| 80 %(CLI 警示條閾值) | 作者設定(形式借鏡 usage_warning_threshold = 0.8) |
語意不同:Grok Bot 那個是配額示警。 |
| ASCII 1 token / 非 ASCII 0.5 token 的估計器 | 作者設定 | 我的 harness 規則;Grok Bot 只在語音逐字稿用 4.0 chars/token。 |
一句話:只有 0.70 / 0.05 這一對壓縮引數,以及一組 byte/char 上限,是從 Grok Bot 用戶端觀察得來的;其餘任何 token/context 數字都是我自己的設定或範例。
本輪檔案完整性檢查(觀察,同輪 evidence):GROK_BOT_TOKENS.md 10785 B、GROK_BOT_FEATURES.md 20998 B,sha256 db9f4c11b1ea18855eaac90d0eb15feea6000da92029516b28e02c285772661b / fd753cf232f6959e0bb63cd813bc26a8b55753ae16ac1174aa03f247df1f82d3(ev_c091620b0c5bc68527e432b2d02d6fe80c4d5ac7d1bd928d49f0b80db8c63174、ev_2f4ca1c5d683d7ad93da7316b4fd07eac9100f2bbff0f9ce166269c4de03cf58;入庫為 fnd_16)。
§12 方法與限制
方法:靜態分析。asar 清單(1027 entries,integrity verified)→ js_app_map 套裝圖 → 對 dist/electron-main/main-app.cjs、dist/local-exec-daemon/main.cjs、dist/node-agent-coordinator/main.cjs 做符號/欄位名掃描。沒有執行目標,因此本文所有陳述都只證明「碼裡寫了什麼」,不證明「跑起來會怎樣」。
限制(逐條):
- 「找不到 token 計數欄位」的範圍是 main-app.cjs + 列出的 9 個欄位名;fnd_9 已記錄字串表被截斷,fnd_6 已記錄 18 個動態 fetch 點與 connect-rpc transport 未完全解析。
estimateMessagesTokenCount的算術不可見;summarizationOutputCap的單位未標。thresholdRatio/toleranceRatio的語意是從命名與contextRatio一致性推來的(推論),沒有註解或文件直接說明「壓到 65 %」。- 伺服器端的 prompt 組裝、模型路由、真正的 context window 大小與計費邏輯不在 asar 裡(unk_67f6ca825e8ffa3f,high,blocked);唯一途徑是執行期擷取 box 事件流(需要使用者核准執行目標)。
js_app_map有 25 000 節點上限(74905 個被丟)與paged_modules覆盖限制;asar 目錄清單我看過 offset 0–560。- 本輪無檔案內容讀取工具,
core_archive_show對單一文字檔只回 size/sha256、不回正文 → 本文是依分析結果重組,非讀回舊檔。
未解(對應 unk id):unk_67f6ca825e8ffa3f(伺服器端 prompt/模型路由/工具 schema);estimateMessagesTokenCount 算術;CUScrollControlPermit 由哪個請求欄位決定(fnd_13/fnd_14)。