LLM Token 计数实战:从 Tokenizer 原理到成本优化的完整指南

深入解析 BPE、SentencePiece 等 Tokenizer 原理,用 JavaScript 和 Python 实现精确 Token 计数,提供成本估算与预算管理的完整方案,帮你把 LLM API 账单砍掉 40%。

开发者效率 2026-05-29 12 分钟

在 LLM 应用开发中,Token 是计费的最小单位,也是上下文窗口的硬性约束。OpenAI GPT-4o 的输入价格是 $2.5/百万 Token,Claude Sonnet 4 是 $3/百万 Token——一个不小心多发了几千 Token 的 System Prompt,每月账单就能多出几百美元。但大多数开发者对 Token 的理解还停留在「一个中文字 ≈ 2 个 Token」这种粗略估算上,直到某天发现 API 调用突然超限或成本飙升才追悔莫及。

🔍 一、Tokenizer 底层原理:为什么理解 Token 很重要

Token 不等于字符,也不等于词

很多开发者以为 Token 就是「按空格分词」,这是一个致命的误解。现代 LLM 使用的都是子词分词(Subword Tokenization)——高频词作为完整 Token,低频词被拆成多个子词片段。这意味着:

  • "hello" → 1 个 Token(高频英文词)
  • "tokenizer" → 2 个 Token:["token", "izer"]
  • "你好世界" → 4-6 个 Token(取决于模型的 Tokenizer)
  • "{\"name\": \"test\"}" → 7-10 个 Token(JSON 中的标点和空格都是 Token)

⚠️ **警告:**不要用「字符数 ÷ 4」来估算英文 Token 数,也不要用「中文字数 × 2」来估算中文 Token 数。不同模型的 Tokenizer 差异巨大,同一个句子在 GPT-4 和 Claude 上的 Token 数可能相差 30%。

BPE 算法:主流 Tokenizer 的核心

Byte Pair Encoding(BPE)是 GPT 系列模型使用的分词算法。它的核心思想极其简单:反复合并语料中出现频率最高的相邻字节对

以训练过程为例,假设初始语料包含这些词频:

"low"    出现 5 次
"lower"  出现 2 次
"newest" 出现 6 次
"widest" 出现 3 次

初始时每个字符都是独立 Token:l o wl o w e rn e w e s t

第一轮:发现 e s 出现频率最高(newest 6次 + widest 3次 = 9次),合并 e + s → es

第二轮:发现 es t 出现频率最高(newest 6次 + widest 3次 = 9次),合并 es + t → est

第三轮:发现 estl o w 的组合中,l o 出现 7 次,合并 l + o → lo

如此反复,直到达到预设的词表大小(GPT-4 为 100,256 个 Token)。

// BPE 算法简化实现 —— 理解 Token 合并过程
// 输入:原始文本和预训练的合并规则
function bpeEncode(text, mergeRules) {
  // 第一步:将文本拆成字节级别的 Token 列表
  let tokens = Array.from(new TextEncoder().encode(text)).map(b => [b]);

  // 第二步:按优先级依次应用合并规则
  for (const [a, b] of mergeRules) {
    const newTokens = [];
    let i = 0;
    while (i < tokens.length) {
      // 如果当前 Token 和下一个 Token 匹配合并规则,就合并
      if (i < tokens.length - 1 &&
          JSON.stringify(tokens[i]) === JSON.stringify(a) &&
          JSON.stringify(tokens[i + 1]) === JSON.stringify(b)) {
        newTokens.push([...a, ...b]);
        i += 2;  // 跳过已合并的两个 Token
      } else {
        newTokens.push(tokens[i]);
        i++;
      }
    }
    tokens = newTokens;
  }
  return tokens;
}

// 演示:手动执行一轮 BPE 合并
const initial = [[108], [111], [119], [101], [114]];  // "lower" 的字节
const mergeRule = [[108], [111]];  // 合并 "l" + "o" → "lo"
const merged = bpeEncode(
  String.fromCharCode(...initial.flat()),
  [mergeRule]
);
console.log(merged);
// 合并后:[[108, 111], [119], [101], [114]] — "lo" 变成一个 Token

三大主流 Tokenizer 对比

不同 LLM 厂商使用的 Tokenizer 差异显著,直接影响你的 API 成本:

维度 GPT-4o (cl100k_base) Claude (自研) Gemini (SentencePiece)
算法 BPE BPE 变体 Unigram / BPE
词表大小 100,256 ~100,000 256,000
中文效率 较好(1汉字≈1.5-2 Token) 优秀(1汉字≈1-1.5 Token) 优秀(1汉字≈1-1.5 Token)
代码效率 较好 优秀 一般
JSON 效率 较好 优秀 一般
计费单位 每 Token 每 Token 每 1000 Token

💡 **提示:**同一个中文句子,Claude 的 Token 数通常比 GPT-4o 少 15-25%。这意味着如果你的应用以中文为主,选择 Claude 在 Token 效率上更有优势——但要综合考虑模型质量和总成本。

🛠️ 二、精确 Token 计数:JavaScript 与 Python 实战

JavaScript/TypeScript:tiktoken 的 WASM 实现

OpenAI 官方的 tiktoken 是 Rust 实现,社区将其编译为 WASM,可以在浏览器和 Node.js 中精确计算 GPT 系列模型的 Token 数:

// 安装:npm install tiktoken
// 精确计算 GPT-4o 的 Token 数
import { encoding_for_model } from 'tiktoken';

function countTokens(text, model = 'gpt-4o') {
  // 获取对应模型的编码器
  const enc = encoding_for_model(model);
  const tokens = enc.encode(text);
  const count = tokens.length;

  // ⚠️ 必须释放编码器,否则 WASM 内存泄漏
  enc.free();

  return count;
}

// 实际测试:不同内容的 Token 数差异巨大
const examples = [
  'Hello, world!',                          // 英文短句
  '你好,世界!',                              // 中文短句
  '{"name": "张三", "age": 30, "city": "北京"}',  // JSON 数据
  'function fibonacci(n) { return n <= 1 ? n : fibonacci(n-1) + fibonacci(n-2); }',  // 代码
  '请帮我分析以下JSON数据的结构,并给出优化建议。我需要你重点关注数据类型的一致性和命名规范。',  // 长中文 Prompt
];

for (const text of examples) {
  const count = countTokens(text);
  const chars = text.length;
  const ratio = (count / chars).toFixed(2);
  console.log(`[${count} tokens | ${chars} chars | ratio: ${ratio}] ${text.slice(0, 40)}...`);
}

// 输出示例:
// [4  tokens | 13  chars | ratio: 0.31] Hello, world!...
// [6  tokens | 5   chars | ratio: 1.20] 你好,世界!...
// [22 tokens | 40  chars | ratio: 0.55] {"name": "张三", "age": 30, "city": "北京"}...
// [29 tokens | 82  chars | ratio: 0.35] function fibonacci(n) { return n <= 1 ? n : f...
// [52 tokens | 46  chars | ratio: 1.13] 请帮我分析以下JSON数据的结构,并给出优化建议...

📌 **记住:**tiktoken 只适用于 OpenAI 模型。如果你用 Claude 或 Gemini,需要使用对应的 Tokenizer。错误的 Tokenizer 会导致 Token 数偏差 20-40%,直接影响成本估算。

不同模型的 Token 计数方案

// 统一 Token 计数接口 —— 支持多模型
// 方案一:使用本地 Tokenizer(精确,适合 OpenAI 模型)
import { encoding_for_model } from 'tiktoken';

class TokenCounter {
  private encoderMap = new Map();

  // 获取指定模型的编码器(懒加载 + 缓存)
  getEncoder(model: string) {
    if (!this.encoderMap.has(model)) {
      try {
        this.encoderMap.set(model, encoding_for_model(model));
      } catch {
        // 回退到通用编码器
        this.encoderMap.set(model, encoding_for_model('gpt-4o'));
      }
    }
    return this.encoderMap.get(model);
  }

  count(text: string, model: string = 'gpt-4o'): number {
    const enc = this.getEncoder(model);
    return enc.encode(text).length;
  }

  // 估算成本(美元)
  estimateCost(
    inputTokens: number,
    outputTokens: number,
    model: string
  ): number {
    const pricing: Record<string, { input: number; output: number }> = {
      'gpt-4o':        { input: 2.5 / 1e6, output: 10 / 1e6 },
      'gpt-4o-mini':   { input: 0.15 / 1e6, output: 0.6 / 1e6 },
      'claude-sonnet-4': { input: 3 / 1e6, output: 15 / 1e6 },
      'claude-haiku':  { input: 0.25 / 1e6, output: 1.25 / 1e6 },
    };
    const rate = pricing[model] || pricing['gpt-4o'];
    return inputTokens * rate.input + outputTokens * rate.output;
  }

  // 清理资源
  destroy() {
    for (const enc of this.encoderMap.values()) {
      enc.free();
    }
    this.encoderMap.clear();
  }
}

// 使用示例
const counter = new TokenCounter();

const systemPrompt = '你是一个专业的JSON数据分析师。请分析用户提供的JSON数据。';
const userMessage = JSON.stringify({ users: Array.from({ length: 100 }, (_, i) => ({
  id: i, name: `user_${i}`, email: `user${i}@example.com`
}))});

const inputTokens = counter.count(systemPrompt) + counter.count(userMessage);
const estimatedOutputTokens = 500;  // 预估输出 500 Token

console.log(`输入 Token: ${inputTokens}`);
console.log(`预估输出 Token: ${estimatedOutputTokens}`);
console.log(`GPT-4o 预估费用: $${counter.estimateCost(inputTokens, estimatedOutputTokens, 'gpt-4o').toFixed(4)}`);
console.log(`Claude Sonnet 预估费用: $${counter.estimateCost(inputTokens, estimatedOutputTokens, 'claude-sonnet-4').toFixed(4)}`);

counter.destroy();  // 释放所有编码器

Python:官方 tiktoken + 多模型支持

# Python 端的 Token 计数 —— 更成熟的生态
# 安装:pip install tiktoken anthropic

import tiktoken
from anthropic import Anthropic

def count_openai_tokens(text: str, model: str = "gpt-4o") -> int:
    """精确计算 OpenAI 模型的 Token 数"""
    try:
        enc = tiktoken.encoding_for_model(model)
    except KeyError:
        enc = tiktoken.get_encoding("cl100k_base")
    return len(enc.encode(text))

def count_claude_tokens(text: str, model: str = "claude-sonnet-4-20250514") -> int:
    """使用 Anthropic API 计算 Token 数"""
    client = Anthropic()
    response = client.messages.count_tokens(
        model=model,
        messages=[{"role": "user", "content": text}]
    )
    return response.input_tokens

def estimate_monthly_cost(
    daily_requests: int,
    avg_input_tokens: int,
    avg_output_tokens: int,
    model: str = "gpt-4o"
) -> dict:
    """估算月度 API 成本"""
    pricing = {
        "gpt-4o":          {"input": 2.5, "output": 10.0},
        "gpt-4o-mini":     {"input": 0.15, "output": 0.6},
        "claude-sonnet-4": {"input": 3.0, "output": 15.0},
        "claude-haiku":    {"input": 0.25, "output": 1.25},
    }
    rate = pricing.get(model, pricing["gpt-4o"])

    monthly_input = daily_requests * 30 * avg_input_tokens
    monthly_output = daily_requests * 30 * avg_output_tokens

    input_cost = monthly_input / 1_000_000 * rate["input"]
    output_cost = monthly_output / 1_000_000 * rate["output"]

    return {
        "model": model,
        "monthly_input_tokens": monthly_input,
        "monthly_output_tokens": monthly_output,
        "input_cost": round(input_cost, 2),
        "output_cost": round(output_cost, 2),
        "total_cost": round(input_cost + output_cost, 2),
    }

# 实际对比:1000 次/天的 API 调用
for model in ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4", "claude-haiku"]:
    result = estimate_monthly_cost(
        daily_requests=1000,
        avg_input_tokens=800,
        avg_output_tokens=500,
        model=model
    )
    print(f"{model:20s} → 月费 ${result['total_cost']}")

📊 三、Token 预算管理与成本优化实战

构建 Token 预算管理器

在生产环境中,你需要一个 Token 预算管理器来控制成本,防止意外超支:

// Token 预算管理器 —— 防止 API 成本失控
class TokenBudgetManager {
  private dailyBudget: number;       // 每日 Token 预算上限
  private monthlyBudget: number;     // 每月 Token 预算上限
  private dailyUsage: number = 0;    // 当日已用 Token
  private monthlyUsage: number = 0;  // 当月已用 Token
  private alertThreshold: number;    // 告警阈值(百分比)

  constructor(config: {
    dailyBudget: number;
    monthlyBudget: number;
    alertThreshold?: number;  // 默认 80%
  }) {
    this.dailyBudget = config.dailyBudget;
    this.monthlyBudget = config.monthlyBudget;
    this.alertThreshold = config.alertThreshold || 0.8;
  }

  // 检查是否还有预算
  canProceed(estimatedTokens: number): {
    allowed: boolean;
    reason?: string;
    remainingDaily: number;
    remainingMonthly: number;
  } {
    const remainingDaily = this.dailyBudget - this.dailyUsage;
    const remainingMonthly = this.monthlyBudget - this.monthlyUsage;

    if (estimatedTokens > remainingDaily) {
      return {
        allowed: false,
        reason: `日预算不足:需要 ${estimatedTokens},剩余 ${remainingDaily}`,
        remainingDaily,
        remainingMonthly,
      };
    }

    if (estimatedTokens > remainingMonthly) {
      return {
        allowed: false,
        reason: `月预算不足:需要 ${estimatedTokens},剩余 ${remainingMonthly}`,
        remainingDaily,
        remainingMonthly,
      };
    }

    return { allowed: true, remainingDaily, remainingMonthly };
  }

  // 记录实际使用量
  recordUsage(tokens: number) {
    this.dailyUsage += tokens;
    this.monthlyUsage += tokens;

    // 检查是否触发告警
    const dailyRatio = this.dailyUsage / this.dailyBudget;
    const monthlyRatio = this.monthlyUsage / this.monthlyBudget;

    if (dailyRatio >= this.alertThreshold) {
      console.warn(`⚠️ 日预算已使用 ${(dailyRatio * 100).toFixed(1)}%`);
    }
    if (monthlyRatio >= this.alertThreshold) {
      console.warn(`⚠️ 月预算已使用 ${(monthlyRatio * 100).toFixed(1)}%`);
    }
  }

  // 重置日预算(每日零点调用)
  resetDaily() {
    this.dailyUsage = 0;
  }
}

// 使用示例
const budget = new TokenBudgetManager({
  dailyBudget: 500_000,    // 每日 50 万 Token
  monthlyBudget: 10_000_000,  // 每月 1000 万 Token
  alertThreshold: 0.8,       // 80% 时告警
});

// 模拟 API 调用前的预算检查
const estimatedTokens = 2000;
const check = budget.canProceed(estimatedTokens);

if (check.allowed) {
  // 调用 LLM API...
  budget.recordUsage(1850);  // 记录实际使用量
  console.log(`✅ 请求成功,日预算剩余: ${check.remainingDaily}`);
} else {
  console.error(`❌ ${check.reason}`);
}

七大成本优化策略

在实际生产中,我总结了七个经过验证的 Token 成本优化策略:

策略 节省比例 实施难度 适用场景
✅ 精简 System Prompt 20-40% ⭐ 低 所有应用
✅ Prompt 缓存(Prompt Caching) 50-90% ⭐⭐ 中 重复 Prompt 场景
✅ 小模型路由(Model Routing) 30-60% ⭐⭐ 中 多模型架构
✅ 输出长度限制 10-30% ⭐ 低 所有应用
✅ 上下文压缩 30-50% ⭐⭐⭐ 高 长对话场景
✅ JSON Schema 约束输出 5-15% ⭐ 低 结构化输出场景
✅ 批量 API(Batch API) 50% ⭐ 低 非实时场景

💡 **提示:**Prompt Caching 是 2025-2026 年最被低估的优化手段。OpenAI 和 Anthropic 都支持自动缓存重复的前缀部分,缓存命中时价格直接打一折。如果你的 System Prompt 有 2000 Token 且每天调用 10000 次,开启 Prompt Caching 每月可以节省 $100+。

实战:Prompt 压缩技术

// Prompt 压缩 —— 在不损失关键信息的前提下减少 Token 数
function compressPrompt(prompt: string): {
  compressed: string;
  originalTokens: number;
  compressedTokens: number;
  savedPercent: string;
} {
  let compressed = prompt;

  // 1. 移除多余空白和换行
  compressed = compressed.replace(/\n{3,}/g, '\n\n');
  compressed = compressed.replace(/[ \t]{2,}/g, ' ');

  // 2. 移除注释(如果是代码 Prompt)
  compressed = compressed.replace(/\/\/.*$/gm, '');
  compressed = compressed.replace(/\/\*[\s\S]*?\*\//g, '');

  // 3. 简化冗余表达(中英文都适用)
  const redundancies = [
    [/请(?:你)?(?:帮我)?/g, ''],
    [/你能(?:不能)?(?:帮我)?/g, ''],
    [/I would like you to/gi, ''],
    [/Please (?:help me )?(?:to )?/gi, ''],
    [/Could you (?:please )?/gi, ''],
    [/as follows\s*[::]?/gi, ':'],
    [/the following/gi, 'this'],
  ];

  for (const [pattern, replacement] of redundancies) {
    compressed = compressed.replace(pattern, replacement);
  }

  // 4. 压缩 JSON 格式(去除美化空格)
  try {
    const parsed = JSON.parse(compressed);
    compressed = JSON.stringify(parsed);
  } catch {
    // 不是 JSON,跳过
  }

  // 注意:这里用简单的字符数比例估算 Token 节省
  // 生产环境应使用 tiktoken 精确计算
  const originalTokens = Math.ceil(prompt.length / 3.5);
  const compressedTokens = Math.ceil(compressed.length / 3.5);

  return {
    compressed,
    originalTokens,
    compressedTokens,
    savedPercent: ((1 - compressedTokens / originalTokens) * 100).toFixed(1),
  };
}

// 实际测试
const verbosePrompt = `
请帮我分析以下 JSON 数据的结构,并且你能帮我给出优化建议吗?

我需要你重点关注以下几个方面:
1. 数据类型的一致性
2. 字段命名的规范性
3. 是否存在冗余字段

请按照以下格式输出分析结果:

{
  "name": "张三",
  "age": 30,
  "address": {
    "city": "北京",
    "district": "海淀区",
    "street": "中关村大街1号"
  },
  "hobbies": ["编程", "阅读", "游泳"]
}
`;

const result = compressPrompt(verbosePrompt);
console.log(`原始: ~${result.originalTokens} tokens`);
console.log(`压缩后: ~${result.compressedTokens} tokens`);
console.log(`节省: ${result.savedPercent}%`);

⚠️ **警告:**Prompt 压缩要在「节省 Token」和「保持效果」之间找平衡。过度压缩会导致模型理解偏差,反而需要更多轮对话才能得到满意结果,最终成本更高。建议先用原始 Prompt 和压缩 Prompt 分别测试 100 个样本,对比输出质量后再决定压缩程度。

💡 四、生产环境的 Token 监控与告警

构建 Token 使用量 Dashboard

在生产环境中,你需要实时监控 Token 使用量,而不是等到月底看账单才发现问题:

// Token 使用量追踪器 —— 记录每次 API 调用的 Token 消耗
interface TokenUsageRecord {
  timestamp: Date;
  model: string;
  inputTokens: number;
  outputTokens: number;
  cost: number;
  userId?: string;
  endpoint?: string;
}

class TokenUsageTracker {
  private records: TokenUsageRecord[] = [];
  private alertCallbacks: Array<(alert: string) => void> = [];

  // 记录一次 API 调用
  record(usage: Omit<TokenUsageRecord, 'timestamp' | 'cost'>) {
    const pricing: Record<string, { input: number; output: number }> = {
      'gpt-4o': { input: 2.5 / 1e6, output: 10 / 1e6 },
      'gpt-4o-mini': { input: 0.15 / 1e6, output: 0.6 / 1e6 },
    };
    const rate = pricing[usage.model] || pricing['gpt-4o'];
    const cost = usage.inputTokens * rate.input + usage.outputTokens * rate.output;

    this.records.push({ ...usage, timestamp: new Date(), cost });

    // 检查是否触发告警
    this.checkAlerts();
  }

  // 生成使用报告
  getReport(period: 'day' | 'week' | 'month' = 'day') {
    const now = new Date();
    const cutoff = new Date(now.getTime() -
      (period === 'day' ? 86400000 : period === 'week' ? 604800000 : 2592000000)
    );

    const recent = this.records.filter(r => r.timestamp >= cutoff);

    const totalInput = recent.reduce((s, r) => s + r.inputTokens, 0);
    const totalOutput = recent.reduce((s, r) => s + r.outputTokens, 0);
    const totalCost = recent.reduce((s, r) => s + r.cost, 0);

    // 按模型分组统计
    const byModel: Record<string, { calls: number; tokens: number; cost: number }> = {};
    for (const r of recent) {
      if (!byModel[r.model]) byModel[r.model] = { calls: 0, tokens: 0, cost: 0 };
      byModel[r.model].calls++;
      byModel[r.model].tokens += r.inputTokens + r.outputTokens;
      byModel[r.model].cost += r.cost;
    }

    return {
      period,
      totalCalls: recent.length,
      totalInputTokens: totalInput,
      totalOutputTokens: totalOutput,
      totalCost: totalCost.toFixed(4),
      avgTokensPerCall: Math.round((totalInput + totalOutput) / (recent.length || 1)),
      byModel,
    };
  }

  private checkAlerts() {
    const todayReport = this.getReport('day');
    const cost = parseFloat(todayReport.totalCost);

    // 日费用超过 $10 告警
    if (cost > 10) {
      const msg = `⚠️ 日 Token 费用已达 $${cost.toFixed(2)},超过 $10 阈值`;
      this.alertCallbacks.forEach(cb => cb(msg));
    }
  }

  onAlert(callback: (alert: string) => void) {
    this.alertCallbacks.push(callback);
  }
}

Token 计数的常见坑点

在实际项目中,我踩过不少 Token 相关的坑,这里总结出来帮大家避坑:

❌ 坑点 1:忘记计算 Message 结构的开销

每条消息不仅有内容 Token,还有角色标识、分隔符等结构开销。GPT-4o 每条消息大约有 4 个 Token 的固定开销,一次 10 轮对话的结构开销就是 40+ Token。

❌ 坑点 2:用错误的模型 Tokenizer 计数

用 GPT-4 的 Tokenizer 去估算 Claude 的成本,偏差可能高达 30%。务必使用模型对应的 Tokenizer。

❌ 坑点 3:忽略 Function Calling 的 Token 消耗

Function 定义本身会消耗大量 Token。一个包含 10 个函数、每个函数有 5 个参数的 Function Calling 定义,可能消耗 2000-3000 个输入 Token。

❌ 坑点 4:JSON 数据没有压缩就发送

开发者习惯发送美化过的 JSON,但那些空格和换行全是 Token。JSON.stringify(data)JSON.stringify(data, null, 2) 平均节省 30-40% 的 Token。

⚡ **关键结论:**Token 优化不是一个一次性的工作,而是一个持续的工程实践。建立 Token 监控体系,定期分析使用模式,持续优化 Prompt 设计——这才是控制 LLM API 成本的正确姿势。

✅ 总结与工具推荐

Token 计数看似简单,实则是 LLM 应用开发中最容易被忽视、却对成本影响最大的环节。掌握 Tokenizer 原理能帮你理解「为什么」,使用精确的计数工具能帮你量化「是多少」,而建立预算管理和监控体系能帮你控制「花多少」。

推荐工具链:

  • 🔧 tiktoken(OpenAI 官方)— GPT 系列模型的精确 Token 计数,有 Python、Rust、WASM 版本
  • 🔧 Anthropic Token Counter — Claude 模型的官方 Token 计数 API
  • 🔧 LLM Tokenizer 在线工具 — 可视化查看文本的 Token 拆分结果,适合调试 Prompt
  • 🔧 Helicone / Langfuse — LLM 可观测性平台,自动追踪 Token 使用量和成本
  • 🔧 jsjson.com 在线工具 — JSON 格式化、压缩、对比,帮你优化发送给 LLM 的 JSON 数据体积

最后记住一个核心原则:Token 优化的终极目标不是用最少的 Token,而是用最合理的 Token 获得最好的输出质量。 过度压缩 Prompt 导致输出质量下降、需要更多轮对话才能完成任务,反而是更大的浪费。

📚 相关文章