前端转 AI 第 38 天:Token 管理与成本优化 - 别让 API 账单爆炸
昨日回顾
Day 37 讲了 Few-shot / CoT / Self-Consistency,让模型会推理。
但推理有代价------token 消耗暴涨。
今天讲"钱"------LLM 应用的成本是实打实的。
Day 38 目标:搞懂 Token 计算,掌握四种省钱策略,写出"成本可控"的 AI 应用。
一、先说个真实的"翻车"故事
Day 31 我第一次调 DeepSeek API 的时候,以为自己很聪明:
python
# 把整个文档塞进去
def summarize(text: str) -> str:
return client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": "总结以下内容"},
{"role": "user", "content": text}, # text 有 5 万字
],
)
测试时没感觉------才几毛钱。
上线后:
- 日活 50 个用户
- 每人平均 20 次调用
- 每次 8 万 token
- DeepSeek 输入价格 ¥0.5/百万
一天成本:
ini
50 × 20 × 80000 / 1000000 × 0.5 = ¥40
一个月:¥1200
如果我用了 GPT-4o 呢?
ini
输入价格约 $2.5/百万 ≈ ¥18/百万
一天:50 × 20 × 80000 / 1000000 × 18 = ¥1440
一个月:¥43200
4 万 3 一个月! 程序员年薪都没这么多。
还好我用的 DeepSeek。但这也敲醒了我------成本必须认真算。
二、Token 是怎么算的
1. 三大误区
误区 1:以为 token ≈ 字符数
❌ 错的:
ini
"Hello" = 5 个字符 = 5 个 token
✅ 对的:
ini
"Hello" = 5 个字符 ≈ 1 个 token
因为 token 是"子词" ------Hello 是一个 token。
误区 2:以为中英文一样
❌ 错的: 中文和英文都是"4 字符 = 1 token"
✅ 对的:
| 语言 | 换算 |
|---|---|
| 英文 | 4 字符 ≈ 1 token |
| 中文 | 1-2 汉字 ≈ 1 token |
| 代码 | 3 字符 ≈ 1 token |
中文的"密度"更高------同样意思,中文的 token 数通常更多。
误区 3:以为 API 返回的 usage.total_tokens 就是"花了多少钱"
❌ 错的: 还要看输入/输出的单价。
✅ 对的:
成本 = prompt_tokens × 输入单价 + completion_tokens × 输出单价
输出通常比输入贵 2-4 倍。
2. 精确计算:tiktoken
tiktoken 是 OpenAI 官方的 token 计算库。
bash
pip install tiktoken
基础用法:
python
import tiktoken
# 用哪个模型的 tokenizer
enc = tiktoken.encoding_for_model("gpt-4o")
# 计算
text = "Hello, world!"
tokens = enc.encode(text)
print(tokens) # [13225, 11, 2375, 0]
print(len(tokens)) # 4
对比不同文本:
python
def count_tokens(text: str, model: str = "gpt-4o") -> int:
enc = tiktoken.encoding_for_model(model)
return len(enc.encode(text))
samples = [
"Hello world", # 英文
"Hello, world!", # 英文带标点
"你好世界", # 中文
"你好,世界!", # 中文带标点
"def hello():\n print('hi')", # 代码
"a" * 100, # 长重复
]
for s in samples:
n = count_tokens(s)
print(f"{len(s):>4} 字符 | {n:>3} token | {s[:30]!r}")
输出:
arduino
11 字符 | 2 token | 'Hello world'
13 字符 | 4 token | 'Hello, world!'
4 字符 | 4 token | '你好世界'
6 字符 | 8 token | '你好,世界!'
27 字符 | 12 token | "def hello():\n print('hi')"
100 字符 | 13 token | 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
几个有意思的发现:
- 中文 4 个字 = 4 个 token(1:1)
- 中文标点也占 token("你好,世界!"多了 4 个)
- 代码的 token 密度介于中英文之间
- 重复字符压缩得好(100 个 a 才 13 token)
这就是"精确计算"的价值------估算能差好几倍。
3. 不同模型的 tokenizer
不同模型用的 tokenizer 不同:
| 模型 | tokenizer |
|---|---|
| GPT-4o / GPT-4 | o200k_base |
| GPT-3.5 | cl100k_base |
| Claude | 自己的 |
| DeepSeek | 自己的 |
| Llama 3 | 自己的 |
tiktoken 只支持 OpenAI 的 tokenizer。
DeepSeek / Claude 的 token 数会稍有差异------但 OpenAI 的估算足够接近。
实践建议:
- 精确计算 GPT :用
tiktoken - 估算其他模型 :用
tiktoken或按经验算 - 账单为准 :API 返回的
usage是唯一标准
三、成本计算:完整公式
1. 基础公式
成本 = 输入 tokens × 输入单价 + 输出 tokens × 输出单价
注意单位是"每百万 token"(per 1M tokens)。
2. 各模型定价(2026 年参考)
| 模型 | 输入(¥/1M) | 输出(¥/1M) | 输出/输入比 |
|---|---|---|---|
| GPT-4o | 18.0 | 72.0 | 4x |
| GPT-4o-mini | 1.1 | 4.4 | 4x |
| Claude 3.5 Sonnet | 21.6 | 108.0 | 5x |
| DeepSeek V3 | 0.5 | 2.0 | 4x |
| 通义千问 Plus | 0.4 | 1.2 | 3x |
| 智谱 GLM-4-Flash | 免费 | 免费 | --- |
| Gemini 2.0 Flash | 0.07 | 0.28 | 4x |
惊不惊喜?
- 最贵和最便宜差 250 倍
- GPT-4o 一次"日常对话" = DeepSeek 的 36 倍
选对模型,就是最大的省钱。
3. 完整成本计算器
创建 day38/cost_calculator.py:
python
"""LLM 成本计算器"""
from dataclasses import dataclass
# 定价表(单位:元 / 1M tokens)
PRICING = {
"gpt-4o": {"input": 18.0, "output": 72.0},
"gpt-4o-mini": {"input": 1.1, "output": 4.4},
"claude-3.5-sonnet": {"input": 21.6, "output": 108.0},
"deepseek-chat": {"input": 0.5, "output": 2.0},
"deepseek-reasoner": {"input": 1.0, "output": 4.0},
"qwen-plus": {"input": 0.4, "output": 1.2},
"glm-4-flash": {"input": 0.0, "output": 0.0},
}
@dataclass
class Usage:
"""一次调用的使用量"""
prompt_tokens: int
completion_tokens: int
model: str
@property
def total_tokens(self) -> int:
return self.prompt_tokens + self.completion_tokens
def cost(self) -> float:
"""计算成本(元)"""
price = PRICING.get(self.model)
if not price:
return 0.0
return (
self.prompt_tokens / 1_000_000 * price["input"]
+ self.completion_tokens / 1_000_000 * price["output"]
)
def calculate_session_cost(
turns: list[Usage],
) -> dict:
"""计算一次完整会话的成本"""
total_cost = sum(u.cost() for u in turns)
total_tokens = sum(u.total_tokens for u in turns)
prompt_tokens = sum(u.prompt_tokens for u in turns)
completion_tokens = sum(u.completion_tokens for u in turns)
return {
"turns": len(turns),
"total_tokens": total_tokens,
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"cost_yuan": round(total_cost, 6),
}
def estimate_monthly(
daily_active_users: int,
calls_per_user: int,
avg_prompt_tokens: int,
avg_completion_tokens: int,
model: str,
) -> dict:
"""估算月度成本"""
u = Usage(
prompt_tokens=avg_prompt_tokens,
completion_tokens=avg_completion_tokens,
model=model,
)
per_call_cost = u.cost()
daily_cost = daily_active_users * calls_per_user * per_call_cost
monthly_cost = daily_cost * 30
return {
"per_call_cost": round(per_call_cost, 6),
"daily_cost": round(daily_cost, 2),
"monthly_cost": round(monthly_cost, 2),
}
if __name__ == "__main__":
print("=== 单次调用成本对比 ===")
print(f"{'模型':<22} {'输入':<8} {'输出':<8} {'成本(元)':<12}")
print("-" * 60)
for model in PRICING:
u = Usage(
prompt_tokens=1000,
completion_tokens=500,
model=model,
)
print(
f"{model:<22} {u.prompt_tokens:<8} {u.completion_tokens:<8} "
f"¥{u.cost():<12.6f}"
)
print("\n=== 月度成本估算 ===")
print("场景:日活 1000,每人每天 10 次调用,平均输入 2000 tokens,输出 500 tokens\n")
for model in ["gpt-4o", "gpt-4o-mini", "deepseek-chat", "qwen-plus"]:
result = estimate_monthly(
daily_active_users=1000,
calls_per_user=10,
avg_prompt_tokens=2000,
avg_completion_tokens=500,
model=model,
)
print(
f"{model:<22} 单次 ¥{result['per_call_cost']:.6f} | "
f"日 ¥{result['daily_cost']:>8.2f} | "
f"月 ¥{result['monthly_cost']:>10.2f}"
)
运行:
yaml
=== 单次调用成本对比 ===
模型 输入 输出 成本(元)
------------------------------------------------------------
gpt-4o 1000 500 ¥0.054000
gpt-4o-mini 1000 500 ¥0.003300
claude-3.5-sonnet 1000 500 ¥0.075600
deepseek-chat 1000 500 ¥0.001500
deepseek-reasoner 1000 500 ¥0.003000
qwen-plus 1000 500 ¥0.001000
glm-4-flash 1000 500 ¥0.000000
=== 月度成本估算 ===
场景:日活 1000,每人每天 10 次调用,平均输入 2000 tokens,输出 500 tokens
gpt-4o 单次 ¥0.081000 | 日 ¥ 810.00 | 月 ¥ 24300.00
gpt-4o-mini 单次 ¥0.004400 | 日 ¥ 44.00 | 月 ¥ 1320.00
deepseek-chat 单次 ¥0.002000 | 日 ¥ 20.00 | 月 ¥ 600.00
qwen-plus 单次 ¥0.001400 | 日 ¥ 14.00 | 月 ¥ 420.00
差异触目惊心:
- GPT-4o:月 ¥24300
- DeepSeek:月 ¥600
- 相差 40 倍
同样的功能,选错模型就是"烧钱"。
四、四种省钱策略
选对模型只是第一步,还要从四个角度优化。
策略 1:Prompt 精简
把 Prompt 里"废话"删掉。
❌ 冗余 Prompt:
erlang
你是一个非常专业、非常友好、非常耐心的 AI 助手,
你的任务是帮助用户解答各种问题。
用户会问你一些问题,你需要认真思考,
然后给出准确、详细、有用的回答。
请记住,你的回答一定要准确,一定要有用,
一定要让用户满意。用户的问题如下:
...
token:约 100 个。其中 80 个是废话。
✅ 精简 Prompt:
erlang
你是专业的 AI 助手。准确、简洁地回答用户问题。
用户问题:
...
token:约 20 个。
省下 80% 的 prompt tokens。
精简原则:
| 删掉 | 保留 |
|---|---|
| 反复强调的形容词 | 明确的行为要求 |
| "请一定"、"务必" | 具体格式说明 |
| 客套话("麻烦你") | 输出约束 |
| 重复的指令 | 具体示例 |
但要注意: 精简过度可能降低效果,要 A/B 测。
策略 2:上下文裁剪
多轮对话里,历史消息越积越多。
第 20 轮时,messages 可能有 1 万 token。
Day 14 讲过记忆裁剪,这里再强调核心策略:
策略 A:滑动窗口
python
def trim_by_window(messages: list[dict], max_turns: int = 10) -> list[dict]:
"""只保留最近 N 轮"""
# system 消息永远保留
system = [m for m in messages if m["role"] == "system"]
others = [m for m in messages if m["role"] != "system"]
return system + others[-max_turns * 2:] # 每轮 user+assistant
策略 B:摘要压缩
python
async def compress_history(messages: list[dict]) -> list[dict]:
"""把早期对话摘要化"""
if len(messages) < 20:
return messages
# 保留 system + 最近 10 条
system = [m for m in messages if m["role"] == "system"]
recent = messages[-10:]
old = messages[len(system):-10]
if not old:
return messages
# 用 LLM 摘要
summary_text = "对话历史摘要:\n" + "\n".join(
f"{m['role']}: {m['content'][:100]}" for m in old
)
summary = {"role": "system", "content": summary_text}
return system + [summary] + recent
策略 C:向量检索(Day 46 讲)
不用全部历史,用向量检索最相关的片段。
效果对比:
| 策略 | 20 轮对话的 tokens |
|---|---|
| 不裁剪 | 10000 |
| 滑动窗口 | 2000 |
| 摘要压缩 | 1500 |
| 向量检索 | 800 |
省 80-90%。
策略 3:Prompt 缓存
OpenAI / DeepSeek 都支持 "Prompt Caching"------重复的 prompt 前缀不重复计算。
原理:
vbnet
如果两次请求的 prompt 前缀相同
→ 缓存命中
→ 输入 token 打折(通常 90% off)
怎么用好?
关键:把"不变的部分"放前面。
❌ 差的顺序:
python
messages = [
{"role": "user", "content": user_question}, # 每次都变
{"role": "system", "content": LONG_INSTRUCTION}, # 不变
]
✅ 好的顺序:
python
messages = [
{"role": "system", "content": LONG_INSTRUCTION}, # 不变,放前面
{"role": "user", "content": user_question}, # 每次都变,放后面
]
同样的对话系统,把 System Prompt 放前面------自动享受缓存。
效果:
一个 5000 token 的 System Prompt:
- 无缓存:每次 ¥0.0025
- 有缓存:每次 ¥0.0005
- 省 80%
对于"高频调用 + 固定 System Prompt"的场景(比如客服机器人),效果巨大。
策略 4:模型分级
不是所有任务都需要最强的模型。
按任务复杂度分级:
| 任务 | 推荐模型 |
|---|---|
| 简单分类、格式化 | DeepSeek / qwen-turbo |
| 普通对话 | deepseek-chat / GPT-4o-mini |
| 复杂推理 | deepseek-reasoner / GPT-4o |
| 超难推理 | Claude 3.5 Sonnet / GPT-4o |
实现"模型路由":
python
class ModelRouter:
"""根据任务选模型"""
def __init__(self, clients: dict):
self.clients = clients # {"cheap": ..., "balanced": ..., "premium": ...}
def route(self, task_type: str, complexity: str = "normal") -> str:
"""选择模型"""
if task_type in ("classify", "extract", "format"):
return "cheap"
if complexity == "hard":
return "premium"
return "balanced"
async def chat(self, messages, task_type: str = "chat", **kwargs):
tier = self.route(task_type)
client = self.clients[tier]
return await client.chat(messages, **kwargs)
用起来:
python
# 简单任务 → 便宜模型
result = await router.chat(messages, task_type="classify")
# 复杂任务 → 贵模型
result = await router.chat(messages, task_type="chat", complexity="hard")
这才是"精打细算"。
五、给 Agent 加成本监控
光"省"没用------得知道"花了多少",才能持续优化。
1. 记录每次调用
升级 RealLLMClient:
python
"""LLM 客户端 - 带成本记录"""
import time
from dataclasses import dataclass
from .observability.logging import get_struct_logger
from .observability.metrics import LLM_COST, LLM_TOKENS
logger = get_struct_logger(__name__)
# 定价表
PRICING = {
"deepseek-chat": {"input": 0.5, "output": 2.0},
"deepseek-reasoner": {"input": 1.0, "output": 4.0},
"gpt-4o-mini": {"input": 1.1, "output": 4.4},
"gpt-4o": {"input": 18.0, "output": 72.0},
"qwen-plus": {"input": 0.4, "output": 1.2},
}
@dataclass
class CallRecord:
"""一次调用的记录"""
model: str
prompt_tokens: int
completion_tokens: int
latency_ms: float
cost: float
def calc_cost(model: str, prompt_tokens: int, completion_tokens: int) -> float:
"""计算成本(元)"""
price = PRICING.get(model, {"input": 0, "output": 0})
return (
prompt_tokens / 1_000_000 * price["input"]
+ completion_tokens / 1_000_000 * price["output"]
)
class CostTrackingLLMClient:
"""带成本追踪的 LLM 客户端(装饰器模式)"""
def __init__(self, inner):
self.inner = inner
self.total_cost = 0.0
self.total_calls = 0
async def chat(self, messages: list[dict], **kwargs) -> str:
# 记录前
start = time.time()
# 调用内层
response = await self.inner.chat_with_usage(messages, **kwargs)
# 记录后
elapsed_ms = (time.time() - start) * 1000
usage = response["usage"]
cost = calc_cost(
model=response["model"],
prompt_tokens=usage["prompt_tokens"],
completion_tokens=usage["completion_tokens"],
)
# 累计
self.total_cost += cost
self.total_calls += 1
# 指标
LLM_COST.labels(model=response["model"]).inc(cost)
LLM_TOKENS.labels(model=response["model"], type="prompt").inc(
usage["prompt_tokens"]
)
LLM_TOKENS.labels(model=response["model"], type="completion").inc(
usage["completion_tokens"]
)
# 日志
logger.info(
"LLM 调用完成",
model=response["model"],
prompt_tokens=usage["prompt_tokens"],
completion_tokens=usage["completion_tokens"],
cost_yuan=round(cost, 6),
cumulative_cost_yuan=round(self.total_cost, 4),
latency_ms=round(elapsed_ms, 2),
)
return response["content"]
def stats(self) -> dict:
"""当前统计"""
return {
"total_calls": self.total_calls,
"total_cost_yuan": round(self.total_cost, 4),
"avg_cost_per_call": round(
self.total_cost / self.total_calls if self.total_calls else 0,
6,
),
}
2. 在 FastAPI 里暴露成本统计
routes.py 加接口:
python
@router.get("/_stats/cost")
def get_cost_stats(llm = Depends(get_llm)):
"""查询成本统计"""
if hasattr(llm, "stats"):
return llm.stats()
return {"error": "当前 LLM 客户端不支持统计"}
访问 /api/_stats/cost:
json
{
"total_calls": 1523,
"total_cost_yuan": 45.67,
"avg_cost_per_call": 0.0299
}
上线后,这个接口就是你每天必看的。
3. 加成本告警
当"日均成本"超过阈值就通知:
python
async def cost_alert_middleware(request, call_next):
"""成本告警中间件"""
response = await call_next(request)
# 每小时检查一次
from . import _last_check
now = time.time()
if now - _last_check.get("ts", 0) < 3600:
return response
_last_check["ts"] = now
# 获取成本
llm = request.app.state.llm
if hasattr(llm, "stats"):
stats = llm.stats()
daily_estimate = stats["total_cost_yuan"] * 24 # 粗略估算
if daily_estimate > 100: # 日成本超 100 元
await send_alert(
f"⚠️ LLM 成本告警:预计日成本 ¥{daily_estimate:.2f}"
)
return response
这就是"成本意识"------不是上线后才想,是设计时就考虑。
4. 按用户 / 会话统计
更细粒度的统计:
python
class CostTracker:
"""成本追踪器(支持多维度)"""
def __init__(self):
self.by_user: dict[str, float] = {}
self.by_model: dict[str, float] = {}
self.by_endpoint: dict[str, float] = {}
def record(
self,
user_id: str,
model: str,
endpoint: str,
cost: float,
):
self.by_user[user_id] = self.by_user.get(user_id, 0) + cost
self.by_model[model] = self.by_model.get(model, 0) + cost
self.by_endpoint[endpoint] = self.by_endpoint.get(endpoint, 0) + cost
def top_users(self, n: int = 10) -> list[tuple[str, float]]:
"""成本最高的用户"""
return sorted(self.by_user.items(), key=lambda x: -x[1])[:n]
def summary(self) -> dict:
return {
"total": sum(self.by_user.values()),
"top_users": self.top_users(5),
"by_model": self.by_model,
"by_endpoint": self.by_endpoint,
}
用途:
- 发现"刷量用户":某个用户成本异常高,可能要限流
- 发现"贵接口":某个 API 成本高,可能可以优化
- 发现"贵模型":某个模型被滥用,可以换更便宜的
这才是"数据驱动"的成本优化。
六、前端视角:LLM 成本 vs 前端"性能预算"
做过前端的你,肯定听过"性能预算":
首屏 JS < 200KB
首屏加载 < 2s
Lighthouse 分数 > 90
LLM 应用也需要"成本预算":
yaml
单次对话 < ¥0.01
日活 1000 用户 < ¥200/天
P95 响应时间 < 5s
对照表:
| 前端 | LLM 应用 |
|---|---|
| Bundle 大小 | Prompt token 数 |
| 首屏加载时间 | 首字延迟 |
| Lighthouse 分数 | Token 效率 |
| 按需加载 | 上下文裁剪 |
| 缓存(SW/Cache) | Prompt Caching |
| CDN | 边缘调用 |
| Tree Shaking | 提示精简 |
| 代码分割 | 模型分级 |
核心心法一样:
不是"越大越好",是"刚好够用"。
前端不追求"bundle 越小越好",而是"功能不减前提下最小"。
LLM 也一样------不追求"token 最少",而是"效果不降前提下最省"。
你已经会的"性能预算"思维,直接迁移到"成本预算"。
七、实战:给助手加成本控制
1. 项目准备
bash
cp -r day37/ai-assistant-cot day38/ai-assistant-cost
cd day38/ai-assistant-cost
2. 加 CostTracker 模块
创建 src/ai_assistant/observability/cost.py:
python
"""成本追踪"""
from collections import defaultdict
from dataclasses import dataclass, field
from threading import Lock
from .logging import get_struct_logger
logger = get_struct_logger(__name__)
# 定价表(元 / 1M tokens)
PRICING = {
"deepseek-chat": {"input": 0.5, "output": 2.0},
"deepseek-reasoner": {"input": 1.0, "output": 4.0},
"gpt-4o-mini": {"input": 1.1, "output": 4.4},
"gpt-4o": {"input": 18.0, "output": 72.0},
"qwen-plus": {"input": 0.4, "output": 1.2},
}
def calc_cost(model: str, prompt_tokens: int, completion_tokens: int) -> float:
"""计算成本"""
price = PRICING.get(model, {"input": 0, "output": 0})
return (
prompt_tokens / 1_000_000 * price["input"]
+ completion_tokens / 1_000_000 * price["output"]
)
@dataclass
class CostStats:
"""成本统计"""
total_cost: float = 0.0
total_calls: int = 0
total_prompt_tokens: int = 0
total_completion_tokens: int = 0
by_model: dict[str, float] = field(default_factory=lambda: defaultdict(float))
by_user: dict[str, float] = field(default_factory=lambda: defaultdict(float))
by_endpoint: dict[str, float] = field(default_factory=lambda: defaultdict(float))
@property
def avg_cost(self) -> float:
return self.total_cost / self.total_calls if self.total_calls else 0
@property
def total_tokens(self) -> int:
return self.total_prompt_tokens + self.total_completion_tokens
class CostTracker:
"""成本追踪器(线程安全)"""
def __init__(self):
self._stats = CostStats()
self._lock = Lock()
def record(
self,
model: str,
prompt_tokens: int,
completion_tokens: int,
user_id: str | None = None,
endpoint: str | None = None,
) -> float:
"""记录一次调用,返回成本"""
cost = calc_cost(model, prompt_tokens, completion_tokens)
with self._lock:
self._stats.total_cost += cost
self._stats.total_calls += 1
self._stats.total_prompt_tokens += prompt_tokens
self._stats.total_completion_tokens += completion_tokens
self._stats.by_model[model] += cost
if user_id:
self._stats.by_user[user_id] += cost
if endpoint:
self._stats.by_endpoint[endpoint] += cost
return cost
def get_stats(self) -> dict:
"""获取统计快照"""
with self._lock:
return {
"total_cost_yuan": round(self._stats.total_cost, 4),
"total_calls": self._stats.total_calls,
"total_tokens": self._stats.total_tokens,
"prompt_tokens": self._stats.total_prompt_tokens,
"completion_tokens": self._stats.total_completion_tokens,
"avg_cost_yuan": round(self._stats.avg_cost, 6),
"by_model": dict(self._stats.by_model),
"top_users": sorted(
self._stats.by_user.items(), key=lambda x: -x[1]
)[:10],
"by_endpoint": dict(self._stats.by_endpoint),
}
def reset(self):
"""重置统计(谨慎使用)"""
with self._lock:
self._stats = CostStats()
# 全局单例
cost_tracker = CostTracker()
3. 集成到 LLM 客户端
修改 llm_real.py:
python
from .observability.cost import cost_tracker
class RealLLMClient:
async def chat(self, messages, user_id=None, endpoint=None, **kwargs):
...
response = await self.client.chat.completions.create(...)
usage = response.usage
# 记录成本
cost = cost_tracker.record(
model=self.model,
prompt_tokens=usage.prompt_tokens,
completion_tokens=usage.completion_tokens,
user_id=user_id,
endpoint=endpoint,
)
logger.info(
"LLM 调用完成",
cost_yuan=round(cost, 6),
**usage.model_dump(),
)
return response.choices[0].message.content or ""
4. 加统计接口
routes.py:
python
from ..observability.cost import cost_tracker
@router.get("/_stats/cost")
def get_cost_stats():
"""查看成本统计"""
return cost_tracker.get_stats()
5. 加预算检查
在 LLM 调用前检查"用户是否超预算":
python
# cost_limits.py
USER_DAILY_LIMIT = 1.0 # 每用户每天最多 ¥1
GLOBAL_DAILY_LIMIT = 500 # 全局每天最多 ¥500
async def check_budget(user_id: str, cost_tracker: CostTracker):
"""检查是否超预算"""
stats = cost_tracker.get_stats()
user_cost = stats["by_user"].get(user_id, 0)
if user_cost > USER_DAILY_LIMIT:
raise HTTPException(429, f"今日额度已用完(已消耗 ¥{user_cost:.2f})")
if stats["total_cost_yuan"] > GLOBAL_DAILY_LIMIT:
raise HTTPException(503, "服务今日额度已用完")
这个机制可以防止"刷量"和"成本失控"。
6. 测试
bash
# 打几个请求
for i in {1..5}; do
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-H "X-API-Key: test-key" \
-d '{"message": "你好"}'
done
# 看成本
curl http://localhost:8000/api/_stats/cost | python -m json.tool
输出:
json
{
"total_cost_yuan": 0.0015,
"total_calls": 5,
"total_tokens": 620,
"prompt_tokens": 420,
"completion_tokens": 200,
"avg_cost_yuan": 0.0003,
"by_model": {"deepseek-chat": 0.0015},
"top_users": [],
"by_endpoint": {}
}
每一次调用的成本,尽在掌握。
八、成本优化的"冷知识"
1. 时间也影响成本
某些厂商的 API 是"错峰定价"------高峰期贵、低谷期便宜。
DeepSeek 曾用过"错峰优惠":夜间调用价格打 2.5 折。
如果你的任务不紧急(比如后台批处理),可以安排到深夜执行。
批量处理场景省 50-75%。
2. 批量调用省 token
如果有多个独立问题,能合并成一个 prompt 就合并。
❌ 逐个调用:
python
for text in texts:
result = llm.chat([{"role": "user", "content": f"分类:{text}"}])
N 次调用的开销:
- N 次 System Prompt
- N 次网络往返
- N 次 API 开销
✅ 批量调用:
python
batch_prompt = "对以下文本分类,返回 JSON 数组:\n" + "\n".join(
f"{i}. {t}" for i, t in enumerate(texts)
)
result = llm.chat([{"role": "user", "content": batch_prompt}])
一次调用处理 N 个文本。
效果:省 30-50% token(因为 System Prompt 只算一次)。
3. 小模型做"初筛"
两步策略:
markdown
1. 便宜模型先判断"这个问题简单吗"
2. 简单 → 便宜模型直接答
3. 复杂 → 转给贵模型
效果:80% 的简单问题用便宜模型处理,成本降 60%。
这就是"级联模型"(Cascade)。
4. 别忽略"输出 token"
输出价格是输入的 2-4 倍。
控制"输出长度"是省钱的关键:
python
response = client.chat.completions.create(
model="deepseek-chat",
messages=[...],
max_tokens=500, # ← 限制输出上限
)
不设 max_tokens ,模型可能输出 2000 字------账单翻 4 倍。
经验值:
| 场景 | 建议 max_tokens |
|---|---|
| 短回答 | 200 |
| 普通对话 | 1000 |
| 长文生成 | 4000 |
| 代码生成 | 2000 |
5. "免费额度"要用足
很多厂商有免费额度:
- 智谱 GLM-4-Flash:完全免费
- Gemini 2.0 Flash:每月 1500 次免费
- 通义千问:新用户 100 万 token
- Groq:Llama 3 免费额度
开发测试用免费额度,上线再接付费 API。
我自己的开发成本接近 0------用免费模型测逻辑,最后一天上付费模型验证。
九、前端视角:成本监控 vs 前端埋点
做过前端的你,成本监控应该一眼看穿:
前端埋点:
js
analytics.track("api_call", {
endpoint: "/api/user",
duration: 320,
status: 200,
user_id: "u-123",
});
成本追踪:
python
cost_tracker.record(
model="deepseek-chat",
prompt_tokens=420,
completion_tokens=200,
user_id="u-123",
endpoint="/api/chat",
)
一模一样的思路------"记录关键指标,聚合分析"。
前端用埋点回答:
- 哪个页面最常访问?
- 哪个接口最慢?
- 哪个用户最活跃?
LLM 应用用成本追踪回答:
- 哪个模型最常用?
- 哪个接口最贵?
- 哪个用户消耗最多?
核心心法:
"用数据说话,而不是靠感觉"。
前端做性能优化------不看数据就是瞎猜。
LLM 做成本优化------不看账单就是烧钱。
你的前端"埋点分析"能力,直接迁移到"成本分析"。
十、今日踩坑
- 以为
usage.total_tokens就是成本 → 输入输出单价不同,要分开算 - 用
tiktoken算 DeepSeek → tokenizer 不同,误差可能 20% - 忘了
max_tokens→ 输出爆炸,账单翻倍 - Prompt 顺序反了 → 变的部分放前面,破坏 Prompt Cache
- 以为 Prompt Cache 免费 → 只是"打折",不是"免费"
- 上下文不裁剪 → 20 轮对话后 token 涨到几万
- 所有任务用最强模型 → 成本是便宜模型的 40 倍
- 不记录成本 → 上线后不知道钱花哪了
- 不设预算上限 → 被恶意刷或 bug 导致天价账单
- 忘了考虑"输出 token"的权重 → 输出比输入贵 2-4 倍
- 成本统计里忘了线程安全 → 高并发下统计错乱
- 忘了
usage在流式响应里 → 需要额外参数stream_options - 单价表硬编码 → 厂商改价要重新部署
- 忽略"免费模型" → 开发测试完全可以用免费的
- 只算平均成本 → 要看 P99,长尾用户可能超支
十一、今日总结
今天核心收获三件事:
- Token 计算要精确 :
tiktoken能用就用,估算可能差几倍 - 四种省钱策略:Prompt 精简 + 上下文裁剪 + Prompt Cache + 模型分级
- 成本监控是必备:记录每次调用、按用户/模型/接口统计、设预算告警
一个感受:
AI 应用和传统应用最大的差异之一:
传统应用:CPU 和内存是"成本",但边际成本递减。
AI 应用:每次调用都是"真金白银",边际成本几乎不变。
用户翻倍,成本就翻倍。
这意味着"成本控制"不是"上线后优化"的事,而是"设计时就要考虑"。
- 选对模型 → 省 90%
- Prompt Cache → 省 80%
- 上下文裁剪 → 省 70%
- 模型分级 → 省 60%
组合使用,能把成本压到 1/10。
从今天起,你不再只是"AI 开发者",而是"可持续的 AI 开发者"。
这是从"Demo"到"产品"的关键一步------只有成本可控,产品才活得下去。
Day 38 完成度:✅
明日预告(Day 39)
- 多模型适配策略 :
- 一套代码适配多家模型
- 模型能力对比(推理 / 代码 / 中文 / 长文本)
- 抽象层设计:接口一致性
- 运行时切换:按场景 / 按用户 / 按成本
- 模型降级的实现细节
最后
如果这篇对你有用,点个赞让我知道有人在看。
评论区互动: 你用 LLM API 的月成本大概多少?有没有被账单吓到过?欢迎分享你的"省钱技巧"。
有问题评论区见,我们第 39 天继续。
100 天,第 39 天见。
附录:系列目录
第一阶段(已完成 ✅)
| 阶段 | 天数 | 内容 |
|---|---|---|
| Python 基础 | 01-15 | 语法 + 命令行助手 |
| 后端工程化 | 16-30 | 数据库/测试/FastAPI/Docker/上线 |
第二阶段:大模型应用(进行中 🎯)
| 天数 | 内容 |
|---|---|
| 31-37 | LLM 入门 + 工程化 + Prompt 工程 + CoT ✅ |
| 38 | Token 管理与成本优化 ✅ |
| 39-40 | 多模型适配 + 本地模型 |
| 41-45 | 阶段复盘 + 项目整合 |
第三阶段:RAG(Day 46-60)
第四阶段:Agent(Day 61-80)
第五阶段:完整项目(Day 81-100)
38 天已过,62 天继续。
一起从切图仔变成 Agent 开发者。