更新于

AI API 预算上限自动化设计:防止账单爆炸的工程实践


有过这样的经历吗:某个周末,一个循环出了 bug,模型在无人值守的情况下反复被调用,等你周一发现时,账单已经超出预期十倍?这不是小概率事件——在 AI API 使用越来越普遍的今天,“预算失控”已经成为一类真实的工程风险。

本文从工程角度系统设计 AI API 的预算上限机制,目标是让账单超支在造成损失之前就被阻断,而不是事后报警。

一、为什么 AI API 特别容易预算失控

传统 API(如短信、地图)通常按次计费,单次成本固定可预测。AI API 的特殊性在于:

  • 计费单位是 Token,而 Token 数量取决于输入长度 × 模型选择,两者都可能在运行时剧烈变化
  • 上下文膨胀:多轮对话中,每一轮都要把历史消息带上,Token 消耗随对话轮数呈线性甚至平方增长
  • 无边界的 prompt 注入:如果用户输入直接拼入 prompt,恶意或异常用户可以输入超长文本,撑爆单次 Token 消耗
  • 异步任务的静默失控:批处理 job 在无人监控时持续运行,出错后重试逻辑若没有上限,会指数级消耗

这些特性让 AI API 的成本曲线天然比普通 API 更陡峭,需要专门设计的预算控制层。

二、预算控制的四个层次

健全的预算控制体系应该覆盖四个层次,从最细粒度到最粗粒度依次递进:

层次粒度控制目标
请求层单次 API 调用防止单次请求 Token 异常
用户层单用户/单租户防止单一用户耗尽配额
功能层业务模块防止某个功能失控
全局层整个应用防止总账单超出预算

每一层的控制机制不同,但都可以抽象为”计数 → 比较 → 熔断”三步。

三、请求层:Token 预检与上限注入

在发送 API 请求之前,先预估本次请求的 Token 消耗,并注入 max_tokens 参数约束输出长度。

import tiktoken

enc = tiktoken.encoding_for_model("gpt-4o")

def safe_chat_completion(
    client,
    messages: list[dict],
    max_input_tokens: int = 4000,
    max_output_tokens: int = 1000,
) -> dict:
    # 预估输入 Token
    input_text = " ".join(m["content"] for m in messages)
    input_tokens = len(enc.encode(input_text))

    if input_tokens > max_input_tokens:
        raise ValueError(
            f"Input too long: {input_tokens} tokens (limit: {max_input_tokens})"
        )

    return client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        max_tokens=max_output_tokens,  # 强制输出上限
    )

关键点max_tokens 是请求层最重要的安全阀。不设置 max_tokens 时,模型可能输出数千甚至上万 Token 的内容,单次成本直接爆炸。

对于多轮对话场景,还需要控制历史消息的长度:

def trim_messages(messages: list[dict], max_tokens: int = 3000) -> list[dict]:
    """从最旧的消息开始裁剪,保留最新上下文"""
    total = 0
    result = []
    for msg in reversed(messages):
        tokens = len(enc.encode(msg["content"]))
        if total + tokens > max_tokens:
            break
        result.insert(0, msg)
        total += tokens
    return result

四、用户层:令牌桶限速

用户层预算控制的核心是**令牌桶(Token Bucket)**算法——每个用户有一个 Token 配额桶,每次 API 调用按实际消耗扣减,配额耗尽后拒绝服务直到次日重置。

import redis
import time

class UserTokenBudget:
    def __init__(self, redis_client: redis.Redis, daily_limit: int = 100_000):
        self.redis = redis_client
        self.daily_limit = daily_limit

    def _key(self, user_id: str) -> str:
        date = time.strftime("%Y-%m-%d")
        return f"token_budget:{user_id}:{date}"

    def check_and_consume(self, user_id: str, tokens: int) -> bool:
        key = self._key(user_id)
        pipe = self.redis.pipeline()
        pipe.incrby(key, tokens)
        pipe.expire(key, 86400)  # 次日自动过期
        used, _ = pipe.execute()

        if used > self.daily_limit:
            # 超限,回滚
            self.redis.decrby(key, tokens)
            return False
        return True

    def get_remaining(self, user_id: str) -> int:
        key = self._key(user_id)
        used = int(self.redis.get(key) or 0)
        return max(0, self.daily_limit - used)

注意这里用 Redis Pipeline 保证原子性——INCRBYEXPIRE 必须一起执行,否则在高并发下可能出现超扣。

五、全局层:滑动窗口成本计量

全局预算需要跟踪实时累计费用,而不只是 Token 数(因为不同模型单价差异很大)。

from decimal import Decimal

# 模型单价表(USD per 1K tokens,近似值,仅作参考)
MODEL_PRICING = {
    "gpt-4o": {"input": Decimal("0.005"), "output": Decimal("0.015")},
    "gpt-4o-mini": {"input": Decimal("0.00015"), "output": Decimal("0.0006")},
    "claude-sonnet-4-6": {"input": Decimal("0.003"), "output": Decimal("0.015")},
}

def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> Decimal:
    pricing = MODEL_PRICING.get(model, MODEL_PRICING["gpt-4o"])
    return (
        pricing["input"] * input_tokens / 1000
        + pricing["output"] * output_tokens / 1000
    )

class GlobalBudgetGuard:
    def __init__(self, redis_client: redis.Redis, monthly_limit_usd: Decimal):
        self.redis = redis_client
        self.limit = monthly_limit_usd

    def _key(self) -> str:
        month = time.strftime("%Y-%m")
        return f"global_cost:{month}"

    def record_and_check(self, cost: Decimal) -> bool:
        key = self._key()
        # 将 Decimal 转为微分存储(精度到 0.000001 USD)
        micro = int(cost * 1_000_000)
        total_micro = self.redis.incrby(key, micro)
        self.redis.expire(key, 86400 * 32)

        total = Decimal(total_micro) / 1_000_000
        return total <= self.limit

六、告警与熔断:三级响应机制

只有”阻断”是不够的——当预算接近上限时,系统需要提前告警,给人工干预留出时间。推荐三级响应机制:

阈值触发条件动作
黄色当月消费达到预算 70%发送 Slack/钉钉告警,日志记录
橙色当月消费达到预算 90%关闭非核心功能的 AI 调用,告警升级
红色当月消费达到预算 100%熔断全部 AI API 调用,只保留缓存响应
from enum import Enum

class BudgetLevel(Enum):
    NORMAL = "normal"
    WARNING = "warning"    # 70%
    CRITICAL = "critical"  # 90%
    CIRCUIT_OPEN = "circuit_open"  # 100%

def get_budget_level(used: Decimal, limit: Decimal) -> BudgetLevel:
    ratio = used / limit
    if ratio >= 1.0:
        return BudgetLevel.CIRCUIT_OPEN
    elif ratio >= 0.9:
        return BudgetLevel.CRITICAL
    elif ratio >= 0.7:
        return BudgetLevel.WARNING
    return BudgetLevel.NORMAL

在 API 调用入口处检查 BudgetLevelCIRCUIT_OPEN 时直接返回降级响应(如”服务暂时不可用,请稍后重试”)。

七、成本分配与归因

多业务线或多功能模块的应用,需要知道每个模块贡献了多少成本,才能做出有效的优化决策。

import contextvars

# 用 contextvars 传递请求上下文,无需修改函数签名
cost_attribution = contextvars.ContextVar("cost_attribution", default="unknown")

def track_cost(func):
    """装饰器:记录函数调用产生的 API 成本"""
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        module = cost_attribution.get()
        usage = result.usage  # OpenAI response usage 字段
        cost = calculate_cost(
            result.model,
            usage.prompt_tokens,
            usage.completion_tokens,
        )
        # 写入成本数据库或 Prometheus 指标
        record_module_cost(module, float(cost))
        return result
    return wrapper

# 使用时
cost_attribution.set("chat_feature")
response = tracked_chat_completion(client, messages)

把各模块的成本数据接入 Grafana,就能看到实时的成本分布热力图,快速定位”哪个功能最烧钱”。

八、异步任务的特殊处理

批处理任务(数据清洗、批量摘要)是最容易失控的场景,因为它们在后台运行且通常量大。几个额外的安全措施:

任务级预算预分配:在任务启动时预先估算总 Token 消耗,如果超出可用预算,直接拒绝提交。

任务内计数器:每处理一条记录就累计消耗,超过任务级上限时主动中止,不等到任务结束才发现超支。

重试次数硬上限:对失败请求的重试次数设置绝对上限(如 3 次),避免无限循环耗尽预算。

关于 AI 编程工具成本控制的实际案例,可以参考AI 编程工具成本控制实战

九、与 Prompt Caching 结合降低基础成本

预算控制的另一面是主动降低成本。OpenAI 和 Anthropic 都提供 Prompt Caching 机制——重复的 system prompt 或上下文在缓存命中时费用大幅下降(约 10%–25% 的原价)。

在设计系统 prompt 时,将固定内容放在最前面,让 Cache 命中率最大化。这与预算控制结合,能有效降低整体账单基线。详细技巧参见Prompt Caching 成本优化实践

十、小结

AI API 的预算控制不是一次性配置,而是需要分层设计的工程系统:

  • 请求层max_tokens 和输入截断防止单次爆炸
  • 用户层用令牌桶隔离不同用户的消耗
  • 全局层用滑动窗口跟踪实时成本并触发熔断
  • 可观测性层用成本归因定位高消耗模块

把这四层都落地,“账单超出预期十倍”这类事故基本可以规避。

相关阅读

如果需要跨多家 AI 提供商统一管理预算,YoTradeApi 支持在中转层设置全局用量上限,一个接口管控 Claude、GPT-4o、Qwen 等多模型的费用,避免分散管理带来的盲区。