211 lines
16 KiB
Markdown
211 lines
16 KiB
Markdown
# 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}`)。規則:
|
||
1. 每個 LLM 事件流結束時取**最後一個**帶 `usage` 的 chunk 當權威值;
|
||
2. 佔用率 = `usage.total_tokens / contextWindow`,寫入事件流(`tokens_used` + `source: "provider"`);
|
||
3. provider 沒給 `usage` 時才退回本地估計,並在事件上標 `est.`,UI 顯示 `~`;
|
||
4. 估計器與權威值並存,永不互相覆蓋——這樣才知道估計誤差。
|
||
|
||
---
|
||
|
||
## §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` | 函式名 | |
|
||
|
||
**推論**:
|
||
1. 全鏈路都以 **byte** 為單位;「token」在客戶端只是「ASCII byte」的別名(註解自己寫了 ASCII=1 token)。
|
||
2. **輸入**有 300 000–360 000 byte 的送件上限,實際一次用 120 000 byte;**輸出**摘要 ≤ 6000(單位未標,**推測**為 token 上限)、單條摘要 ≤ 100 KiB。
|
||
3. 因為摘要由伺服器產生(§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` 做符號/欄位名掃描。**沒有執行目標**,因此本文所有陳述都只證明「碼裡寫了什麼」,不證明「跑起來會怎樣」。
|
||
|
||
**限制(逐條)**:
|
||
1. 「找不到 token 計數欄位」的範圍是 **main-app.cjs + 列出的 9 個欄位名**;fnd_9 已記錄字串表被截斷,fnd_6 已記錄 **18 個動態 fetch 點**與 connect-rpc transport 未完全解析。
|
||
2. `estimateMessagesTokenCount` 的算術不可見;`summarizationOutputCap` 的單位未標。
|
||
3. `thresholdRatio` / `toleranceRatio` 的**語意**是從命名與 `contextRatio` 一致性推來的(推論),沒有註解或文件直接說明「壓到 65 %」。
|
||
4. 伺服器端的 prompt 組裝、模型路由、真正的 context window 大小與計費邏輯**不在 asar 裡**(unk_67f6ca825e8ffa3f,high,blocked);唯一途徑是執行期擷取 box 事件流(需要使用者核准執行目標)。
|
||
5. `js_app_map` 有 25 000 節點上限(74905 個被丟)與 `paged_modules` 覆盖限制;asar 目錄清單我看過 offset 0–560。
|
||
6. 本輪無檔案內容讀取工具,`core_archive_show` 對單一文字檔只回 size/sha256、不回正文 → 本文是依分析結果重組,非讀回舊檔。
|
||
|
||
**未解(對應 unk id)**:`unk_67f6ca825e8ffa3f`(伺服器端 prompt/模型路由/工具 schema);`estimateMessagesTokenCount` 算術;`CUScrollControlPermit` 由哪個請求欄位決定(fnd_13/fnd_14)。
|