Token 概念与成本

要点

  • Token 是 LLM 处理文本的基本单位,不等于字或词
  • Token 数量直接影响 API 调用成本和响应速度
  • 中文大约 1 个字 = 1-2 个 token,英文大约 1 个单词 = 1-1.5 个 token
  • 成本计算:输入 token + 输出 token,不同价格
  • 控制成本的关键:减少不必要的 token,选择合适的模型
  • 需要监控和限制 token 使用,防止成本失控

1. Token 是什么

1.1 定义

Token 是 LLM 处理文本的基本单位。LLM 不直接处理文字,而是先把文本转换成 token:

文本: "Hello, world!"
Token: ["Hello", ",", " world", "!"]

1.2 Token 不是字

中文和英文的 token 化方式不同:

英文: "Hello world"
Token: ["Hello", " world"]  // 2 个 token
 
中文: "你好世界"
Token: ["你", "好", "世界"]  // 3 个 token

规则:

  • 英文:大约 1 个单词 = 1-1.5 个 token
  • 中文:大约 1 个字 = 1-2 个 token
  • 代码:变量名、关键字各算 token

1.3 为什么是 Token

Token 的设计影响模型的:

  • 词汇量:token 越多,词汇量越大
  • 上下文窗口:以 token 为单位计算
  • 成本:按 token 数量计费
  • 性能:token 越多,处理越慢

2. Token 计算

2.1 使用 Tiktoken

OpenAI 官方的 token 计算库:

import { encoding_for_model } from 'tiktoken'
 
// 为特定模型创建编码器
const encoder = encoding_for_model('gpt-4')
 
// 计算 token 数量
function countTokens(text: string): number {
  return encoder.encode(text).length
}
 
// 示例
console.log(countTokens('Hello, world!'))  // 4
console.log(countTokens('你好世界'))  // 5

2.2 计算消息的 Token

function countMessageTokens(message: Message): number {
  // 每条消息有固定的开销
  let tokens = 4  // role, content 等元数据
 
  tokens += countTokens(message.content as string)
 
  return tokens
}
 
function countMessagesTokens(messages: Message[]): number {
  let total = 0
 
  for (const message of messages) {
    total += countMessageTokens(message)
  }
 
  // 每次请求有固定的开销
  total += 3  // 每次请求的 priming tokens
 
  return total
}

2.3 在线工具

3. 成本计算

3.1 定价模型

LLM API 按 token 计费,分为输入和输出:

// GPT-4 定价(2024 年)
const PRICING = {
  'gpt-4': {
    input: 0.03,   // $/1K tokens
    output: 0.06,  // $/1K tokens
  },
  'gpt-4-turbo': {
    input: 0.01,
    output: 0.03,
  },
  'gpt-3.5-turbo': {
    input: 0.0005,
    output: 0.0015,
  },
}

3.2 成本计算

function calculateCost(
  model: string,
  inputTokens: number,
  outputTokens: number
): number {
  const pricing = PRICING[model as keyof typeof PRICING]
 
  const inputCost = (inputTokens / 1000) * pricing.input
  const outputCost = (outputTokens / 1000) * pricing.output
 
  return inputCost + outputCost
}
 
// 示例
const cost = calculateCost('gpt-4', 1000, 500)
console.log(`Cost: $${cost.toFixed(4)}`)  // $0.0600

3.3 实际成本示例

// 场景 1: 简单问答
// 输入: 100 tokens, 输出: 50 tokens
const cost1 = calculateCost('gpt-4', 100, 50)  // $0.0060
 
// 场景 2: 长对话(10 轮)
// 输入: 5000 tokens, 输出: 500 tokens
const cost2 = calculateCost('gpt-4', 5000, 500)  // $0.1800
 
// 场景 3: 代码审查(大段代码)
// 输入: 20000 tokens, 输出: 2000 tokens
const cost3 = calculateCost('gpt-4', 20000, 2000)  // $0.7200
 
// 场景 4: 使用 GPT-3.5
const cost4 = calculateCost('gpt-3.5-turbo', 20000, 2000)  // $0.0130

4. 成本控制策略

4.1 选择合适的模型

// 简单任务用便宜的模型
if (task.complexity === 'low') {
  return 'gpt-3.5-turbo'
}
 
// 复杂任务用更好的模型
if (task.complexity === 'high') {
  return 'gpt-4'
}

4.2 减少输入 Token

缩短 System Prompt

// ❌ 冗长
const systemPrompt = `
你是一位非常专业的编程导师,你有很多年的经验,
你擅长各种编程语言,包括 JavaScript、Python、Java...
(200 字)
`
 
// ✅ 简洁
const systemPrompt = '你是资深编程导师,擅长用简单语言解释复杂概念。'

压缩历史对话

// 使用滑动窗口,只保留最近 5 轮
const messages = [
  system,
  ...history.slice(-10),  // 只保留最近 10 条消息
  currentUserMessage,
]

摘要旧对话

async function compressHistory(messages: Message[]): Promise<Message[]> {
  if (messages.length <= 10) return messages
 
  const old = messages.slice(0, -10)
  const recent = messages.slice(-10)
 
  const summary = await summarize(old)
 
  return [
    { role: 'system', content: summary },
    ...recent,
  ]
}

4.3 限制输出 Token

const response = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  },
  body: JSON.stringify({
    model: 'gpt-4',
    messages,
    max_tokens: 500,  // 限制输出长度
  }),
})

4.4 缓存相似请求

import { createHash } from 'crypto'
 
const cache = new Map<string, string>()
 
async function callLLMWithCache(messages: Message[]): Promise<string> {
  // 生成缓存 key
  const key = createHash('sha256')
    .update(JSON.stringify(messages))
    .digest('hex')
 
  // 检查缓存
  if (cache.has(key)) {
    return cache.get(key)!
  }
 
  // 调用 API
  const response = await callLLM(messages)
  const result = response.choices[0].message.content
 
  // 存储到缓存
  cache.set(key, result)
 
  return result
}

4.5 批量处理

// ❌ 逐个处理
for (const question of questions) {
  await callLLM([
    { role: 'user', content: question },
  ])
}
 
// ✅ 批量处理
const batchPrompt = `
请回答以下问题:
 
1. ${questions[0]}
2. ${questions[1]}
3. ${questions[2]}
 
请按顺序回答。
`
 
await callLLM([
  { role: 'user', content: batchPrompt },
])

5. Token 监控

5.1 记录 Token 使用

export const tokenUsage = pgTable('token_usage', {
  id: uuid('id').primaryKey().defaultRandom(),
  userId: uuid('user_id').notNull(),
  model: text('model').notNull(),
  inputTokens: integer('input_tokens').notNull(),
  outputTokens: integer('output_tokens').notNull(),
  cost: decimal('cost', { precision: 10, scale: 6 }).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})
 
async function trackUsage(
  userId: string,
  model: string,
  inputTokens: number,
  outputTokens: number
) {
  const cost = calculateCost(model, inputTokens, outputTokens)
 
  await db.insert(tokenUsage).values({
    userId,
    model,
    inputTokens,
    outputTokens,
    cost: cost.toString(),
  })
}

5.2 实时查询

// 查询用户今日使用情况
async function getTodayUsage(userId: string) {
  const today = new Date()
  today.setHours(0, 0, 0, 0)
 
  const usage = await db
    .select({
      totalTokens: sql<number>`sum(${tokenUsage.inputTokens} + ${tokenUsage.outputTokens})`,
      totalCost: sql<string>`sum(${tokenUsage.cost})`,
    })
    .from(tokenUsage)
    .where(and(
      eq(tokenUsage.userId, userId),
      gte(tokenUsage.createdAt, today)
    ))
 
  return usage[0]
}

5.3 配额限制

const QUOTA_LIMITS = {
  free: { tokens: 100000, cost: 3.00 },  // 每月 10 万 token 或 $3
  pro: { tokens: 1000000, cost: 30.00 },
  enterprise: { tokens: -1, cost: -1 },  // 无限制
}
 
async function checkQuota(userId: string, estimatedTokens: number) {
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  })
 
  const plan = user?.plan || 'free'
  const limits = QUOTA_LIMITS[plan as keyof typeof QUOTA_LIMITS]
 
  if (limits.tokens === -1) return true  // 无限制
 
  const usage = await getMonthlyUsage(userId)
 
  if (usage.totalTokens + estimatedTokens > limits.tokens) {
    throw new Error('Token quota exceeded')
  }
 
  if (parseFloat(usage.totalCost) + estimatedCost > limits.cost) {
    throw new Error('Cost quota exceeded')
  }
 
  return true
}

6. 成本优化实践

6.1 使用更便宜的 Embedding 模型

// 向量搜索用便宜的模型
const embedding = await callOpenAI({
  model: 'text-embedding-3-small',  // $0.00002 / 1K tokens
  input: text,
})
 
// 不需要用 GPT-4
// const embedding = await callOpenAI({
//   model: 'gpt-4',  // $0.03 / 1K tokens - 太贵了!
// })

6.2 Prompt 缓存

OpenAI 支持 Prompt Caching,可以节省成本:

// 相同的 system prompt 会被缓存
const systemPrompt = '你是一位专业的编程导师...'
 
// 第一次调用:正常价格
await callLLM([
  { role: 'system', content: systemPrompt },
  { role: 'user', content: '问题 1' },
])
 
// 后续调用:缓存命中,价格降低 50%
await callLLM([
  { role: 'system', content: systemPrompt },  // 缓存命中
  { role: 'user', content: '问题 2' },
])

6.3 流式响应的成本

流式响应不节省 token,但改善用户体验:

// 成本相同,但用户感觉更快
const response = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: { ... },
  body: JSON.stringify({
    model: 'gpt-4',
    messages,
    stream: true,  // 流式响应
  }),
})
 
// 逐字返回给用户
for await (const chunk of response.body) {
  const content = parseChunk(chunk)
  sendToUser(content)
}

6.4 避免不必要的调用

// ❌ 每次都调用 LLM
async function answer(question: string) {
  return await callLLM([{ role: 'user', content: question }])
}
 
// ✅ 先检查缓存或简单规则
async function answer(question: string) {
  // 1. 检查缓存
  const cached = cache.get(question)
  if (cached) return cached
 
  // 2. 简单问题用规则
  if (question === '你好') {
    return '你好!有什么可以帮助你的吗?'
  }
 
  // 3. 复杂问题才调用 LLM
  return await callLLM([{ role: 'user', content: question }])
}

7. 成本分析仪表板

app.get('/api/usage/dashboard', async (c) => {
  const userId = c.get('user').id
 
  // 今日使用
  const today = await getTodayUsage(userId)
 
  // 本月使用
  const month = await getMonthlyUsage(userId)
 
  // 按模型统计
  const byModel = await db
    .select({
      model: tokenUsage.model,
      totalTokens: sql<number>`sum(${tokenUsage.inputTokens} + ${tokenUsage.outputTokens})`,
      totalCost: sql<string>`sum(${tokenUsage.cost})`,
    })
    .from(tokenUsage)
    .where(eq(tokenUsage.userId, userId))
    .groupBy(tokenUsage.model)
 
  // 趋势(最近 7 天)
  const trend = await db
    .select({
      date: sql<string>`date(${tokenUsage.createdAt})`,
      totalCost: sql<string>`sum(${tokenUsage.cost})`,
    })
    .from(tokenUsage)
    .where(and(
      eq(tokenUsage.userId, userId),
      gte(tokenUsage.createdAt, new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
    ))
    .groupBy(sql`date(${tokenUsage.createdAt})`)
    .orderBy(sql`date(${tokenUsage.createdAt})`)
 
  return c.json({
    today,
    month,
    byModel,
    trend,
  })
})

总结

Token 是 LLM 处理文本的基本单位,直接影响成本和性能。控制 token 使用是 AI 应用的关键。

这一节涉及到的几个实践:

  1. Token 概念:不等于字或词,中文约 1 字 = 1-2 token
  2. Token 计算:使用 Tiktoken 库
  3. 成本计算:输入 + 输出 token,不同价格
  4. 成本控制:选择合适模型、减少输入、限制输出、缓存
  5. 监控:记录使用、实时查询、配额限制
  6. 优化:便宜模型、Prompt 缓存、避免不必要调用

Token 管理是 AI 应用的核心。好的 token 管理能显著降低成本,提高性能。

下一篇看温度、Top P 与采样参数——如何控制 AI 输出的随机性和创造性。