Agent 预算在任务中途被吃光

Agent 执行长任务时 token 或费用预算耗尽,任务强制中止且无中间结果可用。本文分析预算失控的根因并给出限额与断点续传方案。

你给 Claude Code 设置了 200 美元月度预算,某个重构任务跑了三小时后突然停止,日志显示「Usage limit reached」——但任务才完成 60%,生成的代码处于半成品状态。或者在 LangGraph 里,一个爬取 + 分析流水线在第 47 个子任务时触发了 OpenAI 的 RateLimitError 兜底逻辑,整个 workflow 回滚,之前 46 步的结果全部丢失。预算耗尽有两个层面的危害:一是直接的经济损失,二是中间状态不可恢复带来的重复计算成本。

常见原因

1. 没有设置每次调用的 max_tokens 上限

模型默认会生成到自然结束或达到模型最大输出长度。如果 Agent 被要求「写完整个模块」,它可能生成数千 token 的代码,而你的预算是按「几百 token 的短回复」估算的。一个任务的实际消耗可以是预估的 10 倍。

怎么判断:查看计费后台的「单次调用 token 分布」,如果 p99 的 completion token 数远高于 p50,说明长尾调用在吃掉大部分预算。

2. 重试风暴放大了消耗

工具调用失败后,Agent 框架自动重试 3 次,每次重试都带上完整的对话历史(随着轮数增加,历史越来越长),导致重试的 token 消耗比第一次调用高出 2-5 倍。

怎么判断:统计 Trace 里同一个 tool call 的重试次数,以及重试时 prompt token 的增长幅度。

3. 子 Agent 数量超出预期

CrewAI 或 AutoGen 的动态任务分解会根据任务复杂度自动增加子 Agent 数量。如果原始任务描述模糊(如「分析这个代码库」),编排器可能启动 20 个子 Agent,而你预期的是 3 个。

怎么判断:在 Trace 里统计实际启动的 Agent 实例数,与 workflow 配置里的 max_workersmax_agents 对比。

4. 中间结果没有持久化,失败后全量重跑

任务在第 80% 处失败,但因为没有 checkpoint,框架从头重跑,触发更多 API 调用,进一步消耗预算,形成恶性循环。

怎么判断:检查 workflow 代码里是否有 checkpointresume 逻辑。如果失败后日志显示从 step 1 重新开始,就是这个问题。

5. 上下文压缩策略缺失

长对话没有使用 prompt caching 或滑动窗口压缩,每轮都把全量历史发给模型,导致 prompt token 随轮数线性增长。到第 50 轮时,每次调用的 prompt 可能已经有 100K token。

怎么判断:用 tiktoken 统计各轮的 prompt token 数,如果它随轮数单调递增,就缺少压缩。

6. 预算阈值设置在了账单层而不是代码层

只在 OpenAI 或 Anthropic 控制台设置了月度硬限额,没有在代码里实现软限额和提前预警。当硬限额触发时,任务直接报错终止,没有机会做优雅降级。

怎么判断:代码里搜索 budget / cost_limit / token_limit 关键词,如果只有硬编码的错误处理而没有主动检查,就是这个问题。

最短修复路径

Step 1:为每次 LLM 调用设置显式 max_tokens

# Anthropic SDK
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,          # 单次最大输出
    messages=[{"role": "user", "content": prompt}]
)

# LangChain
llm = ChatAnthropic(model="claude-sonnet-4-6", max_tokens=2048)

Step 2:实现代码层软限额,超过 80% 时提前预警

class BudgetTracker:
    def __init__(self, max_usd: float):
        self.max_usd = max_usd
        self.spent = 0.0
    
    def record(self, input_tokens: int, output_tokens: int, model: str):
        cost = self._calc_cost(input_tokens, output_tokens, model)
        self.spent += cost
        ratio = self.spent / self.max_usd
        if ratio >= 0.8:
            raise BudgetWarning(f"已用 {self.spent:.2f} USD,超过 80% 阈值")
        if ratio >= 1.0:
            raise BudgetExhausted(f"预算耗尽:{self.spent:.2f} / {self.max_usd:.2f} USD")
    
    def _calc_cost(self, in_tok: int, out_tok: int, model: str) -> float:
        # claude-sonnet-4-6: 3 USD/M input, 15 USD/M output
        rates = {"claude-sonnet-4-6": (3e-6, 15e-6)}
        in_rate, out_rate = rates.get(model, (1e-5, 3e-5))
        return in_tok * in_rate + out_tok * out_rate

Step 3:启用 checkpoint,支持断点续传

# LangGraph 示例
from langgraph.checkpoint.sqlite import SqliteSaver

checkpointer = SqliteSaver.from_conn_string("checkpoints.db")
graph = workflow.compile(checkpointer=checkpointer)

# 运行时传入 thread_id,失败后用相同 thread_id 恢复
config = {"configurable": {"thread_id": "task-20260525-001"}}
try:
    result = graph.invoke(input_data, config=config)
except BudgetExhausted:
    print("预算耗尽,下次用相同 thread_id 从断点继续")

Step 4:启用 prompt caching 减少重复 token

# Anthropic prompt caching:对稳定的系统提示加 cache_control
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    system=[{
        "type": "text",
        "text": long_system_prompt,
        "cache_control": {"type": "ephemeral"}  # 缓存系统提示
    }],
    messages=conversation_history
)

Step 5:对子 Agent 数量加硬限制

# CrewAI 限制并发 agent 数
crew = Crew(
    agents=agents,
    tasks=tasks,
    max_rpm=10,           # 每分钟最多 10 次 API 调用
    max_iter=5,           # 每个 task 最多 5 轮迭代
)

# AutoGen 限制对话轮数
groupchat = autogen.GroupChat(
    agents=agents,
    messages=[],
    max_round=20          # 整个 group chat 最多 20 轮
)

预防建议

  • 在所有 LLM 调用处设置 max_tokens,不要依赖模型默认值
  • 用 Langfuse 或 LangSmith 的成本仪表盘监控每条 workflow 的实际消耗,对超出 p95 的任务触发告警
  • 为所有长任务启用 checkpoint,保证失败后可以从中断点恢复,而不是全量重跑
  • 在任务启动前做「预算预估」:根据任务复杂度和历史数据估算 token 消耗,超出预算直接拒绝启动
  • 对递归或动态展开的子任务设置 max_depthmax_agents,防止任务树爆炸
  • 启用 Anthropic prompt caching,对稳定的 system prompt 和工具定义加 cache_control,可降低 60-90% 的 prompt token 成本
  • 区分「任务预算」和「月度预算」:每个 workflow 实例有独立的 token 预算,超出后优雅降级而不是强制中止

常见问答 (FAQ)

Q: Claude Code 的 Usage Limit 和 API 账单限额有什么区别? A: Claude Code 的 Usage Limit 是 Anthropic 在订阅层面设置的软限额(如 Pro 计划每 5 小时的 token 配额),超过后需要等待重置或升级计划。API 账单限额是你在 Anthropic Console 里手动设置的月度硬限额,超过后 API 调用直接返回 429。两者都需要在代码层捕获并优雅处理。

Q: 使用 prompt caching 后 checkpoint 还有必要吗? A: 有。Prompt caching 只降低重复调用的成本,不能恢复任务的执行状态。如果任务在第 30 步失败,没有 checkpoint 就必须从第 1 步重跑(即使每步的 token 成本更低,总成本依然很高)。两者解决不同的问题,应该同时启用。

Q: 任务预算耗尽后,已经生成的中间结果还能用吗? A: 取决于是否有 checkpoint。有 checkpoint 的框架(LangGraph、Temporal)会持久化每个节点的输出,可以用已完成部分的结果;没有 checkpoint 的框架(如裸 AutoGen、纯 LangChain chain)则全部丢失。这是部署长任务前必须解决的架构问题。

Q: 如何估算一个新任务的 token 消耗? A: 先用小规模输入(10% 数据量)跑一次,记录实际 token 消耗,然后线性外推。注意:对话式 Agent 的 token 消耗不是线性的——随着轮数增加,历史上下文会累积,总消耗通常是 O(n²) 而不是 O(n)。要在估算中加入这个系数。

相关阅读

标签: #AI 编程 #Agents #排查