在 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 w、l o w e r、n 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
第三轮:发现 est 和 l 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 导致输出质量下降、需要更多轮对话才能完成任务,反而是更大的浪费。