前端转 AI 100 天 Day 38:Token 管理与成本优化——别让 API 账单爆炸

0 阅读20分钟

前端转 AI 第 38 天:Token 管理与成本优化 - 别让 API 账单爆炸

昨日回顾

Day 37 讲了 Few-shot / CoT / Self-Consistency,让模型会推理。

但推理有代价——token 消耗暴涨。

今天讲"钱"——LLM 应用的成本是实打实的。

Day 38 目标:搞懂 Token 计算,掌握四种省钱策略,写出"成本可控"的 AI 应用。


一、先说个真实的"翻车"故事

Day 31 我第一次调 DeepSeek API 的时候,以为自己很聪明:

# 把整个文档塞进去
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/百万

一天成本:

50 × 20 × 80000 / 1000000 × 0.5 = ¥40
一个月:¥1200

如果我用了 GPT-4o 呢?

输入价格约 $2.5/百万 ≈ ¥18/百万
一天:50 × 20 × 80000 / 1000000 × 18 = ¥1440
一个月:¥43200

4 万 3 一个月! 程序员年薪都没这么多。

还好我用的 DeepSeek。但这也敲醒了我——成本必须认真算。


二、Token 是怎么算的

1. 三大误区

误区 1:以为 token ≈ 字符数

❌ 错的:

"Hello" = 5 个字符 = 5 个 token

✅ 对的:

"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 计算库。

pip install tiktoken

基础用法:

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

对比不同文本:

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}")

输出:

   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'

几个有意思的发现:

  1. 中文 4 个字 = 4 个 token(1:1)
  2. 中文标点也占 token("你好,世界!"多了 4 个)
  3. 代码的 token 密度介于中英文之间
  4. 重复字符压缩得好(100 个 a 才 13 token)

这就是"精确计算"的价值——估算能差好几倍。

3. 不同模型的 tokenizer

不同模型用的 tokenizer 不同:

模型tokenizer
GPT-4o / GPT-4o200k_base
GPT-3.5cl100k_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-4o18.072.04x
GPT-4o-mini1.14.44x
Claude 3.5 Sonnet21.6108.05x
DeepSeek V30.52.04x
通义千问 Plus0.41.23x
智谱 GLM-4-Flash免费免费—
Gemini 2.0 Flash0.070.284x

惊不惊喜?

  • 最贵和最便宜差 250 倍
  • GPT-4o 一次"日常对话" = DeepSeek 的 36 倍

选对模型,就是最大的省钱。

3. 完整成本计算器

创建 day38/cost_calculator.py:

"""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}"
        )

运行:

=== 单次调用成本对比 ===
模型                   输入     输出     成本(元)
------------------------------------------------------------
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:

你是一个非常专业、非常友好、非常耐心的 AI 助手,
你的任务是帮助用户解答各种问题。
用户会问你一些问题,你需要认真思考,
然后给出准确、详细、有用的回答。
请记住,你的回答一定要准确,一定要有用,
一定要让用户满意。用户的问题如下:
...

token:约 100 个。其中 80 个是废话。

✅ 精简 Prompt:

你是专业的 AI 助手。准确、简洁地回答用户问题。

用户问题:
...

token:约 20 个。

省下 80% 的 prompt tokens。

精简原则:

删掉保留
反复强调的形容词明确的行为要求
"请一定"、"务必"具体格式说明
客套话("麻烦你")输出约束
重复的指令具体示例

但要注意: 精简过度可能降低效果,要 A/B 测。

策略 2:上下文裁剪

多轮对话里,历史消息越积越多。

第 20 轮时,messages 可能有 1 万 token。

Day 14 讲过记忆裁剪,这里再强调核心策略:

策略 A:滑动窗口

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:摘要压缩

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 前缀不重复计算。

原理:

如果两次请求的 prompt 前缀相同
→ 缓存命中
→ 输入 token 打折(通常 90% off)

怎么用好?

关键:把"不变的部分"放前面。

❌ 差的顺序:

messages = [
    {"role": "user", "content": user_question},       # 每次都变
    {"role": "system", "content": LONG_INSTRUCTION},  # 不变
]

✅ 好的顺序:

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

实现"模型路由":

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)

用起来:

# 简单任务 → 便宜模型
result = await router.chat(messages, task_type="classify")

# 复杂任务 → 贵模型
result = await router.chat(messages, task_type="chat", complexity="hard")

这才是"精打细算"。


五、给 Agent 加成本监控

光"省"没用——得知道"花了多少",才能持续优化。

1. 记录每次调用

升级 RealLLMClient:

"""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 加接口:

@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:

{
  "total_calls": 1523,
  "total_cost_yuan": 45.67,
  "avg_cost_per_call": 0.0299
}

上线后,这个接口就是你每天必看的。

3. 加成本告警

当"日均成本"超过阈值就通知:

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. 按用户 / 会话统计

更细粒度的统计:

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 应用也需要"成本预算":

单次对话 < ¥0.01
日活 1000 用户 < ¥200/天
P95 响应时间 < 5s

对照表:

前端LLM 应用
Bundle 大小Prompt token 数
首屏加载时间首字延迟
Lighthouse 分数Token 效率
按需加载上下文裁剪
缓存(SW/Cache)Prompt Caching
CDN边缘调用
Tree Shaking提示精简
代码分割模型分级

核心心法一样:

不是"越大越好",是"刚好够用"。

前端不追求"bundle 越小越好",而是"功能不减前提下最小"。

LLM 也一样——不追求"token 最少",而是"效果不降前提下最省"。

你已经会的"性能预算"思维,直接迁移到"成本预算"。


七、实战:给助手加成本控制

1. 项目准备

cp -r day37/ai-assistant-cot day38/ai-assistant-cost
cd day38/ai-assistant-cost

2. 加 CostTracker 模块

创建 src/ai_assistant/observability/cost.py:

"""成本追踪"""
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:

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:

from ..observability.cost import cost_tracker


@router.get("/_stats/cost")
def get_cost_stats():
    """查看成本统计"""
    return cost_tracker.get_stats()

5. 加预算检查

在 LLM 调用前检查"用户是否超预算":

# 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. 测试

# 打几个请求
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

输出:

{
  "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 就合并。

❌ 逐个调用:

for text in texts:
    result = llm.chat([{"role": "user", "content": f"分类:{text}"}])

N 次调用的开销:

  • N 次 System Prompt
  • N 次网络往返
  • N 次 API 开销

✅ 批量调用:

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. 小模型做"初筛"

两步策略:

1. 便宜模型先判断"这个问题简单吗"
2. 简单 → 便宜模型直接答
3. 复杂 → 转给贵模型

效果:80% 的简单问题用便宜模型处理,成本降 60%。

这就是"级联模型"(Cascade)。

4. 别忽略"输出 token"

输出价格是输入的 2-4 倍。

控制"输出长度"是省钱的关键:

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 前端埋点

做过前端的你,成本监控应该一眼看穿:

前端埋点:

analytics.track("api_call", {
  endpoint: "/api/user",
  duration: 320,
  status: 200,
  user_id: "u-123",
});

成本追踪:

cost_tracker.record(
    model="deepseek-chat",
    prompt_tokens=420,
    completion_tokens=200,
    user_id="u-123",
    endpoint="/api/chat",
)

一模一样的思路——"记录关键指标,聚合分析"。

前端用埋点回答:

  • 哪个页面最常访问?
  • 哪个接口最慢?
  • 哪个用户最活跃?

LLM 应用用成本追踪回答:

  • 哪个模型最常用?
  • 哪个接口最贵?
  • 哪个用户消耗最多?

核心心法:

"用数据说话,而不是靠感觉"。

前端做性能优化——不看数据就是瞎猜。

LLM 做成本优化——不看账单就是烧钱。

你的前端"埋点分析"能力,直接迁移到"成本分析"。


十、今日踩坑

  1. 以为 usage.total_tokens 就是成本 → 输入输出单价不同,要分开算
  2. 用 tiktoken 算 DeepSeek → tokenizer 不同,误差可能 20%
  3. 忘了 max_tokens → 输出爆炸,账单翻倍
  4. Prompt 顺序反了 → 变的部分放前面,破坏 Prompt Cache
  5. 以为 Prompt Cache 免费 → 只是"打折",不是"免费"
  6. 上下文不裁剪 → 20 轮对话后 token 涨到几万
  7. 所有任务用最强模型 → 成本是便宜模型的 40 倍
  8. 不记录成本 → 上线后不知道钱花哪了
  9. 不设预算上限 → 被恶意刷或 bug 导致天价账单
  10. 忘了考虑"输出 token"的权重 → 输出比输入贵 2-4 倍
  11. 成本统计里忘了线程安全 → 高并发下统计错乱
  12. 忘了 usage 在流式响应里 → 需要额外参数 stream_options
  13. 单价表硬编码 → 厂商改价要重新部署
  14. 忽略"免费模型" → 开发测试完全可以用免费的
  15. 只算平均成本 → 要看 P99,长尾用户可能超支

十一、今日总结

今天核心收获三件事:

  1. Token 计算要精确:tiktoken 能用就用,估算可能差几倍
  2. 四种省钱策略:Prompt 精简 + 上下文裁剪 + Prompt Cache + 模型分级
  3. 成本监控是必备:记录每次调用、按用户/模型/接口统计、设预算告警

一个感受:

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-37LLM 入门 + 工程化 + Prompt 工程 + CoT ✅
38Token 管理与成本优化 ✅
39-40多模型适配 + 本地模型
41-45阶段复盘 + 项目整合

第三阶段:RAG(Day 46-60)
第四阶段:Agent(Day 61-80)
第五阶段:完整项目(Day 81-100)

38 天已过,62 天继续。
一起从切图仔变成 Agent 开发者。