Token 计算与成本
做 AI 功能开发,迟早要回答两个问题:这次调用花了多少钱? 以及 要不要向用户收 Token 费? 本篇按两个维度展开——内部成本核算与对外计费——共用同一套计量基础,但目标、精度和工程要求不同。
| 维度 | 目标 | 精度要求 | 典型读者 |
|---|---|---|---|
| 内部成本 | 测试、评测、上线后知道项目真实花费 | 以厂商账单为准,可接受 ±5% 预估误差 | 开发、测试、FinOps |
| 对外计费 | 按 Token 或积分向用户收费 | 必须可审计、可对账、防重复扣费 | 产品、后端、财务 |
Agent / RAG 的上下文拼装见 上下文工程;记忆分层见 Agent 记忆分层实践;框架选型见 RAG 与框架选型。
共同语言:Token 是什么
Token 是模型处理文本的最小计费单位,不等于「一个字」或「一个单词」。中文通常 1 个汉字 ≈ 1–2 token;英文约 4 字符 ≈ 1 token。同一句话,不同模型、不同 tokenizer 结果可能不同。
厂商账单一般拆成:
| 字段 | 含义 | 计费 |
|---|---|---|
prompt_tokens / input_tokens | 发给模型的输入(system、history、RAG 片段、tool schema) | 输入单价 |
completion_tokens / output_tokens | 模型生成的输出 | 输出单价(通常更高) |
reasoning_tokens | o 系列 / 推理模型的「思考」token(部分厂商单独列) | 可能按输出价或专属价 |
cached_tokens | Prompt Cache 命中的输入 token | 通常按折扣价 |
total_tokens | 上述合计(各厂商定义略有差异) | — |
记忆口诀:输入看 Prompt 有多胖,输出看模型说多长;Agent 多轮 = 每轮输入都会重新计费。
两种计量方式
请求前(预估) 请求后(实计)
│ │
├─ tiktoken / 厂商 count API ├─ response.usage(最准)
├─ 拼 Prompt 前算预算 ├─ 流式:最后一个 chunk 的 usage
└─ 截断 RAG / 窗口裁剪 └─ 对账:与账单 CSV 核对
| 方式 | 适用 | 局限 |
|---|---|---|
| 预估 | 发请求前控成本、裁剪上下文、余额预检 | 与实计可能有偏差(特殊 token、tool 序列化) |
| 实计 | 内部报表、对外扣费、财务对账 | 只有请求完成后才知道;失败/重试要单独处理 |
原则:内部成本以 实计 为准;对外计费在 实计 基础上扣用户积分,预估只用于 前置拦截(余额不足则拒绝)。
维度一:内部成本核算
目标:开发、测试、评测、生产环境都能回答「这个功能 / 这次实验花了多少钱」。
1.1 单次 LLM 调用的成本
# cost.py — 最小成本计算
def llm_cost(
model: str,
prompt_tokens: int,
completion_tokens: int,
*,
cached_tokens: int = 0,
price: dict | None = None,
) -> float:
"""price 示例:{"input": 2.5, "output": 10.0, "cached": 1.25} # USD / 1M tokens"""
p = price or PRICES[model]
billable_input = prompt_tokens - cached_tokens
return (
billable_input * p["input"]
+ cached_tokens * p.get("cached", p["input"])
+ completion_tokens * p["output"]
) / 1_000_000
OpenAI Chat Completions 返回 response.usage:
from openai import OpenAI
client = OpenAI()
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "你好"}],
)
u = resp.usage
# u.prompt_tokens, u.completion_tokens, u.total_tokens
cost = llm_cost("gpt-4o", u.prompt_tokens, u.completion_tokens)
流式场景:开启 stream_options={"include_usage": True}(OpenAI),在 最后一个 chunk 取 usage;不要按 chunk 数估算。
1.2 发请求前预估(tiktoken)
import tiktoken
def count_tokens(text: str, model: str) -> int:
enc = tiktoken.encoding_for_model(model)
return len(enc.encode(text))
def count_messages(messages: list[dict], model: str) -> int:
"""Chat 格式有固定 overhead,生产环境建议用厂商 token counting API"""
enc = tiktoken.encoding_for_model(model)
total = 0
for m in messages:
total += 4 # 每条消息的格式开销(近似)
total += len(enc.encode(m.get("content") or ""))
total += 2 # 回复 priming
return total
注意:tool call 的 JSON、多模态图片 token、reasoning token 用 tiktoken 估不准,只能作上限参考;财务口径仍靠 usage。
1.3 整条链路的成本,不只 Chat
| 环节 | 计量对象 | 常见遗漏 |
|---|---|---|
| Embedding | 索引文档 + 每次 query 向量化 | 重建索引的批量费用 |
| Rerank | 按 query + 候选条数 | 与 LLM 分开计价 |
| LLM 生成 | 多轮 Agent 每轮重算输入 | 只统计最后一轮 |
| 摘要 / 抽取 | GraphRAG 建索引时的批量 LLM | 离线 job 未纳入 |
| 评测 | jsonl 全量跑用例 × 多模型 | CI nightly 账单爆炸 |
Agent 循环示例:
用户问题
→ RAG 检索(embedding query)
→ LLM 第 1 轮(system + tools + 问题 + 检索片段) ← prompt 大
→ tool 结果回灌
→ LLM 第 2 轮(history 变长,prompt 更大) ← 重复计费
→ … 直到结束
内部报表应记录:request_id、trace_id、feature(chat/rag/agent/eval)、model、各阶段 token、美元成本、环境(dev/staging/prod)。
1.4 推荐日志结构
{
"request_id": "req_abc",
"trace_id": "trace_xyz",
"user_id": "internal_tester_01",
"feature": "agent_support",
"stage": "llm_turn_2",
"model": "gpt-4o",
"usage": {
"prompt_tokens": 4200,
"completion_tokens": 180,
"cached_tokens": 3000,
"total_tokens": 4380
},
"cost_usd": 0.0021,
"latency_ms": 1200,
"env": "staging"
}
实现路径(按投入从低到高):
| 方案 | 适合 |
|---|---|
| 应用层 middleware 写 DB / 日志 | PoC、小团队 |
| LangSmith、Helicone、Langfuse | 已有 LangChain / 需要 trace 可视化 |
| 统一 LLM Gateway(自建或 LiteLLM) | 多模型、多环境、要集中计价 |
1.5 测试与评测的成本控制
| 做法 | 说明 |
|---|---|
| 用例分级 | smoke 用便宜模型;全量 eval 放 nightly |
| 固定 seed + 缓存 | 相同 Prompt 命中 Prompt Cache |
| 预算上限 | CI job 超 $X 自动 fail |
| Mock LLM | 单测不调真 API;集成测才打真模型 |
| 采样 | 1000 条用例先跑 50 条估单价 × 总量 |
维度二:对外 Token 计费
目标:用户消耗可计量、可充值、可退款、可与厂商账单对账。
2.1 产品形态选型
| 形态 | 用户体验 | 工程复杂度 | 适用 |
|---|---|---|---|
| 积分 / Token 包 | 买 100 万 token,用完再买 | 中 | AI 写作、API 平台 |
| 订阅 + 额度 | 月费含 N token,超出按量 | 中 | SaaS Copilot |
| 纯订阅不限量 | 不暴露 token | 低(但成本不可控) | 早期补贴期 |
| 按次 | 一次对话 = 1 次 | 低 | 简单场景,难反映真实成本 |
对外通常 不直接暴露厂商 raw token,而是 平台积分(如 1 积分 = 1000 token),便于调价而不改 UI。
2.2 定价与加价
用户支付单价 ≥ 厂商成本 × (1 + 毛利率) + 分摊固定成本
| 策略 | 做法 |
|---|---|
| 输入/输出分开定价 | 输出价更高,与厂商一致 |
| 按模型分档 | gpt-4o 积分 ≠ gpt-4o-mini |
| 功能溢价 | Agent(多轮)比单次问答贵 |
| 缓存让利 | 命中缓存的输入给用户折扣,鼓励长 system prompt |
维护一张 价目表(版本化),扣费时 快照 当时单价,避免改价影响历史订单。
2.3 扣费流程
┌─────────────────┐
用户请求 ────────►│ 鉴权 + 余额预检 │─── 不足 ──► 402 / 引导充值
└────────┬────────┘
│ 通过
┌────────▼────────┐
│ 调用 LLM │
└────────┬────────┘
│
┌──────────────▼──────────────┐
│ 读 usage,按价目表算扣减量 │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ 事务:扣余额 + 写流水 │ idempotency_key = request_id
└─────────────────────────────┘
关键设计:
| 点 | 建议 |
|---|---|
| 幂等 | 同一 request_id 只扣一次;重试带相同 key |
| 预扣 vs 后扣 | 预扣(冻结估算上限)适合长 Agent;后扣(按实计)更简单 |
| 失败 | API 5xx 不扣用户;4xx 视业务(用户输入过长可扣或不扣) |
| 流式 | 结束时一次性扣;或按 chunk 累计(复杂,一般不必) |
| 并发 | 余额更新用 DB 行锁或 Redis DECR |
2.4 最小扣费实现(后扣 + 实计)
from dataclasses import dataclass
@dataclass
class UsageRecord:
request_id: str
user_id: str
model: str
prompt_tokens: int
completion_tokens: int
def tokens_to_credits(usage: UsageRecord, price_table: dict) -> int:
p = price_table[usage.model]
raw = (
usage.prompt_tokens * p["input_weight"]
+ usage.completion_tokens * p["output_weight"]
)
return max(1, int(raw)) # 最少扣 1 积分,避免零成本刷接口
def settle_usage(db, usage: UsageRecord, price_table: dict) -> None:
credits = tokens_to_credits(usage, price_table)
with db.transaction():
if db.ledger_exists(usage.request_id):
return # 幂等
if db.get_balance(usage.user_id) < credits:
raise InsufficientCredits()
db.debit(usage.user_id, credits, idempotency_key=usage.request_id)
db.insert_ledger(
request_id=usage.request_id,
user_id=usage.user_id,
credits=credits,
usage=usage,
price_version=price_table["version"],
)
2.5 对账
每月:
Σ 用户扣费积分 × 积分单价 ≈ 收入
Σ 厂商账单 USD ≈ 成本
差异来源:预估误差、缓存折扣未传递、免费额度、内部测试账号、汇率。保留 usage 原始字段 便于与 OpenAI Usage Dashboard / 账单 CSV 逐条核对。
2.6 合规与体验
| 项 | 说明 |
|---|---|
| 用量透明 | 设置页展示本月消耗、剩余积分 |
| 软/硬限流 | 80% 预警,100% 拒绝或降级模型 |
| 退款 | 按流水回滚积分,标记 refund_of |
| 滥用 | 同一 IP / 账号异常高频 → 限流 |
常见坑
| 坑 | 现象 | 处理 |
|---|---|---|
| 只统计最后一轮 | Agent 成本低估 5–10 倍 | 每轮 LLM 调用都打点 |
| tiktoken 当财务口径 | 与账单差 1–5% | 对外扣费只用 usage |
| 忽略 embedding | RAG 问答「很便宜」直到索引重建 | embedding 单独记账 |
| 重试双扣 | 用户被扣两次 | request_id 幂等 |
| 中英文混排估错 | 预算截断过早/过晚 | 按语言分桶统计校准 |
| Tool schema 膨胀 | system 越来越大 | 按需加载工具;监控 prompt 增长 |
| 测试打生产 Key | 账单惊现 | 环境隔离 Key + 预算告警 |
快速对照
你要干什么?
│
├─ 知道测试/评测花了多少钱
│ → 实计 usage 写日志 + 按模型单价汇总 + CI 预算
│
├─ 上线后看各功能成本
│ → trace 按 feature 聚合 + 告警
│
└─ 向用户收 Token 费
→ 平台积分 + 后扣实计 + 幂等流水 + 月对账
一句话:内部求 准(实计 + 全链路);对外求 稳(实计扣费 + 幂等 + 可审计)。预估只负责 别超支,不负责 结账。