给按量计费装三道闸:后台设预算、代码里熔断、每天对一次账
按量计费最狠的地方不是贵,是它不会在你花超的那一刻叫你。
订阅制下你最多亏一个月固定费;按量计费下,一个循环写错的 agent 可以在你睡觉的时候连着跑 8 小时。等你早上看到账单,钱已经花完了,而且你不知道花在哪。
一人公司没有运维岗,这件事只能靠结构解决。下面是三层,从外到内,每一层都能单独先上。
第一道:平台侧,把预算和”停手”绑在一起
Vercel 的 Spend Management(Pro 和 Enterprise Flexible Commitment 才有,Owner 或 Billing 角色可操作):
- 团队面板进 Settings → Billing
- 找到 Spend Management,打开开关
- 设 On-Demand Budget,单位是美元,按计费周期算
- 选到达预算时要发生的动作:通知、webhook、暂停所有项目的生产部署
第 4 步里那个暂停开关是整套东西的关键。官方文档写得毫不含糊:设置金额本身不会停止用量,只有打开 Pause Production Deployments,Vercel 才会在到达金额时把项目停掉。
50%、75%、100% 三档会自动发邮件和站内通知;100% 可以额外开短信。webhook 会在同样的三个点发 POST,请求头带 x-vercel-signature,记得校验。
这道闸有三个明确的漏洞,你必须知道:
- 检查不是实时的。 Vercel 每隔几分钟查一次,通知、webhook、暂停都可能在你跨过金额几分钟后才触发。所以预算要设在”你真正能承受的上限”之下,留出这段延迟的余量。
- 只停生产部署。 AI Gateway 的 API key 用量和 v0 用量既不受暂停影响,又继续计入预算。也就是说,你以为停了,账单还在涨——如果你的 agent 走的是 AI Gateway,这道闸等于只有通知功能。
- 不会自动恢复。 触发后每个项目得手动恢复,就算你把预算调高也不会自动解开。暂停期间访客看到的是 503
DEPLOYMENT_PAUSED。
另外,预算只覆盖按量资源。席位、Marketplace 上的集成、单独的 add-on 都不算进去。
第二道:代码里放一个计量器
平台那道闸管的是”总额到了就停”。它管不了”这条任务该不该继续”,也管不了跑在别处的脚本。这一层要自己写,20 行就够。
// scripts/budget.mjs
// 用法:在每次调用模型前 import { charge },超预算直接抛错
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
const FILE = '.budget.json';
const DAY_CAP = Number(process.env.DAY_BUDGET_USD || '2'); // 每天最多花多少
const HARD_CAP = Number(process.env.HARD_BUDGET_USD || '30'); // 一个周期最多花多少
const PRICE = { input: 0.0000024, output: 0.0000096 }; // 每 token 单价,换成你在用的模型
function load() {
if (!existsSync(FILE)) return { day: new Date().toISOString().slice(0, 10), daySpent: 0, total: 0 };
const s = JSON.parse(readFileSync(FILE, 'utf8'));
const today = new Date().toISOString().slice(0, 10);
return s.day === today ? s : { day: today, daySpent: 0, total: s.total };
}
export function charge(usage, tag = 'unknown') {
const s = load();
const cost = (usage.input_tokens || 0) * PRICE.input + (usage.output_tokens || 0) * PRICE.output;
s.daySpent += cost;
s.total += cost;
writeFileSync(FILE, JSON.stringify(s, null, 2));
if (s.daySpent > DAY_CAP) throw new Error(`[budget] 今日已用 ${s.daySpent.toFixed(2)} 美元,超过日上限 ${DAY_CAP},任务 ${tag} 终止`);
if (s.total > HARD_CAP) throw new Error(`[budget] 累计 ${s.total.toFixed(2)} 美元,超过总上限 ${HARD_CAP}`);
return cost;
}
用起来就是在每次拿到响应之后 charge(res.usage, 'nightly-digest')。
两个阈值是分开的,这是故意的:日上限防”一夜跑飞”,总上限防”每天都在合理范围但加起来超支”。只设总量上限的话,一个失控的循环能在一天之内把整个月的额度烧完。
进程要真的停下来,不能只打日志。抛出的错误别被上层 try/catch 吞掉,定时任务里的重试次数也要设上限——否则熔断触发一次,调度器又拉起来一次,等于没熔断。
第三道:每天对一次账
前两道都是”超了怎么办”,这道是”还没超,但方向不对”。
每天跑一次,把 .budget.json 的内容按 tag 汇总,输出昨天花得最多的三个调用来源,以及”本月已用 / 预算”的百分比,推到你真会看的地方(邮件、微信、Slack 都行)。
它的价值不在数字,在于逼你每天看一眼。多数失控在第一天就有征兆——某个 tag 的消耗突然翻倍——但没人看的话,要等账单出来才发现。
怎么知道自己做对了
5 条,全过才算装完:
- 演练过一次。 把 On-Demand Budget 临时设成”当前已用 + 1 美元”,确认暂停动作真的发生、通知真的收到(三选一测通:邮件、短信、webhook)。没演练过的防线等于没有。
- 熔断后退出码非 0。 手动把
DAY_BUDGET_USD设成 0.0001 跑一次,确认脚本退出码是 1 而不是 0,且调度器不会无限重试。 - 预算数字低于你的真实上限。 平台检查有几分钟延迟,AI Gateway 和 v0 又不在暂停范围内,所以预算值必须留出这两块的余量。
- 代码里没有绕过计量器的调用。 全项目搜一遍对模型端点的
fetch,每一处都得经过同一个包装函数。漏一处,整套结构就漏了。 - 日报每天真的到你手上,且你能说出昨天最贵的调用是哪个。 说不出来说明 tag 打得不对,回去改 tag。
几个坑
- 别把预算设成心理上限。 检查延迟加上”暂停管不到的那部分用量”,实际花销会超过你设的数。设 80% 比较稳。
- webhook 一定要验签。 它带
x-vercel-signature,是 SHA 签名。不验的话任何人都能 POST 你的端点,触发你写好的”应急动作”。 - 单价表要自己维护。 上面脚本里那两个数字是示例,换成你实际在用的模型,并且每次换模型都要更新。价格写错,熔断阈值就全错了。
- API key 级别的额度是另一回事。 平台预算管的是团队总量,管不了单把 key 被人拿去用。给每个 agent、每个环境单独发 key 并设额度,别让它们共用——那部分之前在”给每个 AI agent 单独发一张令牌”里写过,两件事要配套做。
- 暂停不是免费。 生产部署停了,你的网站就真的不能访问了。对有真实客户的业务,“被自己的预算把线上停掉”本身就是一种事故,所以第一道闸的金额要设得比第二道宽。
最后一句
这三道闸加起来大概花两个小时。它换来的不是省钱,是你可以放心让 agent 在后台跑——因为你确定它跑飞的时候会有人(或者有脚本)把它摁住。一个人干活,能放心睡觉这件事本身值钱。