Skip to content

9.5 成本控制

在上一节中,我们讨论了如何通过降级和容错让系统在故障中存活。但有一个问题贯穿始终,也是所有 Agent 产品负责人最关心的:这个系统每天烧多少钱?

在传统软件开发中,服务的主要成本来自服务器、带宽和人力,边际成本很低——多一个用户不过多消耗一点带宽。但 Agent 系统完全不同:每次用户交互都在消耗 Token,而 Token 直接对应金钱。 用户越多,成本越高,而且是线性甚至超线性增长的。

9.5.1 为什么成本控制是 Agent 系统的核心挑战?

让我们算一笔账。

假设一个智能客服 Agent,每天处理 10,000 次对话,每次对话平均 3 轮交互,每轮消耗约 2,000 input tokens + 500 output tokens:

  • 使用 GPT-4o($2.50/1M input, $10/1M output):

    • 日均成本 = 10,000 × 3 × (2000 × 2.50/1M + 500 × 10/1M) = $300/天
    • 月均成本 ≈ $9,000/月
  • 如果 30% 的请求可以通过缓存或简单模型处理:

    • 月均成本降至 ≈ $3,000~$4,000/月

生活类比:传统软件的成本就像开一辆电车——充电费很便宜,开多开少差别不大。Agent 系统的成本就像开出租车——每跑一公里都在烧油,而且油价还不便宜。你得精打细算每一趟行程。

这就是成本控制的巨大价值。对于一个拥有百万用户的 Agent 产品,成本可以从每月数十万美元降至数万美元。

9.5.2 Token 用量监控

Token 计算基础

不同模型使用不同的 Tokenizer,但大多数情况下可以用 tiktoken 库进行估算:

python
import tiktoken                                  # 导入 OpenAI 的 Token 计数库

encoding = tiktoken.encoding_for_model("gpt-4o")  # 获取 GPT-4o 的编码器

text = "你好,这是一个测试。Hello, this is a test."
tokens = len(encoding.encode(text))              # 编码并计算 Token 数
print(f"文本: '{text}'")                         # 打印原始文本
print(f"Token 数: {tokens}")                     # 打印 Token 数

# 中文通常 1 个字符 ≈ 1.5~2 个 tokens
# 英文通常 1 个单词 ≈ 1.3 个 tokens
成本追踪中间件
python
import time                                      # 导入时间模块
import json                                      # 导入 JSON 处理
from typing import Dict, List, Optional          # 导入类型注解
from dataclasses import dataclass, field         # 导入数据类
from datetime import datetime, timedelta        # 导入日期时间
import redis.asyncio as aioredis                 # 导入异步 Redis
from fastapi import FastAPI, Request, Response   # 导入 FastAPI
from pydantic import BaseModel                   # 导入数据模型

@dataclass
class TokenUsage:
    """Token 用量记录"""
    model: str                                  # 使用的模型
    prompt_tokens: int                          # 输入 Token 数
    completion_tokens: int                       # 输出 Token 数
    total_tokens: int                           # 总 Token 数
    cost: float                                 # 本次调用成本(美元)
    timestamp: float = field(default_factory=time.time)  # 时间戳
    user_id: str = "anonymous"                  # 用户 ID
    endpoint: str = ""                           # 调用端点

class CostTracker:
    """成本追踪器"""
    
    # 各模型价格(每百万 tokens,美元)
    MODEL_PRICES = {
        "gpt-4o": {"input": 2.50, "output": 10.00},       # GPT-4o
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},   # GPT-4o-mini
        "gpt-4-turbo": {"input": 10.00, "output": 30.00}, # GPT-4-Turbo
        "claude-3-5-sonnet": {"input": 3.00, "output": 15.00},  # Claude
        "claude-3-haiku": {"input": 0.25, "output": 1.25},     # Claude Haiku
        "deepseek-chat": {"input": 0.14, "output": 0.28},       # DeepSeek
        "deepseek-reasoner": {"input": 0.55, "output": 2.19},   # DeepSeek-R1
    }
    
    def __init__(self, redis_url: str = "redis://localhost"):***@staticmethod
    def compress_context(messages: List[dict], max_tokens: int = 2000) -> List[dict]:
        """压缩上下文:从最近的消息开始保留"""
        compressed = []                           # 压缩后的消息列表
        current_tokens = 0                        # 当前 Token 计数
        
        # 从最近的消息开始保留(最近的最重要)
        for msg in reversed(messages):            # 逆序遍历
            msg_tokens = len(tiktoken.encoding_for_model("gpt-4o").encode(msg["content"]))
            
            if current_tokens + msg_tokens > max_tokens:  # 超出预算
                # 如果放不下,尝试截断用户消息
                if msg["role"] == "user":
                    content = msg["content"][:int(len(msg["content"]) * 0.5)]  # 保留前半
                    compressed.append({"role": msg["role"], "content": content + "..."})
                break                            # 停止处理更早的消息
            
            compressed.append(msg)               # 完整保留
            current_tokens += msg_tokens
        
        return list(reversed(compressed))         # 恢复正序
    
    @staticmethod
    def optimize_system_prompt(prompt: str) -> str:
        """优化 System Prompt:去除多余空格和重复说明"""
        prompt = " ".join(prompt.split())         # 压缩多余空白
        
        lines = prompt.split("。")               # 按句号分割
        seen = set()                              # 已见句集合
        unique_lines = []
        for line in lines:                        # 去重
            if line.strip() and line.strip() not in seen:
                seen.add(line.strip())
                unique_lines.append(line.strip())
        
        return "。".join(unique_lines)            # 重新拼接
    
    @staticmethod
    def truncate_tool_results(results: str, max_chars: int = 500) -> str:
        """截断工具调用结果"""
        if len(results) <= max_chars:            # 未超限
            return results
        return results[:max_chars] + f"\n...(截断,原始长度 {len(results)} 字符)"

9.5.6 成本优化全景图

┌─────────────────────────────────────────────────────────────┐
│                    成本优化策略全景                           │
├───────────────┬──────────────┬───────────────┬───────────────┤
│   策略         │   预期节省    │   实现难度     │   适用场景     │
├───────────────┼──────────────┼───────────────┼───────────────┤
│ Prefix Cache  │   30-50%     │   零(自动)   │ 长 System Prompt│
│ Response Cache│   10-30%     │   低           │ FAQ/重复问题   │
│ Semantic Cache│   20-40%     │   高           │ 通用问答       │
│ 模型分层      │   40-70%     │   中           │ 所有场景       │
│ Prompt 压缩   │   15-30%     │   低           │ 长上下文       │
│ 限制输出长度  │   10-20%     │   低           │ 所有场景       │
│ 批量处理      │   5-15%      │   中           │ 后台任务       │
└───────────────┴──────────────┴───────────────┴───────────────┘

常见误区

  1. "成本控制就是用便宜模型"——便宜模型的能力也弱,简单任务可以用,复杂任务用了反而需要更多轮交互,最终成本更高。模型分层是关键:按任务复杂度选模型。

  2. "Token 计数不精确没关系"——小误差累积起来可能偏差很大。特别是长上下文场景,1,000 个请求每个偏差 100 Token,一天就差 10 万 Token,对应几美元的误差。

  3. "预算告警设了就够了"——告警只是第一步,更重要的是自动响应。当成本达到 80% 时应该自动切换到便宜模型,达到 100% 时应该熔断非核心请求。

  4. "Prompt 长一点无所谓"——每 1,000 个 Token 的输入成本约 $0.0025(GPT-4o)。如果 System Prompt 有 5,000 Token,每次调用多花 $0.0125。一天 10,000 次调用就是 $125——仅 System Prompt 就占了月成本的 $3,750。

  5. "缓存命中率不影响成本"——缓存命中率每提高 10%,成本就降低约 10%。监控缓存命中率并持续优化是成本控制的关键手段。

  6. "不需要按用户追踪成本"——不同用户的成本差异可能很大。一个高频用户可能消耗 80% 的成本。按用户追踪成本有助于设计差异化定价策略或设置配额。

本节小结

要点说明
Token 计费了解各模型的 input/output 价格,这是成本控制的基础
实时监控按小时/天/用户维度追踪 Token 用量和成本
模型分层简单任务用便宜模型,复杂任务用贵模型,能节省 40~70%
缓存复用Prefix Cache + Semantic Cache 是成本节省的利器
Prompt 优化压缩上下文、截断工具结果、优化 System Prompt
预算告警设置每日预算上限,80% 警告,100% 熔断

成本控制让系统的运行变得可持续。但还有一个问题影响用户体验:用户在等待 LLM 响应的几秒甚至几十秒里,看到的是什么? 如果只是一个旋转的加载图标,再好的系统也会让人觉得"慢"。下一节我们讨论流式输出——如何让用户在等待中就能看到内容逐渐生成,体验"实时打字"的效果。


下一节:9.6 流式输出 Streaming——SSE 与 WebSocket,让用户在等待中看见进展。