你跑一个 CrewAI 流水线去重构 40 个文件的代码库,结果停在第 22 个文件,日志里是 BudgetExhaustedError: token limit 500000 reached;又或者在 Claude Code 里弹出 You've reached your usage limit 横幅,Agent 直接停手,一半测试还在飘红。已经做完的那部分工作可能是对的、可能是错的,也可能让代码处于比开工前更糟的中间态。预算在任务中途耗尽不只是花钱的问题,它同时是个正确性和恢复性的问题。
最快路径:先抢救已做的工作——用 git diff 看 Agent 在磁盘上改了什么,再用 git stash 把这些半成品文件暂存起来,这样重跑时不必为已完成的步骤重新付费。然后照下面的诊断表定位预算到底花到哪去了(多半是这几种之一:预算估低、工具输出没裁剪、重试风暴、子 Agent 没计费)。只修那一个根因,加上 SQLite 或 Temporal 的 checkpoint 让下次能续跑,再从断点重新 invoke,而不是从第 1 步开始。
你属于哪一类
改任何代码之前,先把你的现象对到最可能的根因上。
| Trace 里的现象 | 最可能的根因 | 跳到 |
|---|---|---|
| 总消耗大约是单文件预估的 3-5 倍,且各步骤分布均匀 | 规划时低估了任务规模 | 原因 1 |
| 消息历史的体积增长快于步骤数 | 工具输出没裁剪、一直堆积 | 原因 2 |
| LLM API 调用次数远多于完成的逻辑步骤数 | 静默的重试循环 | 原因 3 |
| 父级预算计数偏低,但账单很高 | 子 Agent 用量没计入 | 原因 4 |
| 抽取、格式化这类琐碎步骤按前沿模型计费 | 简单子任务用了过强的模型 | 原因 5 |
| 重启总是回到第 1 步 | 没有 checkpoint | 原因 6 |
常见原因
1. 规划时低估了任务规模
最常见的根因。你按理想路径估预算:不报错、不重试、每个文件只读一次。但实际上 Agent 会反复读文件来验证改动、会重试失败的工具调用、还会把中间推理写进上下文,实际消耗常常是裸估算的 3-5 倍。
怎么判断:拿一个最简单的「读取并总结一个文件」的实际 token 数,跟你给每个文件分配的预算对比。如果比值不到 3 倍,说明你的预算是按完美一次跑通算的。
2. 工具输出太长、没裁剪就灌进上下文
read_file 把一个 1,200 行的文件原样返回。Agent 在随后 15 次工具调用里一直把全文留在上下文里,而不是只存相关片段。到第 10 个文件时,上下文里 80% 都是过时的文件内容,而且每次调用都要按 input token 重新为这些内容付一遍钱。
怎么判断:每 5 步打印一次 len(messages) 和消息历史的总字符数。如果体积增长快于 O(steps)(即呈二次增长),说明工具输出在失控堆积。
3. 重试循环在悄悄吃预算
一个不稳定的外部 API 导致每次调用重试 5 次,每次重试都带着完整对话上下文。20 次 API 调用、每次重试 5 遍,就是 100 次 LLM 调用而不是 20 次,于是预算在只完成五分之一进度时就见底了。
怎么判断:统计 Trace 里实际的 LLM API 调用次数,跟完成的逻辑步骤数对比。比值高于 2:1 就说明在重试上浪费了。
4. 子 Agent 没计入父级预算
在 AutoGen、或带嵌套 Agent 的 LangGraph 这类框架里,父 Agent 的预算追踪器只统计自己的调用。子 Agent 的 token 用量是另算的、甚至根本没算,导致父级的预算表读数严重偏低。
怎么判断:把 Trace 里所有 agent ID 的 token 用量加总。如果总数明显高于父级预算计数器显示的数字,说明子 Agent 的开销没入账。参见 成本统计漏算了子 Agent 用量。
5. 大多数子任务用了过强的模型
整条流水线每一步都用 Claude Opus 4.7 或 GPT-5.5,连「从这段代码里抽出函数名」这种琐事也是。截至 2026 年 6 月,Opus 4.7 的计费是输入 $5/MTok、输出 $25/MTok,而 Sonnet 4.6 是 $3 / $15。这些看着不起眼的子任务,一旦每个都按前沿模型计费,加起来很快。
怎么判断:列出每一步用的模型。把所有不需要推理的步骤(读文件、格式转换、截取子串)标出来,看有没有在用顶级模型。
6. 没有 checkpoint,耗尽时中间结果全丢
Agent 一路跑到耗尽,而编排层没有 checkpoint。第 1-22 个文件是在内存里改的,崩溃时这些改动全被丢弃。用户从零重启,再把预算烧一遍。
怎么判断:检查你的编排层是否写 checkpoint(如 LangGraph 的 SqliteSaver,或 Temporal 的持久化执行)。如果重启总是回到第 1 步,就是没有 checkpoint。
最短修复路径
Step 1:审计每步的实际消耗 vs 预算
注意:老的 get_openai_callback 在 langchain-community 包里,而该包已于 2026 年 5 月 26 日归档(只读),它仅支持 OpenAI,且在流式输出时会悄悄报 0 token。改用 langchain-core 里与厂商无关的 UsageMetadataCallbackHandler:
from langchain_core.callbacks import UsageMetadataCallbackHandler
cb = UsageMetadataCallbackHandler()
result = chain.invoke(input, config={"callbacks": [cb]})
# cb.usage_metadata 以模型名为 key
for model, usage in cb.usage_metadata.items():
print(model, usage["input_tokens"], usage["output_tokens"], usage["total_tokens"])
用这套埋点跑一个有代表性的子任务,乘以子任务总数得到现实的预算估计,再乘 3 倍作为安全冗余。
Step 2:设分级预算——硬停 + 软预警
SOFT_BUDGET_TOKENS = 400_000
HARD_BUDGET_TOKENS = 500_000
def check_budget(used: int, step: str):
if used >= HARD_BUDGET_TOKENS:
raise BudgetExhaustedError(f"Hard limit reached at step: {step}")
if used >= SOFT_BUDGET_TOKENS:
logger.warning("Soft budget hit at step %s, consider checkpointing", step)
软预警给你留出在硬停触发前做 checkpoint 的时间。
Step 3:工具输出在进上下文前先裁剪
MAX_TOOL_OUTPUT_CHARS = 4_000
def trim_tool_output(output: str, max_chars: int = MAX_TOOL_OUTPUT_CHARS) -> str:
if len(output) <= max_chars:
return output
half = max_chars // 2
return output[:half] + "\n... [trimmed] ...\n" + output[-half:]
读文件时,用搜索 / grep 的方式取代整文件读取:
# 不要读 1200 行,只抽出相关函数
grep -n "def authenticate" src/auth.py | head -5
sed -n '47,82p' src/auth.py
Step 4:把廉价子任务路由到更便宜的模型
Claude Code 只跑 Anthropic 的模型,所以在这个生态里的杠杆是 Opus 4.7(推理)对 Sonnet 4.6(其余一切)。在通用流水线里,抽取级的活儿也可以退回到小而快的档位,比如 GPT-5.5 Instant 或 Gemini 3.1 Pro。
def pick_model(task_type: str) -> str:
cheap_tasks = {"extract", "format", "summarize_short", "classify"}
if task_type in cheap_tasks:
return "claude-sonnet-4-6" # 主力模型,$3/$15 每 MTok
return "claude-opus-4-7" # 仅推理任务,$5/$25 每 MTok
只在真正需要推理的步骤上调用前沿模型,对任务复杂度参差的流水线通常能省下 60-80% 的开销。
Step 5:启用 checkpoint,让中间结果挺过耗尽
在当前版本的 LangGraph(langgraph-checkpoint-sqlite 包,2026 年 5 月 12 日刷新)里,SqliteSaver.from_conn_string 是个上下文管理器,必须放进 with 块里用:
from langgraph.checkpoint.sqlite import SqliteSaver
with SqliteSaver.from_conn_string("checkpoints.sqlite") as checkpointer:
graph = workflow.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "run-42"}}
result = graph.invoke(input, config=config)
Temporal 原生支持这点:每个 await workflow.execute_activity(...) 都是一个持久化的 checkpoint,所以预算中止永远不会丢掉已完成的 activity。
Step 6:从断点续跑,而不是重启
预算耗尽后,用 graph.get_state(config) 查看最后保存的状态,它返回一个 StateSnapshot:.values 是各通道的状态,.next 列出当时即将运行的节点,.metadata 记录最后一次写入。调高预算后,用相同的 thread_id 重新 invoke:
from langgraph.checkpoint.sqlite import SqliteSaver
with SqliteSaver.from_conn_string("checkpoints.sqlite") as checkpointer:
graph = workflow.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "run-42"}}
snap = graph.get_state(config)
print("Saved channels:", list(snap.values.keys()))
print("Next nodes to run:", snap.next)
# 续跑:传入 None 会从挂起的节点重新进入,而不是第 1 步
result = graph.invoke(None, config=config)
至于 Claude Code,没有运行中追加预算这回事。一旦出现 You've reached your usage limit 横幅,会话就转为只读,直到时间窗重置。截至 2026 年 6 月,Claude Code 同时执行两道叠加的额度:一道 5 小时滚动窗口(2026 年 5 月 6 日对 Pro、Max、Team 和按席位的 Enterprise 翻倍),一道针对活跃算力的每周上限。先用 git diff / git stash 抢救中间结果,再等重置、或升到更高计划继续(Max 5x 为 $100,Max 20x 为 $200)。
怎么确认已修好
- 带埋点重跑同一个任务。
UsageMetadataCallbackHandler的总数应落在你的 3 倍冗余估算之内,而不是冲破上限。 - 每 5 步打印消息历史体积,它应近似线性增长而非二次增长(确认工具输出裁剪生效)。
- 中途强制让它失败(杀进程),再用相同的
thread_id重新 invoke。任务必须从挂起的节点继续,而不是从第 1 步。 - 查每步的模型日志。
extract/format/classify这类步骤不应再命中前沿模型。
预防建议
- 在设任何预算前,先基准测一个有代表性的子任务,用实测成本的 3 倍作为最小分配额。
- 所有工具输出在进入 LLM 上下文前裁到最大长度;完整输出按引用存到外部。
- 第一天就启用 checkpoint,崩溃之后再补这套机制非常痛苦。
- 把分类、抽取、格式化这类子任务路由到更便宜的模型档位。
- 在硬限额的 80% 处设软阈值,并打印当前进度百分比的告警。
- 用单一聚合计数器统计子 Agent 的 token 用量,绝不只信父级的本地计数。
- 给每个工具调用的重试设上限。CrewAI 的
max_iter默认是 25,是主要的成本驱动项,建议每个 Agent 设到 5-8,并配合max_rpm;AutoGen 里则约束max_round/max_consecutive_auto_reply。重试到顶就转人工审核队列,而不是无限重试。 - 对提示里稳定的部分(system prompt、工具定义、共享的大块上下文)启用 Anthropic prompt caching。最多标 4 个断点,用
cache_control: {"type": "ephemeral"}(默认 5 分钟 TTL,可选"ttl": "1h");缓存命中的读取远比重发前缀便宜,不过首次写入约比普通 input token 贵 25%。 - 在需要之前就把「从第 N 步续跑」的路径建进流水线,事后再补要难得多。
常见问答 (FAQ)
Q: 怎么给一个新流水线估算合理预算? A: 拿 3-5 个有代表性的任务带全套埋点各跑一遍,取第 90 百分位的 token 数,再乘 2 倍。绝不要按中位数估算。Agent 的 token 用量有很重的右尾,而且对话式步骤随历史累积大致呈 O(n²) 增长,不是 O(n)。
Q: 能在运行途中暂停 Agent 来追加预算吗?
A: 在带 checkpointer 的 LangGraph 里可以。状态被持久化保存,你用相同的 thread_id、更高的限额重新 invoke,并把输入传成 None 即可从挂起节点续跑。无状态框架里得自己围绕 checkpoint 实现暂停 / 续跑。Claude Code 本身不支持运行中追加预算,只能等窗口重置或升级到更高计划。
Q: prompt caching 对预算耗尽有帮助吗? A: 有,而且很显著,但它解决的问题和 checkpoint 不同。缓存能把每次调用重发同一前缀(system prompt、工具定义、共享上下文)的成本降低约 70-90%(真实工作流里)。但它不能恢复执行状态,所以一个运行如果在第 30 步没有 checkpoint 就挂了,你还是得从第 1 步重来,只是每步更便宜。两者都要开。
Q: 预算耗尽时,半改的文件会怎样?
A: 取决于框架。如果 Agent 直接在磁盘上改文件(Claude Code、Cursor),磁盘上的改动是真实的,可能让代码停在一个被改坏的中间态。用 git diff 看改了什么,再用 git stash 或 git checkout -- . 回退后再重试。只在内存里改的编排器,则会丢掉所有尚未 checkpoint 的内容。
Q: 为什么我的 LangChain 成本回调报 0 token?
A: 老的 get_openai_callback 只支持 OpenAI,且响应一旦流式输出就返回 0;它所在的包 langchain-community 已于 2026 年 5 月 26 日归档。改用 langchain-core 的 UsageMetadataCallbackHandler,它跨厂商、也支持流式。