上线AI功能前,先建一道「开销熔断器」

免费AI额度仍是预算,需在代码中先算Token再算钱的熔断器。
文章以一个独立开发者周末耗尽免费额度的虚构故事切入,指出免费套餐并不消除预算,只是把计价单位从美元换成Token。作者提出用一个刻意保持无聊的熔断器来防止失控:仅追加账本负责记账,调用前闸门在预算用掉九成时关闭并返回确定性降级结果,调用后结算即使请求失败也要记录消耗的输入Token。模型调用被放在薄适配器后,避免锁定单一供应商。通过离线桩模型和可注入时钟的测试,可以在几毫秒内验证闸门确实会关闭。方案适合单进程独立应用,不适合多区域部署或高吞吐服务,且免费额度本身是持续变化的移动靶。
一个独立开发者在周五下午上线了一个AI按钮,把杂乱的客户留言整理成三条要点。演示效果不错,周末照常开始,没人盯着这个接口。
到了周一早上,这个接口已经处理了数万次调用。一个Worker的重试循环卡在超时上反复请求。免费额度耗尽,后续每次请求都在烧真金白银。
故事里的数字是示意性的,但失败的结构不是。免费套餐从不消除预算,只是把计价单位从美元换成了Token和CPU。当AI功能在凌晨三点失控时,谁在买单?对独立开发者来说,答案必须写进代码里,而不是靠人的注意力。

吃掉免费额度的循环
大多数第一版代码长这样:
// worker.mjs
async function summarize(note) {
while (true) {
try {
return await callModel(note);
} catch {
// retry forever, because "free"
}
}
}
这个循环没有刹车。一次超时、一个429或后端变慢,就能把一次用户操作变成数千次计费调用。服务商乐意一直应答直到配额耗尽,然后收费或拒绝。
开源项目MonkeyCode提供免费模型访问和免费服务器选项,均由运营方提供,免费Token额度目前标称为1000万。依赖任何具体数字前请先核对该项目的最新条款,因为额度和限制会变化。(披露:本文为MonkeyCode产品推广的一部分。)
免费额度终究是预算,只不过花的是Token而非美元,并且按别人的时间表刷新。因此熔断器先算Token,再算钱。在任何独立项目的第一天,这个顺序都很关键。
指数退避(Exponential Backoff)与抖动
示例中的 while (true) 之所以危险,是因为它隐含了零延迟、无上限的重试假设。生产环境里,应对 429 或 5xx 的标准做法是指数退避:第一次重试等待约 1 秒,此后每次按 2 的幂增长,并设置一个最大重试次数。单纯的退避仍会制造"惊群"——大量客户端在同一时间点齐步重试,把瞬时故障放大成持续过载。因此在退避间隔上叠加随机抖动(jitter),让每次等待落在 [base, 2×base] 之类的区间内,是常见组合。
对熔断器而言,退避解决的是"不要自己压垮服务商",而有界重试解决的是"不要让一次用户操作变成数千次计费调用"。两者必须同时存在:只有退避而没有上限,Token 消耗仍会在长时间故障中线性累积;只有上限而没有抖动,则可能在多个 Worker 之间形成周期性的账单尖峰。文章建议"重试两次后降级",正是把这两个约束落到具体数字上。
三块拼图:账本、闸门、结算
熔断器很小,刻意保持无聊。一个仅追加的账本、一个调用前的闸门、一个调用后的结算。
// spend.mjs
const TOKEN_BUDGET = Number(process.env.TOKEN_BUDGET ?? "200000");
export function estimateTokens(text) {
// 粗略启发式:英文散文约4字符1个Token
return Math.ceil(String(text).length / 4);
}
export function gate(text, now = new Date()) {
const estIn = estimateTokens(text);
const s = spendToday(now);
const projected = s.in + s.out + estIn + EST_OUT;
if (projected >= TOKEN_BUDGET * 0.9) {
return { ok: false, reason: "token-budget", estIn, ...s };
}
return { ok: true, estIn, ...s };
}
闸门在预算用掉九成时关闭,而不是等到百分之百。闸门关闭后仍要回答用户,所以包装层提供一个确定性的降级结果,而不是错误页。
export async function guardedSummarize(note, { callModel, now }) {
const verdict = gate(note, now);
if (!verdict.ok) {
settle({ route: "fallback", in: 0, out: 0, usd: 0, reason: verdict.reason }, now);
return { text: cheapFallback(note), route: "fallback" };
}
try {
const res = await callModel(note);
settle({ route: "model", in: res.usage?.input_tokens ?? verdict.estIn, ... }, now);
return { text: res.text, route: "model" };
} catch (err) {
settle({ route: "error", in: verdict.estIn, out: 0, usd: 0, error: String(err) }, now);
throw err;
}
}
注意catch分支:失败的尝试通常仍会消耗输入Token,所以即使请求报错也要记账,否则账面支出会低于实际账单,熔断器会在错误的时间点才关闭。
模型调用本身放在一个薄适配器后面。任何兼容OpenAI的端点都能用,熔断器不会把独立项目锁定在单一供应商。baseUrl、apiKey和model从环境变量读取,绝不把密钥硬编码进会被免费主机克隆的仓库。
仅追加账本(Append-Only Ledger)
账本是一个只能写入、不能就地修改的事件序列:每次调用追加一行记录,包含时间戳、路由、输入/输出 Token 数、估算或实际美元成本和原因。要修正错误,不能改写旧行,而是追加一条冲销记录。这种结构带来三个好处:审计时能完整重放当天发生了什么;崩溃后不会出现"部分写入"的中间状态(最坏情况是丢掉最后一行);并可以在读侧按天聚合出 spendToday(now)。
代价是文件会持续增长。对单进程独立应用,按天分片(如 spend-2026-10-16.jsonl)即可,既方便归档也方便删除。由于账本以纯文本 JSONL 存储,它同时也是一个便宜的调试工具:当闸门意外关闭时,直接搜索当天的 route: "error" 记录,往往比翻日志更快定位失控的重试来源。
用测试证明闸门会关
熔断器如果只是被读懂而从没被执行过,就毫无价值。下面是一个完全离线的测试,用桩模型和显式时钟:
test("the gate closes before the budget is spent", async () => {
const now = new Date("2026-10-16T09:00:00Z");
const routes = [];
for (let i = 0; i < 40; i++) {
const out = await guardedSummarize(note, { callModel, now });
routes.push(out.route);
}
const total = spendToday(now);
assert.ok(total.in + total.out < 1000, "stayed under the hard token budget");
assert.equal(routes.at(-1), "fallback", "the gate closes and stays closed");
});
第一个测试循环四十次,仍然在几毫秒内跑完。这正是离线桩的意义。你担心的那个故障曾经花掉某个人一个周末,在这里只需一次测试运行。
时间可注入(Injectable Clock)与确定性测试
依赖真实 Date.now() 的代码很难测试跨天行为:你无法在不等待二十四个小时的情况下验证"新的一天预算重置"。把 now 作为参数传入、在测试中显式构造时间点,是把时间变成普通输入的标准技巧。生产代码用 new Date() 作为默认值,测试则传入固定时刻,于是同一段逻辑既能在线上对真实时间生效,又能在毫秒内遍历任意时间序列。
桩模型(stub model)同理:用返回固定 usage 的假函数替换 callModel,测试就不需要网络、不需要 API Key、也不会产生任何费用。这使"闸门关闭"这类预算相关的断言可以进入常规 CI——因为一次失败的预算测试如果不小心真的调用了远端模型,本身就会造成损失。文章强调"几毫秒内跑完",落点正是这种可重复、零成本、零外溢的验证方式。
免费服务器适合放在哪
把Worker和账本跑在同一个进程、同一台小主机上。免费服务器选项恰好适合这种形态:一个Node进程、一个SQLite或JSONL文件、一个外部模型端点。
账本路径放在应用目录内,而不是/tmp。重启不应该悄悄退还当天的支出。同时把进程限制为单实例,因为两个Worker写同一个文件可能导致重复计数或丢行。免费主机有自己的限制,在向客户承诺之前先读清楚当前条款,并决定冷启动或被限流时降级路径如何表现。
第一个月的路由矩阵
| 场景 | 路由 | 原因 |
|---|---|---|
| 用户点击,正在等待 | 带闸门的模型调用 | 延迟可接受,每次操作一次调用 |
| 后台任务遍历多行 | 按每次运行上限批处理 | 失控循环始于Worker而非UI |
| 闸门已关闭 | 确定性降级 | 交付更差的答案,而不是故障 |
| 服务商返回429或5xx | 重试两次后降级 | 无界重试会让Token开销成倍增长 |
| 记账必须精确 | 都不用 | 用计费系统,而不是JSONL文件 |
局限与谁该跳过
账本是进程内、基于文件的。适合单个独立应用,不适合跨区域运行。九成阈值只是一个猜测,应针对真实流量调整。Token估算与供应商计数存在偏差,端点返回用量报告时应以报告为准结算。
更重要的是,熔断器保护的是额度,不是正确性。糟糕的提示词在预算内依然返回糟糕的摘要。免费模型访问和任何免费套餐一样是移动靶,在把业务规则建立其上之前先核对当前条款。
如果你已经有供应商侧配额和计费仪表盘,可以跳过这套方案。对高吞吐服务,应在每次调用前放置网关,也跳过。如果你无法反复阅读供应商的限制,同样跳过。一个独立项目加一个AI按钮是最佳点位,因为一个JSONL文件和四十行Node代码换来一条真正重要的保证:周末保持无聊。
高吞吐场景下的网关(Gateway)方案
当调用量上升到多实例、多区域时,进程内的 JSONL 文件会同时遭遇两个问题:并发写入的竞态(两个进程读-改-写同一计数器,导致少计或重复计),以及无法跨地域共享状态(新加坡的 Worker 看不到法兰克福的支出)。这时通常引入一个独立的网关服务,所有模型请求先经过它,由网关在调用前集中检查配额、在调用后集中写入共享存储(Redis、数据库或供应商侧计量)。
网关的代价是多一跳延迟和额外的运维面。对只有一个 AI 按钮的独立项目,这笔开销远超收益;但一旦每天调用量进入百万级,进程内熔断器就会退化成"每个实例各自为政"的局部限制,无法兑现全局预算承诺。判断标准不是技术先进与否,而是你是否需要跨进程的单一事实来源。
相关推荐

AI数字分身实战:用DOT自动追踪Bug与用户反馈
DOTS 团队工程师演示如何用 AI 数字分身 DOT 跨平台收集用户反馈、自动归类问题主题、开线程调查 Bug 根因并直接提交 PR,展示 AI 代理参与缺陷分诊的完整工作流与可审计设计。

晶体管为何不再扁平?从平面到环绕栅极的演进
晶体管从平面到FinFET再到环绕栅极的演进:为何栅极需要从三面包裹到四面环绕,以抑制短沟道效应和泄漏电流,延续摩尔定律。

蜂群思维=单个RL智能体?多臂老虎机等价性证明
一篇论文证明蜂群通过局部模仿进行的集体决策,在数学上等价于单智能体强化学习。N个盲目模仿者相当于N个并行环境采样,有效学习率为1/N。本文解读Maynard-Cross Learning算法的推导与影响。