避免智能体循环耗尽你的 API 预算
我会直接起草这篇文章,加入具体的工程策略、对比表和实用示例。内容会围绕真实的循环行为和成本控制展开,并确保以可执行的建议收尾。## 耗尽 API 预算的最快方式
我曾亲眼看到,一个“简单”的编程智能体在不到两分钟内,把一条 400 词元的用户请求变成了账单上一笔 28,000 词元的费用。
这种模式熟悉得令人痛心:智能体调用搜索工具,总结结果,重新阅读自己的总结,换个略有不同的说法再次调用同一个工具,然后不断扩充上下文,直到每一轮都带上整段对话、半个代码仓库,以及一连串失败的计划。模型本身没有问题,出问题的是循环设计。
这就是智能体系统的核心问题:成本并不随用户提示词增长,而是随 循环行为增长。一个糟糕的循环就能成倍增加:
- 持续膨胀的上下文带来的输入词元,
- 冗长“思考”带来的输出词元,
- 重复调用带来的工具费用,
- 以及串行重试带来的延迟。
如果你正在构建生产环境中的智能体,真正的任务并不是“让模型更聪明”,而是“让循环成本更低、流程更短、更难失控”。
为什么智能体循环会让成本暴涨
普通聊天的一轮交互只有一次输入和一次输出。智能体循环则包含如下序列:
- 用户提出问题,
- 模型制定计划,
- 模型调用工具,
- 工具返回数据,
- 模型读取数据,
- 模型决定下一步,
- 重复上述过程。
每次迭代都会为下一次迭代的提示词增加新的词元。如果你不断追加完整的工具输出和先前的思考内容,提示词就会大致呈阶梯式增长。
在实际运行中,这会产生三种成本放大因素:
- 上下文累积:每一步都带上此前各步的内容。
- 工具调用冗余:使用略有不同的参数反复调用同一个工具。
- 失控的重试:模型已经做得足够多,却仍不断“再试一种办法”。
一个常见的坑是,词元浪费通常很难察觉,直到你查看每次运行的追踪记录。最终答案可能很简洁,但隐藏的执行链可能比用户可见的输出大 10x。
一个简单的成本模型
如果一个循环运行 N 步,每步包含:
C词元的携带上下文,I词元的工具输出重新注入上下文,O词元的模型输出,
那么总量可粗略估算为:
total_tokens ≈ N * (C + I + O)
这个估算偏乐观,因为 C 通常会随着循环继续而增长。在真实的智能体中,后面的轮次往往成本最高。
真正重要的控制手段
你可以从几个地方着手施加控制。关键在于将它们结合起来。
| 控制手段 | 减少的内容 | 最佳适用场景 | 权衡 |
|---|---|---|---|
| 步数上限 | 失控循环 | 任何使用工具的智能体 | 处理困难任务时可能提前停止 |
| 上下文裁剪 | 输入词元 | 长程任务 | 可能丢失有用的历史信息 |
| 成本更低的子智能体 | 常规工作的费用 | 搜索、提取、分类 | 协调开销 |
| 工具结果缓存 | 重复的工具费用 | 稳定的数据源、重复查询 | 数据过时与缓存失效 |
| 费用可观测性 | 意外的高额账单 | 生产系统 | 需要埋点工作 |
| 循环停止条件 | 无限或近乎无限的循环 | 不可靠的智能体 | 必须定义“足够好”的标准 |
最有效的生产环境配置会使用所有这些手段。
1) 严格限制步数
这是最简单、也最被低估的解决办法:如果你不设定硬性上限,就等于相信模型会自行停止。
我通常从以下设置开始:
3–5步,用于范围较小的任务,6–8步,用于中等复杂度的任务,- 只有在理由充分时才增加步数。
合理的上限不仅是安全阀,还会促使任务得到更好的拆解。如果智能体只能行动四次,它往往会:
- 更早请求使用正确的工具,
- 避免探索性的绕路,
- 并更快收敛到可用的答案。
循环控制器示例
MAX_STEPS = 5
for step in range(MAX_STEPS):
action = model.next_action(state)
if action.type == "final":
return action.final_answer
observation = run_tool(action.tool_name, action.arguments)
state = update_state(state, action, observation)
raise RuntimeError("Agent stopped after step cap")
一个常见的坑:步数上限应适用于 整个计划,而不是分别对每个子程序独立生效。否则,你只是把失控增长从顶层智能体转移到了嵌套的辅助程序中。
2) 每轮都裁剪上下文
大多数智能体携带了过多的历史信息。它们不需要此前每一次工具响应,尤其是当响应很长、却只有一个细节有用时。
在实践中,分层记忆策略给我带来了最好的效果:
- 原样保留:用户请求、当前目标、最终决策、尚未解决的约束。
- 总结:先前的推理、分支尝试、冗长的工具输出。
- 丢弃:走入死胡同的假设、重复的工具结果、旧的中间草稿。
应保留哪些内容
紧凑的状态对象比直接回放原始对话记录更有效:
{
"goal": "Find the failing endpoint and propose a fix",
"constraints": [
"No schema changes",
"Must preserve backward compatibility"
],
"known_facts": [
"500s started after deploy 1842",
"Only /v1/search is affected"
],
"open_questions": [
"Is the new cache layer returning stale payloads?"
],
"step": 3
}
这比向模型输入 20 轮“边想边说”的内容便宜得多。
实用的裁剪模式
使用三个缓冲区:
working_set:当前轮次、工具输出、当前执行中的计划。summary_memory:压缩后的历史记录。archive:审计时可能需要、但不必放入提示词上下文的内容。
智能体只能看到 working_set + summary_memory。当 working_set 变得过大时,将其总结到 summary_memory.
这里的陷阱是摘要漂移。如果摘要生成过程会丢失信息,就可能抹去约束,导致智能体重蹈覆辙。我倾向于将结构化事实与自由形式的摘要分开保留,这样模型就不必从文字描述中推断这些事实。
3) 用更便宜的子智能体处理简单工作
并非每一步都值得动用你最好的模型。
如果任务是:
- 从 JSON 中提取字段,
- 对工单进行分类,
- 检查文件是否发生变化,
- 或生成简短的搜索查询,
那么更小、更便宜的模型通常就够用了。将昂贵的模型留给规划、综合分析和处理歧义。
常见分工
- 主智能体:对任务进行推理,决定何时停止。
- 子智能体:执行搜索、提取或规范化等范围明确的工作。
- 验证器:检查输出的格式、完整性或明显错误。
一种实用架构:
User
-> Orchestrator (strong model)
-> Search agent (cheap model)
-> Extract agent (cheap model)
-> Final synthesis (strong model)
为什么这样有效
主模型可以专注于控制流程,而不是亲自处理所有事情。子智能体则可以通过提示词限定任务范围,并严格限制 token 用量。
搜索子智能体的提示词示例:
Return only:
1. the best search query,
2. 3 likely source categories,
3. no explanation.
这种范围明确的约定能让输出保持简短且可预测。
如果你的平台提供价格实惠的多模型访问能力,这正是它发挥价值的地方:用一个强大的模型负责协调,用一个更便宜的模型处理重复性子任务,没有理由把高价 token 消耗在常规解析上。
4) 认真做好工具结果缓存
智能体循环经常为同一份信息重复付费:
- 重复读取文件,
- 重复查询数据库,
- 重复进行网页搜索,
- 重复调用嵌入或检索功能。
缓存工具输出是你能做出的投资回报率最高的改进之一。
使用语义键缓存,而不是原始文本
只要措辞稍有变化,原始提示词缓存就无法命中。更好的缓存键应基于规范化后的意图:
def cache_key(tool_name, args):
normalized = normalize_args(args)
return f"{tool_name}:{hash(normalized)}"
缓存条目示例:
{
"tool": "repo_search",
"query": "auth middleware rate limit bug",
"result": {
"files": ["src/middleware/auth.py", "src/api/routes.py"],
"ts": 1710001123
}
}
缓存在哪些场景下最有帮助
- 代码仓库检查智能体,
- 文档搜索,
- 内容稳定的知识库,
- 确定性转换,
- 多轮工作流中的重复检索。
缓存可能带来问题的场景
- 快速变化的数据,
- 因用户而异的结果,
- 任何对数据时效性有严格要求的内容。
常见的陷阱是缓存了工具输出,却没有向智能体说明数据的新鲜程度。如果模型拿到的是缓存数据,却以为是实时数据,就可能做出错误决策。应在工具元数据中包含时间戳或有效期。
5) 在仪表板上展示支出
如果你只跟踪延迟和成功率,就会漏掉真正的失败模式:代价高昂的成功。
你需要在每次运行中观测以下指标:
- 输入 token 数,
- 输出 token 数,
- 工具调用次数,
- 工具延迟,
- 总支出,
- 步骤数,
- 停止原因。
最简事件结构
{
"run_id": "r_8f2c",
"step": 4,
"model": "gpt-5.5",
"input_tokens": 1842,
"output_tokens": 221,
"tool_name": "repo_search",
"tool_latency_ms": 312,
"estimated_cost_usd": 0.087,
"stop_reason": "step_cap"
}
实际运行中应关注什么
- 每个成功任务的 token 用量:最直接的成本指标。
- 每一步的 token 用量:用于发现提示词膨胀。
- 重复的工具调用:通常意味着规划存在缺陷。
- 停止原因分布:如果大量运行因达到上限而结束,说明智能体的规划不够充分。
按任务类型统计成本是最有用的图表之一。有些类别自然会比较昂贵,这没有问题。你要找出的是那些本应便宜、实际却不便宜的任务。
6) 尽早终止失控循环
循环的停止理由不应只有“我已达到步骤上限”。
有用的停止条件包括:
- 模型已生成最终答案,
- 同一工具调用不断重复,却没有带来新信息,
- 置信度或效用的增量低于阈值,
- 验证器判定任务已解决,
- 计划已停滞。
循环中断器示例
seen_calls = set()
for step in range(MAX_STEPS):
action = model.next_action(state)
signature = (action.tool_name, canonicalize(action.arguments))
if signature in seen_calls:
return fail("repeated tool call with no new information")
seen_calls.add(signature)
observation = run_tool(action.tool_name, action.arguments)
state = update_state(state, action, observation)
if verifier_says_done(state):
return finalize(state)
这可以发现一种非常常见的失败模式:模型“想要帮上忙”,却只是在改写同一个查询。
另一项有用的防护措施:效用门控
如果某一步没有让状态发生足够大的变化,就不要继续。判断依据可以很简单,例如:
- 没有找到新文件,
- 没有提取出新实体,
- 答案的置信度没有提升,
- 没有加入新的证据。
具体指标取决于任务,但必须以某种方式衡量边际收益。
调优到位后,实际会发生什么
收紧循环之后,智能体通常会在一些起初容易被忽略的方面变得更好:
- 它会少说废话,决策更果断。
- 它会更早请求合适的工具。
- 它会减少“我还在调查”的循环。
- 它的成本会更低, 和 速度也会更快,因为轮次减少意味着上下文更少、延迟更低。
需要注意的权衡是,硬性上限和激进裁剪可能让系统在处理真正困难的任务时变得脆弱。因此,不要把智能体实际上因超时而终止的问题说成已经“解决”。应在日志和面向用户的消息中如实说明。
我的经验法则是:优化目标应是 有边界的胜任能力 而非无休止的探索。
实用的生产环境配置
如果明天就要上线一个智能体,我会从以下配置开始:
MAX_STEPS = 5- 使用结构化状态,而非重放完整对话记录
- 每轮结束后生成摘要记忆
- 用低成本子智能体执行搜索和提取
- 为工具结果建立缓存,并附带数据新鲜度元数据
- 记录每次运行的 token 用量和成本
- 重复调用检测器
- 明确的停止原因
这套配置并不亮眼,但它能防止账单增长速度超过产品增长速度。
实践要点
- 首先限制步骤数;这是防止循环失控的最低成本防护措施。
- 大幅精简上下文,并将结构化状态与摘要分开保存。
- 让更便宜的子智能体处理范围明确的任务,避免处处消耗高价 token。
- 缓存可重复使用的工具输出,但要明确呈现数据新鲜度,避免模型盲目信任过时数据。
- 按每次运行、每个步骤和每次工具调用记录支出,让成本恶化立即显现。
- 在出现重复、进展停滞或确认完成时停止循环,而不是只在资源耗尽时停止。
- 围绕有边界的胜任能力进行设计:足够好、快速且可预测,始终胜过聪明但昂贵。
一个 API 密钥即可使用 Claude Opus 5.5、Sonnet 5、Haiku 4.5 和 Fable 5.1,以及 GPT-6 个模型。按量付费,无需订阅。
获取 API 密钥 →