用 Python 给每位 AI 用户设置花费门禁
教程用约 200 行 Python 标准库代码和 SQLite,在模型 API 调用前检查用户是否已超出预算。费率由开发者自行提供,演示金额使用明确标注的虚构价格。
AI 深度解读
AI 功能上线后,费用日志能告诉你钱花在哪,却拦不住下一次调用。一个重试脚本、一段失控的 Agent 循环,就可能让单个用户持续消耗预算。原文给出的办法很直接:把按用户、按功能计费的预算检查,放到模型 API 前面。
这套方案采用 Python 标准库和 SQLite,调用前估算、调用后记实账,适合作为轻量费用控制的起点。不过,演示金额全部是假定费率算出的结果,输入里的核心代码也没有贴完整。它能说明设计思路,距离生产环境的硬性限额还有几处需要补齐。
为什么记了费用,还要在调用前设一道关
费用看板、日报和日志回答的是「已经花了多少」。要控制失控循环,还得回答另一个问题:下一次请求是否允许发出去?
作者把每次调用拆成三个步骤:先用 check() 检查预算,再请求模型,最后用 record() 保存真实 token 数和费用。run_guarded() 则负责把这三个步骤串起来。
检查时,系统把本月已花金额与本次预估费用相加。如果超过限额,就抛出 BudgetExceeded,让业务代码决定如何回应用户。这样,一个不断重复的低价请求,也会遇到明确的停止条件。
预算控制必须参与请求执行,才能阻止后续费用继续产生。
这里有两套数字,不能混用。调用前的估算负责判断能不能放行;调用后拿到的真实用量负责入账,下次检查使用的是实际支出。估算偏高,会提前拒绝请求;估算偏低,本次请求仍可能超出剩余额度,事后记账只能影响后续调用。
因此,这个设计更接近「按估算准入、按实际结算」。原文把它描述成限制失控消费的方法是成立的,但不能据此认定账户绝不会超出预算。
用户总额和功能额度,应该一起检查
预算的标识由 user_id 和 scope 组成。scope 是业务已有的功能名称,例如 chat、summariser、agent。同一个用户可以分别设置聊天额度和 Agent 额度。
特殊值 "*" 表示用户总预算,每次调用都要检查。功能预算与总预算同时存在时,哪个先耗尽,哪个就阻止后续请求。这适合限制昂贵功能,同时保留账户整体的费用边界。
没有配置预算的用户,默认不会被阻止。作者希望团队可以先给重度用户、昂贵功能或试用档位设置限额,再逐步扩大范围。这降低了接入时的影响,但也有明确前提:漏配预算就等于无限额,团队需要知道哪些调用方理应受到控制。
演示中,alice 的 chat 月预算是 $0.012,每次调用费用为 $0.0035。前三次累计分别为 $0.0035、$0.0070、$0.0105。第四次的预估会把总额推到 $0.0140,因此在请求发出前被拒绝。
bob 的预算是 $0.004,一次调用后花费 $0.0035,占比为 87.5%,输出显示为 88%,超过 80% 告警线。不过,如果下一次仍预估花费 $0.0035,按前述规则就应该拒绝。原文没有展示这次告警检查传入的估算值,不能把它理解成「达到 88% 后,下一次正常请求仍然可以放行」。
这些金额来自作者刻意简化的假定费率,既不是供应商报价,也不是实际应用的测试数据。
月度窗口则从 UTC 当月第一天开始,由 _month_start_utc() 计算。跨月后,旧记录自动落在统计窗口之外,无须定时清零,也不会删除历史账目。国内产品如果承诺北京时间每月一日恢复额度,就需要调整边界,否则界面承诺与实际恢复时间会错开。
接入 SDK 不复杂,完整计费链路才是重点
作者描述的实现约两百行,需要 Python 3.8 或更新版本,只依赖标准库中的 sqlite3 和 datetime。默认数据库文件为工作目录里的 budgets.db,测试可传入 ":memory:"。
但输入中的 budget_guard.py 在 estimate_cost() 的加法表达式处中断,没有给出完整的 check()、record()、run_guarded() 和演示程序。原文提供了 python3 budget_guard.py 命令,当前这份代码却无法直接保存运行。这是材料缺失,不能当作已经验证可用的完整实现。
真实 SDK 的接入示例比较清楚:让调用函数返回响应,以及服务商报告的输入、输出 token 数,再交给包装器结算。
def make_call():
response = client.chat.completions.create(
model="your-model-id", messages=messages)
return (response,
response.usage.prompt_tokens,
response.usage.completion_tokens)
try:
response, cost, warn = guard.run_guarded(
user_id, "chat", make_call,
est_input_tokens, est_output_tokens,
PRICE_IN, PRICE_OUT)
except BudgetExceeded:
return "You have used this month's AI budget. It resets on the 1st."
要看的是 response.usage.prompt_tokens 和 response.usage.completion_tokens:它们才是实际记账的依据。est_input_tokens、est_output_tokens 用于事前准入,PRICE_IN、PRICE_OUT 则需要接入方根据供应商价格填写。示例中的 your-model-id 也是待替换值。
作者建议优先使用 tokenizer 估算输入量,也可以先用字符数粗估;难以估算时采取保守值,不应直接跳过检查。不过,最终输出长度仍有不确定性,保守估算也会带来提前拒绝的代价。
流式响应需要等结束、拿到最终用量后再记账,不能把每个 chunk 都算成一次完整调用。实际接入时,还要确认所用 SDK 如何返回 usage,以及中断后如何处理缺失用量,原文没有展开。
当前计费只区分输入和输出。如果供应商对缓存输入单独收费,就要为 record()、estimate_cost() 增加第三类 token 数和费率。费率错误时,保存的 token 数仍有用,但预算金额会失真。
数据库只保存用户标识、功能标签、token 数、费用和时间戳,不保存提示词与回答。这样的记录足以支撑预算判断,也减少了额外保存业务内容的需要。
我的判断
我认同它把费用控制放在业务入口,并让用户总预算与功能预算共同生效。相比只做请求次数限流,金额预算能反映长提示词、长输出和不同模型之间的费用差异;但次数、并发和 Agent 步数限制仍有作用,金额预算并不能替代它们。
最需要补的是并发一致性。如果两个请求同时读取相同的剩余额度,都可能通过检查,完成后再一起入账。调用前查余额、调用后写费用,并不能自动构成并发安全的硬限额。
作者建议多 worker 环境改用 Postgres 或 MySQL,但更换数据库不会自动消除这个时间窗口。需要严格限额时,还应考虑原子预留额度、完成后结算和释放差额,以及请求失败后的恢复。这些是生产化补充,原文没有提供实现。
告警也类似。返回一个 warning 字符串,不等于实现「首次跨过 80% 时通知」;若没有记录告警状态,后续检查可能反复通知。
所以,它更适合作为低并发场景的轻量起点。原文没有提供生产压测、超额幅度或替代方案的对照数据,「容易接入、便于改造」应当按教程经验理解,不能延伸成生产可靠性的证明。
能拿来做什么
- 在前端聊天或 Agent 产品的服务端入口统一检查额度,并将
BudgetExceeded转成清楚的业务提示,说明恢复时间;前端据此展示状态,实际拦截由服务端执行。 - 先为最昂贵的功能设置独立
scope,同时配置"*"总预算,并核对哪些账户没有预算记录,避免把漏配当成有意不限额。 - 在 Node 或 Python 的 SDK 包装层统一收集实际 usage,流式请求结束后结算;接入时同时核对费率、缓存输入分类和预算时区。
- 多 worker 上线前,用并发请求验证剩余额度不足时的行为;若业务要求严格限额,补上额度预留与失败恢复,再评估是否满足要求。