Grok 预算护栏怎么落地
预算事故很少是「用超了」,而是「没人拦」。一个负责导出的接口,平时每次几千 token,某天用户传了一份 300 页的合同,同一天又跑了三遍重试——单次请求跨过 20 万阈值,整包按高档计费,一天把月度预算用掉一半。事后复盘,所有人都说自己没做错,因为系统里根本没有一道会自动说「不」的关卡。
这一节把护栏做成可离线测的三层结构:单次上限、日累计、月累计,并明确每一层超限时该降级还是该拒绝。
一、问题不是「预算够不够」,而是「超限时做什么」
讨论预算,容易停在「一个月给多少额度」。但对工程来说,真正要定义的是触发之后的行为:
- 单次请求预估成本超上限 → 是拒绝,还是先截断上下文再重算?
- 今日累计超日预算 → 是换更便宜的模型继续服务,还是直接报错?
- 本月累计超月预算 → 是硬停,还是只允许低优先级任务?
三档如果不分开,就会出现两种坏结果:要么一超限全站不可用,要么哪一档都不生效、只在月底发一封邮件。所以护栏的第一件事不是设数字,是给每一档配一个动作。
二、最小可运行示例:三层预算守卫
下面的脚本不联网、不需要真实密钥。单价与阈值沿用前面的口径,重点在「判定顺序」与「累计只在放行时发生」。
"""三层预算护栏(mock):单次上限、日累计、月累计,各自配一个动作。"""
from dataclasses import dataclass
PRICE_LOW = {"in": 2.0, "cached": 0.5, "out": 6.0}
PRICE_HIGH = {"in": 4.0, "cached": 1.0, "out": 12.0}
THRESHOLD = 200_000
def estimate(prompt: int, cached: int, output: int) -> float:
"""按 20 万阈值整包跳档的规则估算单次成本(USD)。"""
tier = PRICE_HIGH if prompt >= THRESHOLD else PRICE_LOW
return ((prompt - cached) / 1e6) * tier["in"] \
+ (cached / 1e6) * tier["cached"] \
+ (output / 1e6) * tier["out"]
@dataclass
class Guard:
per_request_usd: float
daily_usd: float
monthly_usd: float
spent_today: float = 0.0
spent_month: float = 0.0
def check(self, prompt: int, cached: int, output: int) -> tuple[str, float]:
"""返回 (动作, 预估成本)。动作只有 allow / degrade / reject 三种。"""
cost = estimate(prompt, cached, output)
if cost > self.per_request_usd:
return "reject", cost
if self.spent_today + cost > self.daily_usd:
return "degrade", cost
if self.spent_month + cost > self.monthly_usd:
return "reject", cost
# 只有放行才记账;降级与拒绝由上层决定是否另算
self.spent_today += cost
self.spent_month += cost
return "allow", cost
def offline_check() -> None:
g = Guard(per_request_usd=0.5, daily_usd=1.0, monthly_usd=10.0)
cases = [
(100_000, 0, 1_000), # 常规请求
(100_000, 0, 1_000), # 再来一次,累计接近日上限
(250_000, 0, 2_000), # 跨阈值长请求,超单次上限
(150_000, 0, 1_000), # 仍在日预算内
(150_000, 0, 1_000), # 会顶到日上限,应降级
]
for i, (p, c, o) in enumerate(cases, 1):
action, cost = g.check(p, c, o)
print(f"{i} {action} ${cost:.4f}")
print(f"累计:今日 ${g.spent_today:.4f} / 本月 ${g.spent_month:.4f}")
if __name__ == "__main__":
offline_check()
真正上线时,check() 放在发请求之前,用预估而不是事后账单;预估的输入是 token 计数,输出是美元。降级动作由调用方实现,守卫只负责给结论——这样护栏逻辑可以单测,不掺业务分支。
三、三道护栏的触发条件与动作
| 护栏 | 触发条件 | 建议动作 | 容易漏的地方 |
|---|---|---|---|
| 单次上限 | 预估单次成本 > 上限 | 拒绝,或截断上下文后重算 | 只按平均 token 估,漏掉长尾 |
| 日预算 | 今日累计 + 本次 > 日上限 | 降级到便宜模型或短输出 | 降级后忘了重新记账 |
| 月预算 | 本月累计 + 本次 > 月上限 | 拒绝并告警,保留缓冲 | 把月预算当日预算用 |
三件要强调的事:
- 判定顺序是「单次 → 日 → 月」,不能反。先看单次,是因为一次异常长请求能在月预算还很宽时就烧掉一大块;先卡住它,日/月累计才有意义。
- 降级也要记账。切到便宜模型后,成本变小但不是零;如果降级路径绕过了
check(),账就会漏。 - 不要假设平台自带配额。控制台是否有原生限额、限额怎么生效,以官方页与控制台当前显示为准;本节讲的是应用侧护栏,它和平台能力是两层,不能互相替代。
四、预期输出与异常分支
运行守卫,预期输出:
1 allow $0.2060
2 allow $0.2060
3 reject $1.0240
4 allow $0.3060
5 degrade $0.3060
累计:今日 $0.7180 / 本月 $0.7180
换成真实流量时,若出现异常,按下面顺序定位:
| 现象 | 优先怀疑 | 第一步动作 |
|---|---|---|
| 该拒的没拒 | 预估低估了 token | 打印预估输入,对比实际用量 |
| 该降的没降 | 日累计没更新 | 核对放行分支是否都记账 |
| 降级后仍然超 | 降级路径绕过守卫 | 把降级请求也走 check() |
| 月初就告警 | 月预算被当日预算 | 检查两个计数是否共用变量 |
最后一行尤其重要:degrade 和 reject 都不记账,是本节刻意的设计——它们的成本由上层策略决定(换模型后的新请求会重新过守卫)。如果直接把「被拒绝请求的预估成本」也计进累计,账会虚高,护栏会提前触发。
五、判据卡
| 判断项 | 该这么做 | 不该这么做 |
|---|---|---|
| 数字设定 | 三档分开,各配动作 | 只设一个月度总额 |
| 判定顺序 | 单次 → 日 → 月 | 先看月总额 |
| 降级实现 | 降级请求重新过守卫 | 降级路径绕开记账 |
| 预估口径 | 用 token 数与分档单价 | 用历史平均值一刀切 |
| 平台能力 | 应用侧护栏 + 控制台核对 | 假定平台会自动兜底 |
这张卡的重点是给每一档配动作。四类现象里「该拒没拒」和「降级后仍超」常常同源:预估或记账有缺口,表现一个是漏拦一个是漏算,但修法都在守卫的入口与出口上。
六、来源与核验
- 官方定价页:
https://docs.x.ai/developers/pricing(核验日期 2026-11-04)。<200k与≥200k两档单价、缓存价以该页为准;本节单价为示例值,实际以官方页与控制台当前显示为准。 - 官方模型页:
https://docs.x.ai/developers/models(核验日期 2026-11-04)。20 万阈值「整请求所有 token 按高档计费」的规则来自该页注释(见 4.3)。 - 平台是否提供原生配额、限额与告警,以及其生效方式,请以官方页与控制台当前显示为准;本节不虚构控制台功能。
七、动手试
- 给
Guard加一个degrade_model参数:当日预算触发时返回建议模型,并说明为什么降级后的请求仍要重新调用check()。 - 把
estimate()的输入改成「本轮实际 token + 上一轮缓存命中量」,验证命中缓存能否把单次成本压到上限以下,并解释为什么这只影响单价、不影响是否跨阈值。 - 设计一条「月预算剩余 20%」的预警规则:写出触发条件、通知对象与自动动作,并说明为什么预警不能直接等同于硬停。