2814 字
约 9 分钟
15
S22-A06 · AI 应用成本控制:预算、配额与降级

月底账单是上月的三倍,但没人说得出钱花在哪。这是 AI 应用最典型的成本事故:不是单价涨了,是分母失控——输入变长、输出变长、重试变多、检索条数变多、评测轮次变多。成本控制因此不是"砍功能",而是在设计阶段把三件事定死:钱按什么口径归集、超支时系统自动做什么、降级后用户看到什么。

范围先划清:这里说的是按 API 计费的 token 账;自建推理的 GPU 与显存账是另一本,判据完全不同。


1. 成本结构:账单上的六个格子

看不到结构,就只能砍功能。先把成本拆成可归集的项:

成本项 计费口径 增长驱动 可控手段
输入 token 按千 token 计 系统提示、检索片段、历史轮次 裁剪、摘要、检索条数上限
输出 token 单价常高于输入(按厂商价目核对,常见 2–5 倍量级) 要求长回答、JSON 冗余、重复解释 输出格式约束、max_tokens
缓存命中 命中部分按折扣价 提示前缀是否稳定 前缀固定、模板版本冻结
重试 每次重试都计费 超时、限流、解析失败 幂等重试、失败次数上限
检索 存储 + 查询 QPS top_k、过滤条件、索引规模 降 top_k、先过滤后检索
评测跑量 用例数 × 轮次 × 模型 回归频率、模型切换次数 分层用例、抽样回归

最小记账单元必须落在每次调用上,而不是落在"本月总消耗"上:

{
  "request_id": "req_9f1c",
  "tenant_id": "t_1024",
  "feature": "chat.summary",
  "prompt_version": "p_2026_03",
  "model": "[MODEL]",
  "input_tokens": "[N]",
  "cached_tokens": "[N]",
  "output_tokens": "[N]",
  "retry_index": 0,
  "latency_ms": "[N]",
  "cost": "[COMPUTED]"
}

retry_index 这一列是最容易被省掉的,也是账单对不上估算的头号原因:失败的重试同样计费,而监控里通常只统计成功请求。


2. 预算的三个层级与四种动作

预算不挂在"应用"这个整体上,因为超支时没人知道该找谁。三层是能落地的最小结构:

层级 谁负责 观察窗口 超支后的默认动作
公司级 技术负责人 + 财务 月 冻结新功能上线、重谈单价
功能级 产品 + 功能 owner 日 降级模型、收紧配额
租户/用户级 系统自动 分钟 / 日 排队、拒绝、提示升级
budget:
  company:
    window: month
    limit: "[AMOUNT]"
    on_exceed: freeze_new_feature
  feature:
    key: "chat.summary"
    window: day
    limit: "[AMOUNT]"
    on_exceed: degrade_model
  tenant:
    window: day
    limit_tokens: "[N]"
    burst: "[N]/10min"
    on_exceed: [queue, then_reject]

四种超支动作的取舍,按效果损失从小到大排,不要按"实现简单"排:

  1. 减少上下文:砍历史轮次、砍检索条数、截断工具返回。损失通常最小,且可逆。
  2. 降级到小模型:效果下降因任务而异。分类、抽取、摘要通常可接受;审核、金额计算、法务表述通常不可接受。
  3. 排队:只对可异步的入口成立(报表、批量摘要)。用户等三分钟和拿到差答案是两回事。
  4. 拒绝:唯一对实时交互入口也成立的动作。拒绝要给明确原因和恢复时间,否则等于把成本问题变成投诉问题。

降级链写成声明式配置,别散在 if 里:

DEGRADE_CHAIN = [
    ("model-large", 32_000),   # 正常档
    ("model-large", 12_000),   # 先裁上下文
    ("model-small", 12_000),   # 再换模型
]

判断顺序很关键:先做损失小的动作。先换模型再裁上下文,等于用两次效果损失换一次本可以避免的损失。


3. 配额:硬限、软限与突发额度

配额和预算不是一回事:预算是钱,配额是速率。混在一起写,最后两个都失效。

维度 类型 作用 越界表现
每分钟 token 硬限 防突发打爆上游速率限制 直接拒绝,返回可重试提示
每日 token 软限 控总账 降级 + 通知 owner
每租户并发 硬限 防单租户挤占 排队或拒绝
突发额度 令牌桶 允许短时冲高 桶空后回到基础速率
def admit(tenant, cost):
    if not minute_bucket.try_take(tenant, cost):   # 硬限:先判
        return Decision.REJECT
    if day_usage(tenant) + cost > soft_limit:      # 软限:后判
        return Decision.DEGRADE
    return Decision.ALLOW

顺序写反会出一个很别扭的现象:软限已经降级,但分钟硬限仍在拒,用户同时看到"降级"和"拒绝"两种反馈。计数必须走共享存储(Redis 计数或集中限流器),本地内存计数在多实例下会把真实配额放大到实例数倍。


4. 缓存与上下文裁剪:成本收益与风险

缓存是唯一"不损失效果就能省钱"的手段,前提是命中条件足够严:

缓存类型 命中条件 收益 主要风险
精确缓存 请求体哈希一致 确定性高,重复问答直接归零 命中率低,长尾请求无效
前缀缓存 系统提示前缀逐字节一致 单价折扣,工程成本低 模板加时间戳或版本号即失效
语义缓存 向量相似度超过阈值 命中率高 答非所问,且错得很难发现

语义缓存的判据只有一条:有没有评测集守门。有,可以按功能白名单开;没有,先别开。强上下文依赖的场景(多轮追问、按当前页面内容作答)天然不适合语义缓存——相似问题在上下文不同时答案不同。

上下文裁剪的收益比换模型更稳,也更容易做错:

def build_context(history, files, budget_tokens):
    ctx = [SYSTEM_PROMPT]
    ctx += retrieve(query, top_k=min(5, budget_tokens // 800))
    ctx += summarize(history) if len(history) > N_TURNS else history
    ctx += [f for f in files if f.relevant]   # 只带必要文件
    return truncate(ctx, budget_tokens)

两个反模式:把整份文档塞进去(token 涨十倍,效果未必涨);摘要做得太早(三轮就开始摘,模型丢掉关键约束)。摘要的触发点应当由"上下文是否已超出有效窗口"决定,而不是由"省多少钱"决定。


5. 重试、失败与批处理:被低估的放大器

一次失败不是"没花钱",而是"花了钱还没结果"。设单次失败率 p、最多重试 k 次,平均每次成功请求的计费次数约为 1/(1-p) 量级——p=20% 时接近 1.25 倍,再叠加超时前的等待成本,账单会比按成功请求估算的高出一截。

控制手段:只对可重试错误重试(超时、429、5xx),解析失败不要原样重试而要改提示;重试间隔退避,且设总时长上限;在请求标签里记 retry_index,让账单能按"第几次尝试"拆开看。

批处理是另一条路:离线任务走批量接口,单价通常显著更低,代价是延迟从秒级变成小时级。判据很直接——结果是否需要即时反馈。日报摘要、历史回溯打标、评测跑量都适合;对话、审核、实时推荐都不适合。


6. 降级可用性、可观测最小集与治理落点

降级路径要先回答"效果下降是否可接受",再回答"要不要告知用户":

场景 可降级 是否告知
长文摘要 是 可静默,但在结果页标注模型档位
客服问答 是,需保留转人工 必须告知,否则用户会反复追问
内容审核 否,降级应改为"进人工队列" 不涉及用户
金额/合同类输出 否 不涉及用户

可观测的最小集只有四列:tenant_id、feature、model、prompt_version,加上每次调用的 input_tokens / output_tokens。缺 prompt_version,模板一改就无法解释成本跳变;缺 feature,就只能按应用整体看总数。

治理落点:预算要挂到功能 owner,不是挂到"AI 平台组"。超支处理走变更流程(谁批、批多少、多久复盘),而不是临时口头放宽——口头放宽一次,后面所有配额都会被当成建议值。


7. 排错:五个对不上账的现象

现象 原因 处理
账单远高于按成功请求的估算 重试与失败调用计费,未归集 补 retry_index,按尝试次数拆分
配额没到却被拒 多实例本地计数,或分钟桶与日桶口径不一致 计数移到共享存储,统一窗口定义
缓存命中率突然掉很低 模板加了时间戳/请求 ID,前缀不再一致 前缀冻结,动态内容后置
降级后大量"答非所问"投诉 降级链先换模型、后裁上下文 调换顺序,并给降级档单独评测
成本归因不到租户 标签只在入口打,异步链路丢失上下文 标签随任务透传,异步任务带原始标签

8. 边界:什么时候不必上这套

情况 判断 做法
调用量小、单价低 治理成本高于省下的钱 只记账,不设配额
内部试验期 拦请求会阻断验证 只告警,不拦截
单模型单功能 三层预算退化成一层 只留功能级预算
强实时交互 排队不可用 只保留拒绝与降级两条路径
上游已提供组织级配额 重复建设 直接用上游配额,自建只做归集

一句话判据:先能回答"这笔钱属于谁、为什么花",再谈拦不拦。 归集做不到,配额只会变成随机报错。


9. 自测

  1. 账单比按成功请求估算高出一大截,最先该补哪个字段?
  2. 三层预算里,哪一层的超支动作必须是自动的,为什么?
  3. 分钟硬限与日软限的判定顺序写反,用户会看到什么异常组合?
  4. 语义缓存上线前,唯一必须准备好的东西是什么?
  5. 降级链为什么应当"先裁上下文、后换模型"?
  6. 什么情况下上配额反而增加总成本?
S22-A06 · AI 应用成本控制:预算、配额与降级
https://www.clxhxhhr.top/posts/6140/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。