Codex、Claude Code 跑一夜,API 账单为什么超预算?先查这 5 类请求

📅 2026/7/23 5:29:24
Codex、Claude Code 跑一夜,API 账单为什么超预算?先查这 5 类请求
凌晨让 Codex 或 Claude Code 修一个问题早上看起来只完成了一项任务控制台里却多出了十几次甚至更多请求。模型价格没变用户也只点了一次费用为什么还会超出预估这类问题不能只看最后一条回答。Agent 会读文件、调用工具、运行测试再把工具结果送回模型。一次任务通常对应多轮 API 请求其中任何一轮都可能重试上下文也可能越带越长。所以先别急着换模型。第一步是把“一次点击”还原成实际请求弄清楚每笔 Token 花在了哪里。先别看总 Token先给“一个任务”划清边界普通聊天比较好算发一个问题收一个回答。Agent 不是这样。一次“修复登录失败并运行测试”的任务内部可能经历读取文件、分析、工具执行、测试和失败后的再次修改。用户侧只有一条任务接口侧已经走了多轮。排查时需要给业务任务生成task_id每次模型请求再生成独立的attempt_id。至少记录这些字段task_id attempt_id model request_id status_code input_tokens cached_tokens output_tokens reasoning_tokens tool_calls retry_index duration_ms final_statusrequest_id用于向服务商定位单次请求task_id才能把多轮调用重新归到同一个业务任务。两者不能互相替代。下面是一段演示数据不是任何平台的生产结果task_id attempt status input cached output retry tool t-1042 1 200 8200 0 640 0 read_file t-1042 2 200 19100 4096 510 0 run_tests t-1042 3 502 42500 8192 0 0 - t-1042 4 200 42720 8192 780 1 run_tests t-1042 5 200 81100 8192 930 0 write_file这五行已经暴露了三个问题第三次失败后发生了重试输入 Token 从 8200 涨到 81100缓存虽然已经命中但命中量停留在 8192缓存占输入的比例从约 19% 降到约 10%。没有请求级数据只看任务最终成功很难发现这些变化。SDK 重试和业务重试叠在了一起本文按openai-python v2.46.0核对SDK 默认会对连接错误、408、409、429 和 5xx 自动重试 2 次。这里的“重试 2 次”意味着一次 SDK 调用最多可能产生 3 次网络请求。后续版本如有变化应以官方 README 和当前安装版本为准。如果业务代码外面又包了一层“失败后最多尝试 3 次”最坏情况不是 3 次而是业务层 3 次尝试 × SDK 每次最多 3 个请求 9 个网络请求下面这种写法看起来很普通实际已经有双重重试风险fromopenaiimportOpenAI clientOpenAI()# SDK 默认 max_retries2forattemptinrange(3):try:responseclient.responses.create(modelYOUR_MODEL_ID,input分析测试失败原因,)breakexceptException:ifattempt2:raise更容易审计的方式是只保留一层重试。比如由业务层统一处理就明确关闭 SDK 重试importrandomimporttimefromemail.utilsimportparsedate_to_datetimefromdatetimeimportdatetime,timezoneimportopenaifromopenaiimportOpenAI clientOpenAI(max_retries0)defretry_delay(exc,attempt):retry_afterexc.response.headers.get(retry-after)ifexc.responseelseNoneifretry_after:ifretry_after.isdigit():returnmax(0,int(retry_after))try:targetparsedate_to_datetime(retry_after)returnmax(0,(target-datetime.now(timezone.utc)).total_seconds())except(TypeError,ValueError,OverflowError):passreturn(2**attempt)random.random()forattemptinrange(3):try:responseclient.responses.create(modelYOUR_MODEL_ID,input分析测试失败原因,)breakexcept(openai.APIConnectionError,openai.APITimeoutError):ifattempt2:raisetime.sleep((2**attempt)random.random())exceptopenai.APIStatusErrorasexc:retryableexc.status_codein{408,409,429}orexc.status_code500ifnotretryableorattempt2:raisetime.sleep(retry_delay(exc,attempt))401、确定的 400 参数错误、模型不存在等问题通常不会因为多试几次而恢复。反复请求只会制造更多日志某些情况下还会放大费用。服务端如果返回Retry-After客户端还应优先尊重它。RFC 9110 规定该字段可以是一个 HTTP 日期也可以是需要等待的秒数。还有一个容易漏算的情况客户端超时不代表服务端一定停止处理。上游如果已经接收并完成请求这次调用仍可能进入用量统计客户端随后重试又会产生下一次请求。是否计费必须结合服务端request_id和账单记录确认不能把所有超时都当作“没有发生”。每轮都把越来越长的上下文重新带上第一轮也许只读了一个配置文件。第二轮加入搜索结果第三轮加入测试日志第四轮又加入 Diff 和报错堆栈。后续每轮的输入规模可能不断变大。不要用“原始提示词只有 500 字”估算成本要看每一轮响应里的usage。下面用 AI快站的 OpenAI Compatible 地址把示例写完整使用其他接口时替换 Base URL、环境变量和真实模型 ID 即可。importjsonimportosimporttimeimportuuidimportopenaifromopenaiimportOpenAI clientOpenAI(api_keyos.environ[AIFAST_API_KEY],base_urlhttps://www.aifast.club/v1,max_retries0,)task_idstr(uuid.uuid4())attempt_idstr(uuid.uuid4())startedtime.perf_counter()record{task_id:task_id,attempt_id:attempt_id,request_id:None,model:YOUR_REAL_MODEL_ID,status_code:None,input_tokens:None,cached_tokens:None,cache_write_tokens:None,output_tokens:None,reasoning_tokens:None,total_tokens:None,tool_calls:0,retry_index:0,# SDK 重试已关闭由业务层填写实际序号duration_ms:None,final_status:started,}errorNonetry:responseclient.responses.create(modelrecord[model],input只回复 COST_LOG_OK,)usageresponse.usage input_detailsgetattr(usage,input_tokens_details,None)output_detailsgetattr(usage,output_tokens_details,None)record.update({request_id:response._request_id,model:response.model,status_code:200,input_tokens:getattr(usage,input_tokens,None),cached_tokens:getattr(input_details,cached_tokens,None),cache_write_tokens:getattr(input_details,cache_write_tokens,None),output_tokens:getattr(usage,output_tokens,None),reasoning_tokens:getattr(output_details,reasoning_tokens,None),total_tokens:getattr(usage,total_tokens,None),final_status:completed,})exceptopenai.APIStatusErrorasexc:errorexc record.update({request_id:exc.request_id,status_code:exc.status_code,final_status:api_error,})except(openai.APIConnectionError,openai.APITimeoutError)asexc:errorexc record[final_status]type(exc).__name__finally:record[duration_ms]round((time.perf_counter()-started)*1000)print(json.dumps(record,ensure_asciiFalse))iferror:raiseerror这段代码有两个边界需要说明某些 OpenAI Compatible 服务不会返回所有明细字段生产代码应做空值兼容响应里的usage适合排查请求结构正式费用仍应与服务商控制台账单核对。失败请求也要写入同一套日志。上面的代码会记录APIStatusError的状态码和request_id连接错误与超时则通过final_status区分。无论哪种情况都不要把完整 API Key、用户提示词或公司源码写进日志。拿到日志后把同一task_id的input_tokens按请求顺序画出来。如果是8K → 19K → 42K → 81K这种持续上涨问题就不在模型单价而在上下文管理。处理时先砍掉最明显的无效输入只读取当前任务真正相关的文件不把整个仓库一次性塞进去工具输出先过滤保留错误摘要、关键行和必要上下文大段测试日志保存到文件让 Agent 按需搜索不要每轮原样回传长任务拆成有明确产物的阶段阶段结束后再压缩上下文设置单任务输入 Token 上限超过后停止并要求人工确认。固定前缀不断变化缓存收益被稀释系统提示、编码规范、工具描述和项目规则通常会在多次请求中重复出现。这些内容适合保持稳定便于服务端进行提示缓存。但很多程序会把时间戳、随机 ID、用户临时信息放在提示词最前面当前时间2026-07-22 18:03:41 请求编号b1f8... 固定系统规则... 工具定义... 用户问题...每次请求开头都不同后面的固定内容就更难复用。更合理的顺序是固定系统规则... 稳定的工具定义... 项目约束... 当前时间2026-07-22 18:03:41 请求编号b1f8... 用户问题...是否真正命中缓存不能凭感觉判断。检查cached_tokens、缓存写入字段和cached_tokens / input_tokens的变化再与控制台账单核对。已经出现缓存 Token 也不等于缓存效果理想输入持续增长而缓存量不变时命中比例仍会下降。还要先确认当前模型和服务确实支持对应缓存机制。缓存价格会变化本文不写固定折扣。工具调用失败后Agent 在同一个坑里打转有一类日志很典型模型连续调用同一个工具只改了文件名或相对路径底层错误却一直没变。例如一个文件写入工具因为目录没有权限而失败。模型没有拿到清晰的错误类型只看到“执行失败”于是继续换文件名、换相对路径、再试一次。每一轮都会产生新的输入和输出 Token。工具层至少要返回结构化错误{ok:false,error_code:PERMISSION_DENIED,retryable:false,message:目标目录不可写,suggested_action:请求用户选择可写目录}同时给 Agent 设置硬限制同一工具连续失败 2 次后停止自动尝试retryablefalse时不得修改参数继续碰运气发邮件、创建订单、写数据库等有副作用的操作必须使用幂等键单任务工具调用次数达到阈值后输出当前证据并交给人工判断。这类限制有时会让 Agent 少一点“自动完成”的感觉却能避免一次配置错误变成长时间循环。只比较每百万 Token 单价没有比较完整任务成本“一次调用多少钱”只能比较请求。“完成一个通过验收的任务多少钱”才适合比较 Agent 方案。可以先用这个基础公式单次请求成本 输入 Token ÷ 1,000,000 × 输入单价 输出 Token ÷ 1,000,000 × 输出单价 单任务模型成本 该任务全部请求成本之和 单个成功任务成本 全部任务模型成本 ÷ 最终通过验收的任务数举个只用于演示算法的例子。假设某批任务计划执行 100 次平均每次输入 20,000 Token、输出 2,000 Token输入和输出价格分别是每百万 Token 2 和 8 个计费单位单次成本 20,000 / 1,000,000 × 2 2,000 / 1,000,000 × 8 0.056 100 次基础成本 5.6如果日志显示额外重试比例为 20%仅按相同 Token 规模粗算总成本会变成5.6 × (1 20%) 6.72但这仍不是完整业务成本。图像、视频、检索、缓存写入、外部工具、失败后的人工返工都可能单独计费或产生时间成本。不想手算时可以把控制台的当前价格和真实日志填进 大模型 API Token 成本计算器。这是 AI快站文档站提供的浏览器端工具会单独列出基础成本和额外重试成本不内置容易过期的模型报价。用一段脚本把最贵的任务找出来假设上面的记录按 JSON Lines 格式写入agent-usage.jsonl可以先用下面的脚本做粗排。它不会计算价格只负责找出请求次数和 Token 总量异常的任务。importjsonfromcollectionsimportdefaultdict tasksdefaultdict(lambda:{requests:0,failed:0,unknown_status:0,retries:0,input_tokens:0,output_tokens:0,})withopen(agent-usage.jsonl,encodingutf-8)asfile:forlineinfile:rowjson.loads(line)itemtasks[row[task_id]]item[requests]1status_coderow.get(status_code)item[failed]int(row.get(final_status)!completed)item[unknown_status]int(status_codeisNone)item[retries]int(row.get(retry_index,0)0)item[input_tokens]row.get(input_tokens)or0item[output_tokens]row.get(output_tokens)or0rankingsorted(tasks.items(),keylambdapair:pair[1][input_tokens]pair[1][output_tokens],reverseTrue,)fortask_id,iteminranking[:10]:print(task_id,item)排在前十的任务不一定有问题。复杂任务本来就可能消耗更多 Token。接下来要看它是否完成、是否重复读取同一批内容、是否连续调用失败工具以及重试是否由配置错误触发。一次排查可以按这个顺序走如果账单已经开始异常不需要先重构整套 Agent。先选一小批近期任务把最大的一块找出来选 10 个最近执行过的真实任务给每个任务恢复task_id统计每个任务的请求数而不是只看用户点击次数检查 SDK 重试和业务重试是否同时开启按时间画出每轮输入 Token确认上下文是否持续膨胀统计cached_tokens / input_tokens查看固定前缀是否命中按工具名称统计失败次数找出重复调用最多的工具用“总成本 ÷ 成功任务数”比较模型和路由方案给请求次数、Token 和工具循环设置预算上限再做一次小流量复测。没有请求级日志时先补日志。只看控制台的一条总金额很难分清问题来自单价、重试、上下文还是工具循环。模型先不换把账算明白Agent 成本超预算往往是几件小事叠在一起SDK 重试业务层又重试上下文每轮变长缓存没命中工具失败后继续循环。模型单价当然重要但它只是公式中的一个变量。把一次业务任务拆回请求日志能看到的东西比价格表多得多任务为什么变贵、哪里能停、哪种模型真的更省以及成本下降后完成率有没有一起下降。本文日志中的 Token 和价格数字只用于演示算法不是实测结果或实际报价。代码示例使用的接口地址为https://www.aifast.club/v1。复现前请通过/v1/models获取账号当前可用的模型 ID开放范围、价格和计费规则以实时控制台为准。参考资料OpenAI Python SDKRetries 与 Request IDsOpenAI Responses API 参考OpenAI Prompt Caching 指南RFC 9110Retry-After