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 返回的精确值,必要时使用估算值

关键原则

  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
    }