Skip to content

Token 計算原理

學完你能做什麼

  • 理解 DCP 的 Token 計算策略(API 值 vs 估算值)
  • 讀懂 /dcp context 輸出中的 Token 分佈
  • 知道 Token 估算的誤差範圍和優化方法
  • 準確評估 DCP 的節省效果

你現在的困境

你看著 /dcp context 的輸出,一堆數字和百分比,但不知道它們是怎麼算出來的:

Session Context Breakdown:
─────────────────────────────────────────────────────────

System         15.2% │████████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│  25.1K tokens
User            5.1% │████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│   8.4K tokens
Assistant       35.8% │██████████████████████████████████████▒▒▒▒▒▒│  59.2K tokens
Tools (45)      43.9% │████████████████████████████████████████████████│  72.6K tokens
─────────────────────────────────────────────────────────

Summary:
  Pruned:          12 tools (~15.2K tokens)
  Current context: ~165.3K tokens
  Without DCP:     ~180.5K tokens

這些數字從哪裡來?為什麼 Assistant 是「殘差計算」?prunedTokens 怎麼算的?

什麼時候用這一招

  • 想準確評估 DCP 的節省效果時
  • 需要分析 Token 消耗結構時(哪個部分佔用最多)
  • 懷疑 Token 計算有誤差,想驗證準確性時
  • 需要向團隊匯報 DCP 的優化成果時

核心思路

DCP 的 Token 計算採用混合策略優先使用 API 返回的精確值,必要時使用估算值

mermaid
graph TD
    A[Token 計算] --> B{是否需要精確計算?}
    B -->|是| C[使用 API 返回值]
    B -->|否| D[使用 Tokenizer 估算]

    C --> E[Total: input + output + reasoning + cache.read + cache.write]
    C --> F[System: firstAssistant.input + cache.read - firstUserTokens]

    D --> G[countTokens 函式]
    G --> H{Tokenizer 可用?}
    H -->|是| I[使用 Anthropic Tokenizer]
    H -->|否| J[回退到 字元數 / 4]

    I --> K[User: tokenizer 所有使用者訊息]
    I --> L[Tools: tokenizer 工具輸入輸出 - prunedTokens]

關鍵原則

  1. API 值優先:Total 和 System 使用 API 精確返回值
  2. 估算值補全:User 和 Tools 使用 tokenizer 估算(因為 API 不提供這些值)
  3. 殘差計算:Assistant 透過 Total - System - User - Tools 計算得出

Token 計算策略

API 返回的精確值

OpenCode API 會為每次 AI 回應返回精確的 Token 統計:

欄位說明來源
tokens.input本次請求的輸入 Token 數API 精確值
tokens.output本次請求生成的輸出 Token 數API 精確值
tokens.reasoning推理 Token 數(如 Claude 3.5 思維鏈)API 精確值(如適用)
tokens.cache.read快取讀取的 Token 數API 精確值
tokens.cache.write快取寫入的 Token 數API 精確值

這些值是精確的,因為它們直接來自 LLM 提供商的計費系統。

估算值的計算方法

對於 API 不提供的值(如使用者訊息、工具呼叫),DCP 使用 Anthropic Tokenizer 進行估算。

實作程式碼lib/strategies/utils.ts:36-43):

typescript
export function countTokens(text: string): number {
    if (!text) return 0
    try {
        // 優先使用 Anthropic Tokenizer
        return anthropicCountTokens(text)
    } catch {
        // 失敗時回退到 字元數 / 4
        return Math.round(text.length / 4)
    }
}

為什麼用字元數/4 作為備用

  • 英文文字的平均 Token 比約為 4 字元 = 1 Token
  • 這只是一個粗略估算,可能存在誤差
  • 只有在 Tokenizer 不可用時才會使用(如網路問題、函式庫載入失敗)

分類 Token 計算邏輯

Total(總 Token 數)

計算公式

typescript
total = input + output + reasoning + cache.read + cache.write

實作程式碼lib/commands/context.ts:103-108):

typescript
const apiInput = lastAssistant?.tokens?.input || 0
const apiOutput = lastAssistant?.tokens?.output || 0
const apiReasoning = lastAssistant?.tokens?.reasoning || 0
const apiCacheRead = lastAssistant?.tokens?.cache?.read || 0
const apiCacheWrite = lastAssistant?.tokens?.cache?.write || 0
breakdown.total = apiInput + apiOutput + apiReasoning + apiCacheRead + apiCacheWrite

說明

  • 使用最後一次 Assistant 回應的 Token 統計
  • 包含所有 API 返回的值
  • 與 OpenCode UI 顯示的總 Token 數一致

System(系統提示詞 Token)

計算公式

typescript
system = firstAssistant.input + firstAssistant.cache.read - firstUserTokens

實作程式碼lib/commands/context.ts:161-165):

typescript
if (firstAssistant) {
    const firstInput =
        (firstAssistant.tokens?.input || 0) + (firstAssistant.tokens?.cache?.read || 0)
    breakdown.system = Math.max(0, firstInput - firstUserTokens)
}

原理

  1. 第一次 Assistant 回應的 input 包含:系統提示詞 + 第一個使用者訊息
  2. 減去第一個使用者訊息的 Token 數(使用 tokenizer 估算),得到純系統提示詞的 Token 數
  3. 加上 cache.read(快取讀取也算系統 Token)

為什麼是殘差計算?

API 不直接提供「系統提示詞」的 Token 數,只能透過「第一次回應的輸入 - 第一個使用者訊息」來推算。這是標準的估算方法。

User(使用者訊息 Token)

計算公式

typescript
user = tokenizer(all user messages)

實作程式碼lib/commands/context.ts:110-157):

typescript
const userTextParts: string[] = []

for (const msg of messages) {
    if (isMessageCompacted(state, msg)) continue
    if (msg.info.role === "user" && isIgnoredUserMessage(msg)) continue

    const parts = Array.isArray(msg.parts) ? msg.parts : []
    for (const part of parts) {
        if (part.type === "text" && msg.info.role === "user") {
            const textPart = part as TextPart
            const text = textPart.text || ""
            userTextParts.push(text)
        }
    }
}

breakdown.user = countTokens(userTextParts.join("\n"))

說明

  • 使用 tokenizer 計算所有使用者訊息的 Token 數
  • 跳過已被壓縮的訊息(isMessageCompacted
  • 跳過 ignored 訊息(系統內部訊息)

Tools(工具呼叫 Token)

計算公式

typescript
tools = tokenizer(toolInputs + toolOutputs) - prunedTokens

實作程式碼lib/commands/context.ts:110-167):

typescript
const toolInputParts: string[] = []
const toolOutputParts: string[] = []

for (const msg of messages) {
    if (isMessageCompacted(state, msg)) continue

    const parts = Array.isArray(msg.parts) ? msg.parts : []
    for (const part of parts) {
        if (part.type === "tool") {
            const toolPart = part as ToolPart
            breakdown.toolCount++

            // 提取工具輸入
            if (toolPart.state?.input) {
                const inputStr =
                    typeof toolPart.state.input === "string"
                        ? toolPart.state.input
                        : JSON.stringify(toolPart.state.input)
                toolInputParts.push(inputStr)
            }

            // 提取工具輸出(僅限已完成的狀態)
            if (toolPart.state?.status === "completed" && toolPart.state?.output) {
                const outputStr =
                    typeof toolPart.state.output === "string"
                        ? toolPart.state.output
                        : JSON.stringify(toolPart.state.output)
                toolOutputParts.push(outputStr)
            }
        }
    }
}

const toolInputTokens = countTokens(toolInputParts.join("\n"))
const toolOutputTokens = countTokens(toolOutputParts.join("\n"))
breakdown.tools = Math.max(0, toolInputTokens + toolOutputTokens - breakdown.prunedTokens)

說明

  • 計算所有工具的輸入和輸出 Token
  • 減去已修剪工具的 Token(prunedTokens
  • 只統計 status === "completed" 的工具輸出

Assistant(助手訊息 Token)

計算公式

typescript
assistant = total - system - user - tools

實作程式碼lib/commands/context.ts:168-171):

typescript
breakdown.assistant = Math.max(
    0,
    breakdown.total - breakdown.system - breakdown.user - breakdown.tools,
)

原理

  • Assistant 是殘差計算(Residual Calculation)
  • 從 Total 中減去 System、User、Tools,剩下的就是 Assistant

為什麼 Assistant 包含推理 Token?

如果模型將推理 Token(reasoning)持久化在上下文中,它們屬於「Assistant 生成的內容」,因此會被歸類到 Assistant 類別。這是合理的,因為推理就是助手在「思考」。


Token 節省計算

prunedTokens 的計算

函式簽章lib/strategies/utils.ts:49-94):

typescript
export const calculateTokensSaved = (
    state: SessionState,
    messages: WithParts[],
    pruneToolIds: string[],
): number

計算邏輯

  1. 遍歷所有訊息
  2. 找到被修剪的工具(透過 pruneToolIds 匹配 part.callID
  3. 提取工具的輸入和輸出內容
  4. 使用 countTokens 計算 Token 數
  5. 累加所有被修剪工具的 Token 數

實作程式碼

typescript
export const calculateTokensSaved = (
    state: SessionState,
    messages: WithParts[],
    pruneToolIds: string[],
): number => {
    try {
        const contents: string[] = []
        for (const msg of messages) {
            if (isMessageCompacted(state, msg)) {
                continue
            }
            const parts = Array.isArray(msg.parts) ? msg.parts : []
            for (const part of parts) {
                if (part.type !== "tool" || !pruneToolIds.includes(part.callID)) {
                    continue
                }
                // 處理 question 工具的特殊情況
                if (part.tool === "question") {
                    const questions = part.state.input?.questions
                    if (questions !== undefined) {
                        const content =
                            typeof questions === "string" ? questions : JSON.stringify(questions)
                        contents.push(content)
                    }
                    continue
                }
                // 提取已完成工具的輸出
                if (part.state.status === "completed") {
                    const content =
                        typeof part.state.output === "string"
                            ? part.state.output
                            : JSON.stringify(part.state.output)
                    contents.push(content)
                } else if (part.state.status === "error") {
                    // 提取錯誤工具的錯誤資訊
                    const content =
                        typeof part.state.error === "string"
                            ? part.state.error
                            : JSON.stringify(part.state.error)
                    contents.push(content)
                }
            }
        }
        const tokenCounts: number[] = estimateTokensBatch(contents)
        return tokenCounts.reduce((sum, count) => sum + count, 0)
    } catch (error: any) {
        return 0
    }
}

說明

  • 只計算被修剪工具的 Token 數
  • 對於已完成工具,計算其輸出
  • 對於錯誤工具,計算其錯誤資訊
  • 如果計算失敗(異常),返回 0

Token 計算的誤差範圍

估算誤差來源

類別精確度誤差來源影響
Total100%API 精確值,無誤差
System95-98%第一個使用者訊息的 Token 估算有輕微誤差很小
User90-95%Tokenizer 與實際模型的分詞可能有差異中等
Tools90-95%Tokenizer + 工具參數序列化格式的影響中等
Assistant90-95%受 User 和 Tools 的誤差傳遞中等

誤差控制

DCP 採用多種方法控制誤差

  1. 使用 Anthropic Tokenizer

    • 最接近 Claude 實際分詞邏輯
    • 對於 OpenAI、Gemini,可能有輕微差異
  2. 跳過系統內部訊息

    • isIgnoredUserMessage 過濾掉 OpenCode 內部訊息
    • 避免計算不相關的 Token
  3. Math.max(0, ...)

    • 確保結果不為負數
    • 防止異常情況

實際誤差觀察

在測試中,DCP 的 Token 估算與 OpenCode UI 顯示的差異通常在 ±5% 以內。對於估算 Token 節省效果,這個精度已經足夠。


/dcp context 輸出解讀

Token 分佈圖

System         15.2% │████████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│  25.1K tokens
User            5.1% │████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│   8.4K tokens
Assistant       35.8% │██████████████████████████████████████▒▒▒▒▒▒│  59.2K tokens
Tools (45)      43.9% │████████████████████████████████████████████████│  72.6K tokens

各項含義

項目說明佔比正常範圍
System系統提示詞 Token10-20%
User所有使用者訊息 Token3-8%
Assistant助手文字輸出 + 推理 Token30-40%
Tools (n)工具呼叫 Token(n = 上下文中的工具數)40-50%

Summary 解讀

Summary:
  Pruned:          12 tools (~15.2K tokens)
  Current context: ~165.3K tokens
  Without DCP:     ~180.5K tokens

各項含義

項目計算方式說明
Prunedstate.prune.toolIds.length 工具數 + prunedTokens已修剪的工具數量和節省的 Token 數
Current contextbreakdown.total(API 精確值)目前上下文的實際 Token 數
Without DCPtotal + prunedTokens假設沒有 DCP 時的理論 Token 數

節省率計算

節省率 = prunedTokens / Without DCP
      = 15.2K / 180.5K
      ≈ 8.4%

節省率參考

  • 輕度使用:5-10%(偶爾重複讀取檔案)
  • 中度使用:10-20%(頻繁去重、覆蓋寫入修剪)
  • 重度使用:20-30%(大量工具呼叫、AI 主動修剪)

實戰:分析 Token 分佈

場景 1:工具呼叫過多

問題Tools 佔比超過 60%,上下文很大。

Tools (120)     65.2% │████████████████████████████████████████████████│  120.5K tokens

可能原因

  • 對話中呼叫了大量工具(如多次讀取檔案、搜尋)
  • 工具輸出內容很大(如讀取長檔案)

優化建議

  • 啟用去重策略strategies.deduplication.enabled: true
  • 啟用覆蓋寫入策略strategies.supersedeWrites.enabled: true
  • 使用 /dcp sweep 手動清理舊工具

場景 2:Assistant 佔比過高

問題Assistant 佔比超過 50%,可能是推理 Token。

Assistant       52.3% │██████████████████████████████████████████████▒▒▒▒▒▒│  95.2K tokens

可能原因

  • 使用了帶推理的模型(如 Claude 3.5)
  • 助手輸出了大量文字

優化建議

  • 推理 Token 是無法修剪的(它們屬於 Assistant 的生成內容)
  • 可以透過縮短助手輸出(讓 AI 更簡潔)來減少
  • 檢查是否有冗餘的工具呼叫(透過去重修剪)

場景 3:節省率偏低

問題Pruned 很小,節省率 < 5%。

Summary:
  Pruned:          2 tools (~1.2K tokens)
  Current context: ~165.3K tokens
  Without DCP:     ~166.5K tokens

可能原因

  • 對話中很少有重複工具呼叫
  • 沒有啟用自動策略(去重、覆蓋寫入、清除錯誤)
  • AI 沒有主動呼叫 discard/extract 工具

優化建議

  • 啟用所有自動策略:
    jsonc
    {
      "strategies": {
        "deduplication": { "enabled": true },
        "supersedeWrites": { "enabled": true },
        "purgeErrors": { "enabled": true }
      }
    }
  • 啟用提醒功能tools.settings.nudgeEnabled: true),讓 AI 主動修剪

踩坑提醒

❌ 誤區 1:期望 100% 精確

問題:期望 Token 計算與 API 帳單完全一致。

現實:DCP 的計算是估算值,存在 ±5% 的誤差。

解決

  • 使用 DCP 的統計作為相對參考(如節省率)
  • 不要用作精確的計費稽核
  • API 帳單才是唯一準確的消費記錄

❌ 誤區 2:忽略推理 Token

問題:推理 Token 被歸類到 Assistant,但使用者以為被漏掉了。

現實:推理 Token 是 Assistant 生成的一部分,歸類合理。

解決

  • 推理 Token 無法修剪(它們不屬於任何工具)
  • 如果推理 Token 佔比過高(> 30%),可以嘗試:
    • 使用不帶推理的模型
    • 簡化任務,減少推理需求

❌ 誤區 3:快取 Token 不計入 Total

問題:以為 cache.read 不計入 Total,導致誤解。

現實cache.read 會計入 Total,但不會產生實際費用(因為已快取)。

解決

  • Total上下文大小,不是實際費用
  • 快取命中的 Token 仍然佔用上下文空間
  • 如果需要優化成本,可以查看 /dcp stats 的累計節省

❌ 誤區 4:工具輸入被修剪後,Token 不減少

問題:DCP 修剪了工具輸入,但 /dcp contextCurrent context 沒有變化。

現實Current context 顯示的是傳送給 LLM 的 Token,已修剪的內容不包含在內。

解決

  • Current context = Without DCP - prunedTokens
  • 如果 prunedTokens 增加,說明修剪生效
  • 如果 Current context 沒有減少,可能是 prunedTokens 太小,或者有新訊息增加了 Token

本課小結

DCP 的 Token 計算採用混合策略:

  1. API 精確值:Total、System 使用 API 返回的精確值
  2. Tokenizer 估算:User、Tools 使用 Anthropic Tokenizer 估算
  3. 殘差計算:Assistant 透過 Total - System - User - Tools 計算

核心函式

  • countTokens():計算文字 Token 數(優先使用 Anthropic Tokenizer)
  • calculateTokensSaved():計算已修剪工具的節省 Token 數
  • analyzeTokens():分析上下文 Token 分佈

誤差範圍

  • Total:100% 精確(API 值)
  • 其他類別:90-98%(估算值)
  • 整體誤差:±5%

使用場景

  • 使用 /dcp context 查看 Token 分佈
  • 根據分佈情況優化策略配置
  • 使用 /dcp stats 查看累計節省效果

下一課預告

下一課我們學習 API 參考

你會學到:

  • DCP 外掛的 API 介面文件
  • 配置項的完整說明
  • 狀態管理介面
  • 為外掛開發者提供的擴充點

附錄:原始碼參考

點選展開查看原始碼位置

更新時間:2026-01-23

功能檔案路徑行號
countTokens 函式lib/strategies/utils.ts36-43
calculateTokensSavedlib/strategies/utils.ts49-94
analyzeTokens 函式lib/commands/context.ts68-174
Token 計算策略註解lib/commands/context.ts5-38
handleContextCommandlib/commands/context.ts238-247

關鍵函式

  • countTokens(text: string): number:計算文字 Token 數,使用 Anthropic Tokenizer 或回退到 字元數/4
  • calculateTokensSaved(state, messages, pruneToolIds): number:計算已修剪工具的節省 Token 數
  • analyzeTokens(state, messages): TokenBreakdown:分析上下文 Token 分佈

關鍵介面

  • TokenBreakdown:Token 分佈分析結果
    typescript
    interface TokenBreakdown {
        system: number
        user: number
        assistant: number
        tools: number
        toolCount: number
        prunedTokens: number
        prunedCount: number
        total: number
    }