codecamp

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)。
  • 平台是否提供原生配额、限额与告警,以及其生效方式,请以官方页与控制台当前显示为准;本节不虚构控制台功能。

七、动手试

  1. 给 Guard 加一个 degrade_model 参数:当日预算触发时返回建议模型,并说明为什么降级后的请求仍要重新调用 check()。
  2. 把 estimate() 的输入改成「本轮实际 token + 上一轮缓存命中量」,验证命中缓存能否把单次成本压到上限以下,并解释为什么这只影响单价、不影响是否跨阈值。
  3. 设计一条「月预算剩余 20%」的预警规则:写出触发条件、通知对象与自动动作,并说明为什么预警不能直接等同于硬停。
Grok 缓存前缀与 cache key
Grok 账单对账与异常定位
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }