
1. 事故现场一个协程跑掉 3000 万 Token 的账单爆表复盘先说结论LLM 应用里最贵的 bug不是崩溃而是「不崩溃但一直跑」。崩溃了进程会退出账单会停而一个陷入递归推理的 Agent 协程会安安静静地、稳定地、每秒钟都在烧钱直到你早上打开账单页面。我这次遇到的场景很典型后台有一个异步总结文章的 Agent输入是一篇 PDF 解析后的文本输出是结构化摘要。正常情况下一篇文档消耗 2 万到 5 万 Token 就结束了。但那天晚上一篇格式错乱的 PDF 让模型进入了「反复自我纠正」的循环——它不断认为自己的摘要不够好于是重新读取上下文、重新推理、重新生成循环了整整 8 个小时。第二天早上看到的数据是单个 Session 消耗 3000 万 Token账单突增近 1500 美金。更麻烦的是这个协程不是孤立的它跑在一个并发池里同时还有几十个协程在跑别的任务。如果没有及时定位损失会继续放大。这里要区分两个概念确定性消耗和非确定性消耗。传统后端服务里一次请求消耗多少 CPU、多少内存基本是可预估的你可以按 QPS 做容量规划。但 LLM 调用不是——同样的 prompt模型可能返回 200 Token也可能返回 8000 TokenAgent 可能 3 步结束也可能 300 步还在循环。这种非确定性意味着你不能只靠「平均消耗」做预算必须给每个协程装上硬性的预算闸门。这篇文章我会从三个角度拆解调用链埋点先看清楚钱花在哪、协程级配额给每个协程一个独立的钱包、超限熔断钱包空了立刻切断调用流。最后给出一份可复制的预算闸门配置片段以及一次压测复现步骤验证闸门触发后协程被正确拦截、账单不再失控。适合谁看正在做多协程 Agent 并发调用 LLM 的后端工程师、负责大模型应用成本控制的同学、以及被账单吓过一次想提前建防线的人。核心检索词就三个Agent 协程预算闸门、LLM Token 熔断、非确定性消耗治理。2. TaoToken 前置把调用入口统一埋点才有地方落事故复盘时我最大的痛点是调用入口太散。有的协程直接用 OpenAI SDK有的走内部封装的 HTTP 客户端有的走某个第三方库的默认配置。结果就是——我想统计「单个协程消耗了多少 Token」得去三个地方捞日志还对不上账。所以第二步不是急着写闸门而是先把 LLM 调用入口统一到一个可观测的网关。我这边用的是 TaoToken 作为统一调用入口它的好处是所有协程的请求都经过同一个 Base URLToken 用量、请求耗时、模型 ID 都能在同一个维度上对齐埋点只需要做一次。先明确三个东西后面配置会反复用到Base URLhttps://taotoken.net/api注意 API 调用不加 UTM 参数保持干净API Key在控制台生成建议按环境区分dev / staging / prod 各一把Model ID比如claude-sonnet-4-5、gpt-4o这类按你实际用的模型填控制台和 Key 管理入口在这里控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsoleAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc如果你只是想先验证模型通不通可以用模型对话页面直接发一条请求确认 Key 和 Model ID 没问题模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentchat为什么要先做这一步因为预算闸门的核心逻辑是「调用前预估 调用后扣减」。如果调用入口不统一你没法在同一个地方拿到usage.prompt_tokens和usage.completion_tokens闸门就成了摆设。统一入口之后你可以在网关层做一层轻量代理把每次请求的 Token 用量写进协程上下文闸门只需要读这个上下文就行。另外提醒一点不要把生产库直连到任何 MCP 或自动化工具上预算闸门是代码层的防线不是运维层的补丁。闸门要写在业务代码里跟着协程生命周期走。3. 可复制配置协程级 Token 预算闸门 熔断片段这一节是核心直接给可复制的配置和代码。我按「配置片段 → 闸门实现 → 调用包装」三层来写你可以直接抄。3.1 统一调用配置JSON / TOML / settings 三选一先给一份通用的 JSON 配置放在项目根目录config/llm_gateway.json{ base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, default_model: claude-sonnet-4-5, budget: { per_call_max_tokens: 8000, per_session_max_tokens: 100000, per_coroutine_max_tokens: 50000, warn_threshold_ratio: 0.8, hard_cutoff: true }, timeout: { request_seconds: 60, session_seconds: 1800 } }如果你用 TOML 风格比如某些 Python 项目等价写法[llm_gateway] base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY default_model claude-sonnet-4-5 [llm_gateway.budget] per_call_max_tokens 8000 per_session_max_tokens 100000 per_coroutine_max_tokens 50000 warn_threshold_ratio 0.8 hard_cutoff true如果你用 Claude Code 或类似工具settings.json里对应字段是{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key, ANTHROPIC_MODEL: claude-sonnet-4-5 }, budget: { max_tokens_per_session: 100000, hard_cutoff: true } }注意三件套必须齐全Base URL Key Model ID。少任何一个闸门都拿不到准确的 Token 用量。3.2 协程级预算闸门实现Go 版下面这份代码可以直接跑核心是TokenBudgetGatekeeper用原子操作保证并发安全package budget import ( context errors fmt sync/atomic ) var ErrBudgetExceeded errors.New(session token budget exceeded) type TokenBudgetGatekeeper struct { maxSessionTokens int64 usedTokens int64 warnRatio float64 warned int32 } func NewTokenBudgetGatekeeper(maxBudget int64, warnRatio float64) *TokenBudgetGatekeeper { return TokenBudgetGatekeeper{ maxSessionTokens: maxBudget, warnRatio: warnRatio, } } // Consume 尝试扣减预算超额立即返回错误调用方必须中断 func (g *TokenBudgetGatekeeper) Consume(tokens int64) error { newTotal : atomic.AddInt64(g.usedTokens, tokens) if newTotal g.maxSessionTokens { return fmt.Errorf(%w: used%d limit%d, ErrBudgetExceeded, newTotal, g.maxSessionTokens) } // 预警线达到 80% 打一次日志不阻断 if g.warnRatio 0 float64(newTotal) float64(g.maxSessionTokens)*g.warnRatio { if atomic.CompareAndSwapInt32(g.warned, 0, 1) { fmt.Printf([BUDGET WARN] used%d limit%d\n, newTotal, g.maxSessionTokens) } } return nil } func (g *TokenBudgetGatekeeper) GetUsed() int64 { return atomic.LoadInt64(g.usedTokens) }关键点Consume是先加后判这样即使并发调用也不会漏判。如果先判后加两个协程同时读到 49000都以为没超 50000结果加起来 98000闸门就失效了。3.3 带预算拦截的 LLM 调用包装func CallLLMWithBudget(ctx context.Context, g *TokenBudgetGatekeeper, prompt string, estimatedPromptTokens int64) (string, error) { // 1. 调用前预估 prompt token先扣一次 if err : g.Consume(estimatedPromptTokens); err ! nil { return , err } // 2. 实际调用这里替换成你的 TaoToken 客户端 resp, err : callTaoToken(ctx, prompt) if err ! nil { return , err } // 3. 调用后按实际 output token 扣减 if err : g.Consume(resp.Usage.CompletionTokens); err ! nil { return , err } return resp.Content, nil }每个协程启动时创建一个独立的TokenBudgetGatekeeper生命周期跟着协程走。协程结束闸门销毁预算归零。这样即使某个协程失控它最多也只能烧掉自己那份配额不会拖垮整个进程。4. 验证请求压测复现闸门触发与协程拦截配置写完了必须验证。我设计了一个压测脚本模拟「模型陷入递归推理」的场景看闸门是否在预算耗尽时正确切断。4.1 压测脚本Gofunc main() { // 单协程预算 50000 Token预警线 80% gatekeeper : NewTokenBudgetGatekeeper(50000, 0.8) for i : 1; i 100; i { // 模拟每轮消耗 6000 Tokenprompt 2000 output 4000 if err : gatekeeper.Consume(2000); err ! nil { fmt.Printf([CUTOFF] round%d prompt阶段被拦截: %v\n, i, err) break } if err : gatekeeper.Consume(4000); err ! nil { fmt.Printf([CUTOFF] round%d output阶段被拦截: %v\n, i, err) break } fmt.Printf(round%d used%d\n, i, gatekeeper.GetUsed()) } }4.2 预期输出round1 used6000 round2 used12000 ... round6 used36000 [BUDGET WARN] used42000 limit50000 round7 used42000 round8 used48000 [CUTOFF] round9 prompt阶段被拦截: session token budget exceeded: used50000 limit50000看到[CUTOFF]就说明闸门生效了。第 9 轮在 prompt 阶段就被拦下协程没有继续调用 LLM账单停止增长。4.3 真实请求验证压测通过后用真实请求再验一次。发一条正常请求确认返回结构里有usage字段curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: 用一句话解释什么是预算闸门}], max_tokens: 200 }返回里重点看usage.prompt_tokens和usage.completion_tokens这两个值就是闸门扣减的依据。如果这两个字段缺失说明你的调用入口没走对需要回到第 2 节检查 Base URL 和 Model ID。验证通过后把闸门接到真实的 Agent 协程里跑一轮完整任务观察日志里used是否随轮次递增、是否在预警线打日志、是否在超限时中断。三步都符合闸门就算上线了。5. 常见报错排查401、local proxy failed、reading choices、OAuth上线过程中我踩了几个坑这里按真实报错对照排查。401 Unauthorized最常见。原因通常是 Key 没读到环境变量或者 Key 和 Base URL 不匹配。检查TAOTOKEN_API_KEY是否 export 成功ANTHROPIC_BASE_URL是否写成https://taotoken.net/api不要带/v1后缀除非文档明确要求。如果用的是 Claude Code检查settings.json里三件套是否齐全。local proxy failed / connection refused这个报错通常出现在你本地起了代理但没启动或者代理端口写错。排查顺序先确认没有配置任何本地代理环境变量HTTP_PROXY、HTTPS_PROXY再确认 Base URL 是直连的https://taotoken.net/api。如果代码里硬编码了127.0.0.1:xxxx删掉。reading choices 报错 / index out of range这个不是网络问题是返回结构解析问题。某些模型在触发内容过滤或达到 max_tokens 时choices数组可能为空。你的代码如果直接取resp.Choices[0]就会 panic。修复方式解析前先判空空则视为一次失败调用但仍然要扣减已消耗的 Token因为请求已经发出去了。OAuth 相关报错如果你用 Claude Code 或 Codex 这类工具可能会遇到 OAuth token 过期。注意区分OAuth 是工具层的登录态和 API Key 是两套东西。用 TaoToken 的 API Key 接入时不需要走 OAuth 流程直接在配置里填 Key 即可。如果工具强制要求 OAuth检查是否选错了接入模式。Codex auth.json 配置如果你用 Codex~/.codex/auth.json里需要填{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-5 }三件套缺一不可。改完重启 Codex 进程否则配置不生效。CC Switch / Cline MCP 场景如果你用 CC Switch 切换配置或者用 Cline 的 MCP 模式同样要保证 Base URL Key Model ID 三件套完整。MCP 模式下特别注意不要让 MCP 直连生产库闸门要写在 MCP 调用的上游而不是 MCP 内部。排查完这些如果还有问题去接入文档对照一遍参数接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc6. 长期编码与 Agent 场景把闸门做成默认配置单次事故修完了但真正要解决的是「下次不再发生」。我的做法是把预算闸门做成 Agent 框架的默认配置而不是每个项目单独写一遍。具体来说在 Agent 启动层统一注入TokenBudgetGatekeeper每个协程创建时自动分配配额。配额来源可以是三级单次调用上限比如 8000 Token、单协程上限比如 50000 Token、单 Session 上限比如 100000 Token。任何一级触发都立即中断当前协程并记录日志。如果你在跑长期的编码 Agent 或自动化任务建议用 Coding Plan 这类按周期计费的方式把成本从「按 Token 波动」变成「按周期固定」再叠加闸门做兜底Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan最后给一个我实测有效的技巧在闸门里加一个「异常轮次检测」。如果同一个协程连续 5 轮消耗都超过 5000 Token且输出内容相似度高于 0.9直接判定为递归循环主动熔断不用等预算耗尽。这样能把 3000 万 Token 的事故压缩到 3 万 Token 以内就拦下来。预算闸门不是限制 Agent 能力而是给非确定性加一个确定性的边界。边界之内随便跑边界之外立刻停。这才是 LLM 应用能长期稳定跑下去的前提。