账单又爆了。这句话我最近听到的频率,比“Agent 真香”高出好几倍。身边做 Agent 开发的朋友,十个里有七八个在某个深夜发现,自己部署的智能体并没有崩,但它正安静地躺在后台,一遍又一遍地调用同一个工具,像一只仓鼠在滚轮上狂奔,每跑一圈就烧掉一叠 token。你去翻日志,全是同一个动作、同一个报错、同一个“再试一次”的判断,然后你就会明白什么叫“明明什么都没干,钱却没影了”。这个话题的核心就三个关键词:Agent、循环调用、兜底方案。今天这篇不绕弯子,直接聊聊循环调用为什么会烧钱,以及我实测下来真正能止血的几套兜底方案。
1. 先复盘:Agent 为什么会在循环里烧钱
1.1 Agent 的运行机制:一个天然的循环结构
要理解为什么 Agent 会陷入循环,首先得看懂它的底层运行方式。任何一个 Agent 本质上是“感知—决策—行动”的循环:模型接收用户任务后,判断是否需要调用工具,工具返回结果后,模型再次分析结果并决定下一步动作,直到它认为自己能给出最终答案。这个结构天然就是循环的,本身没有问题,问题出在“退出的条件”上。
你可以把它想象成一个外卖小哥:任务是送到某个地址,他手里有导航(工具),每到一个路口就打开导航看一眼(调用工具),对比当前位置和目标位置,然后继续走。正常情况下走几步就到了,但如果导航地图数据有误,或者他一直走错路,小哥就会陷入“看导航—走两步—再看导航—再走两步”的死循环,永远到不了目的地,油费和流量却一直在烧。
Agent 的循环调用就是外卖小哥反复看导航的过程。模型每次循环都要把“当前状态 + 工具返回结果 + 历史对话”拼在一起发给大模型,拿到新的决策后再执行一次工具调用。这个过程的每一轮,都在消耗输入 token 和输出 token,也就是真金白银的 API 费用。
具体来说,我见过一个典型场景:客户服务的 Agent,用户只是问了一句“我的订单为什么还没发货”。Agent 决定调用订单查询接口,接口返回正常,但返回信息里没有“预计发货时间”这个字段。模型一看,觉得缺信息,又决定调用一次查询接口,结果还是一样。反复几次后,Agent 开始调用物流查询接口、客服工单接口、甚至一个完全不相关的用户画像接口,每次调用都是新的上下文和新的输出,最后在十几次循环之后给出一个和第一次判断一模一样的结论。
这就是循环调用的可怕之处:它不报错、不崩溃、看起来非常正常,甚至每一步决策都显得“有理有据”,但实际上已经偏离最初的目标十万八千里。
1.2 烧钱路径与成本模型:算一笔账你就清醒了
很多人对 Agent 烧钱的感知是模糊的,只知道“贵了”,但贵在哪里、贵了多少,心里没数。这里我直接算一笔账。
假设你用的是目前比较常见的中高配模型,输入 token 价格约为 0.005 美元/千 token,输出 token 价格约为 0.015 美元/千 token。一次典型的循环调用过程中,输入大概要带上前几轮的历史上下文,我见过的一个真实案例里,单轮循环的输入 token 大约在 3000 左右,模型输出一个决策加工具参数大约 500 token。
那么单次循环的成本就是:输入 3000 token × 0.005 美元/千 + 输出 500 token × 0.015 美元/千,算下来大约 0.0225 美元。如果这个 Agent 循环了 100 次,就是 2.25 美元;循环 1000 次,就是 22.5 美元。
看起来不多?别急,这只是单用户单请求。如果是一个上线的服务,同时有几十个用户触发这种循环,再加上上下文越长输入 token 越多、模型越贵,一天烧掉几百美元非常正常。而且别忘了,每次工具调用背后还挂着真实的下游服务:数据库查询、第三方 API、云函数执行,这些都是一层一层的费用。循环调用不仅烧 token,还在连锁烧你的基础设施账单。
我还见过更极端的情况:一个 Agent 在半夜里陷入循环,因为没人值守,整整跑了四个小时,API 账单多出了上千美元。这个事故的直接原因就一句话——代码里没有循环调用的兜底方案。所以接下来的重点就很明确了:怎么防、怎么控、怎么让它停下来。
2. 兜底方案的整体设计思路:三层防线组合拳
2.1 第一层防线:硬性终止机制
兜底方案最基础的动作,是给循环调用加一个“物理上限”。不管 Agent 觉得下一步多么有道理,只要循环次数达到阈值,就必须停止并返回当前已获得的结果。这就好比外卖小哥的电动车装了电量限制,跑够一定里程必须停下来,哪怕他觉得再走两步就能到。
具体到实现层面,硬性终止包含两个维度:一个是最大迭代次数,也就是 Agent 从开始到结束最多允许执行多少轮“模型决策 + 工具调用”;另一个是最大执行时间,也就是从任务开始到结束,最多允许多少秒。两个维度一横一纵,把循环限定在一个有限的矩形里。
为什么这两个缺一不可?我吃过亏才明白:有些 Agent 的循环次数不多,但每一轮调用一个很慢的外部接口,比如某个上游服务响应要 30 秒,循环 20 次就是 10 分钟,用户早就等疯了,时间成本也是成本。反过来,有些 Agent 循环极快但无限次,比如本地函数调用,每次只花几十毫秒,眨眼间跑了上万次,这时候时间维度就测不出来,必须靠次数维度兜底。
2.2 第二层防线:语义收敛检测
硬性终止只是“不管三七二十一先停”,但真正聪明的兜底是让 Agent 在合理的时候主动停下来——这就涉及到收敛检测。
我的理解是:“收敛”就是 Agent 的决策开始重复、结果开始稳定、继续调用的收益趋近于零。在很多死循环案例里,Agent 并不是完全没头绪,而是它已经在反复围绕同一个点打转了,只是模型因为自洽性偏好,觉得自己下一步“可能有新发现”。
我举一个真实场景。一个内容总结 Agent,任务是“找出文档中所有提到的时间节点”。它会调用一个文档解析工具,解析完发现一段文字提到“2023年”,又调用一次解析工具想确认上下文,结果返回的还是同一段文字,它继续调用、继续确认。每一轮它都很笃定,但输出的结果没有任何增量信息。
这时候靠的就是重复检测。把每次工具调用返回的关键结果做一个指纹记录,比如对返回文本做哈希,或者用 embedding 向量算相似度。如果发现连续 N 轮的输入输出高度相似,就可以判断 Agent 在空转,直接触发终止,并强制带回当前的结论。
借用前面外卖小哥的类比:他走了一段路后发现,自己绕了一圈又回到了同一个路口,这时候系统不该继续让他“再试试”,而应该直接判定“导航有问题,请用另一个方案”。
2.3 第三层防线:异常与超时兜底
前面两层防线防的是“Agent 自己陷入循环”,但实际开发中还有一个很常见的烧钱场景,就是 Agent 因为异常报错而反复重试。很多 Agent 框架在执行工具调用时,如果工具返回错误,框架默认会让模型重新决策,这在设计上是有意的——模型可能换一种方式调用就成功了。
但问题在于,有些工具的错误是确定性的、永久性的。比如这个工具已经被下架了,或者传入的参数本身就不可能有效。模型每次重试都是同一个错误,重试 20 次也不可能成功,但每次重试都要消耗 token。
这就是第三层防线要做的事:区分“可重试错误”和“不可重试错误”。可重试的是超时、限流、服务暂时不可用;不可重试的是参数非法、接口不存在、权限不足,遇到这类错误直接中断循环,不要给模型继续尝试的机会。同时在框架层面设置单次工具调用的超时时间,避免一个卡死的调用拖住整个 Agent。
三层防线合在一起,大概就是这样一个配合关系:语义收敛检测让局面尽快归拢,硬性终止守住最后底线,异常兜底掐掉无谓的重试。单独拎出任何一层都有漏洞,组合起来才能在多数事故中真正兜住。
3. 实操细节:把兜底方案真正落地到代码里
3.1 最大迭代次数守护:参数怎么定才不会误伤
聊完思路,进入实操。先说最常见的硬性终止实现。
很多 Agent 框架本身提供了 max_iterations 或者 max_steps 参数,比如一些主流开发框架里直接传进去就行。但如果你的 Agent 是手写的 ReAct 结构,或者想更精细地控制,建议自己实现一个循环守卫。
我常用的做法是维护一个迭代计数器,每次模型决策后递增,判断是否超过阈值。同时这个阈值不能是拍脑袋定的,我一般会结合任务复杂度来推导:预估正常情况下需要几步。比如“查天气”这种单工具任务,我认为 5 步以内必须结束;比如“分析一批数据并生成报告”,涉及多次查询和计算,我允许 20 步;比如“多 Agent 协作的复杂任务”,我会把单 Agent 的阈值放低,把总编排层的阈值放高。
这里给出一个参考基准表,来源于我实际项目的配置经验:
- 单一工具问答型任务:建议最大迭代 5-8 次,大多数正常完成只需要 2-4 次
- 多工具串联任务:建议最大迭代 15-20 次,预留一些修复错误的空间
- 数据分析/研究报告类:建议最大迭代 25-30 次,但每一步要有明确进展记录
- 多 Agent 协作(单个子 Agent):建议最大迭代 10-15 次,宁可让主调度层重新规划,也不让子 Agent 无限深入
这个阈值宁可偏小也不要偏大。少循环几次最多是任务失败,用户重新发一次请求;没有上限则可是一次没有尽头的烧钱。我见过有人把 max_iterations 设为 50,结果一个简单的 bug 让 Agent 真的跑了 48 次,账单直接多出几十美元。
代码实现里有个细节:不仅仅是“次数到了就抛异常”,更好的做法是“次数快到了就降级”。我一般会做两级处理:迭代达到阈值的 70% 时,在上下文中注入一条系统提示,让模型优先收敛;达到 100% 时,才强制结束并以当前已有的信息生成最终回复。这样既保证了兜底,也不至于在边缘情况直接中断导致任务完全失败。
3.2 重复检测与结果收敛判断:用相似度打分拦住空转
硬性终止是限流阀,但循环里最隐蔽的烧钱点是“表面每轮都在做不同的事,实际在原地打转”。重复检测的意义就是用算法识别这种假进展。
我目前用的比较顺手的方案是双通道检测:第一通道是文本指纹,适合检测完全相同的返回结果;第二通道是语义相似度,适合检测内容不同但含义相近的返回结果。
文本指纹很简单,每次工具返回后取内容哈希,放进一个列表,如果最新哈希在近期出现过,就记录一次重复。连续重复超过 3 次,断定空转。语义相似度稍微复杂一点,需要把返回文本转成 embedding 向量,然后计算与之前几轮结果的余弦相似度,超过 0.92 就视为重复。
为什么两个通道都要?因为实际场景里两种空转都出现过:一种是工具接口返回的数据完全没变,比如查一个状态字段,永远返回“处理中”;另一种是每次返回措辞不同但信息相同,比如有些模型自己生成的中间推理结果,句子换了几个同义词,实际内容一模一样。只做哈希检测会漏掉第二种,只做语义检测又浪费时间成本,双通道组合才是完整闭环。
需要强调的是,重复检测一定要在工具调用结束后立刻执行,不要等模型下一轮决策出来再判断。因为检测本身很便宜,而模型决策很贵。如果已经判断出上一轮结果和之前重复,这一轮的模型调用完全可以省掉。
我在一个日志分析 Agent 里做过实测:加了这个检测之后,空转循环的平均轮数从 11.6 轮降到了 2.3 轮,每个请求的 token 消耗大约减少了 40%。这不是一个复杂的优化,效果却非常显著。
3.3 超时控制与异常捕获:掐掉那些卡住的重试
超时控制是很多 Agent 项目最容易忽略的一环,却是事故高发区。我遇到过的最离谱的一次,是 Agent 调用一个内部数据分析接口,接口因为死锁一直不返回,Agent 也没设置超时,就那么挂着,用户的请求没结束,计费一直在走,直到云平台的内存被吃完才触发 OOM 终止,整整浪费了一个小时。
针对这种情况,我给自己的 Agent 框架做了三层超时:单次工具调用超时、单轮模型决策超时、整个任务总超时。单次工具调用超时根据接口性质来定,查询类接口 10 秒,复杂计算类 30 秒,超过就返回一个“工具超时”的状态给模型,让它决定是换一个工具还是给出最终答案。单轮模型决策超时主要是防模型推理卡死,这个在大模型 API 偶发长响应时很有用。整个任务总超时在前面已经说过,是最后一道闸门。
异常捕获方面有一条经验法则:不要把异常直接抛给模型,而是先自己做分类。我把异常分成三类——瞬时故障、永久故障、未知故障。瞬时故障如超时、限流,允许重试,但重试次数要封顶,我一般只允许 2 次;永久故障如 403 权限、404 接口不存在、400 参数非法,直接终止该工具调用,并把“这个工具不可用”作为事实写入上下文,让模型改用其他方案;未知故障则统一按可控错误处理,记录日志后返回一个可读的错误描述,而不是让模型拿着堆栈信息反复猜测。
分类的关键在于错误信息本身。很多工具返回的错误码里带着明确含义,只要你在代码里做一个映射表,就能把大部分异常精准归类。如果工具返回的是自然语言的错误描述,那就用关键词匹配,比如“not found”“permission denied”“invalid argument”,分别对应永久故障的不同子类。
3.4 成本监控与熔断机制:让兜底变成一个自动化的收盘控制
前三节解决的是“让 Agent 停下来”,这一节解决的是“让整个系统在失控之前停下来”。我把它称为成本监控与熔断机制,它在架构上高于单个 Agent 的循环控制,作用于整个服务。
核心思路是在 Agent 的执行链路上做计量打点:每一次模型调用、每一次工具调用,都把 token 消耗和费用累加到一个全局计数器或分布式计量服务里,并实时对比预算阈值。我一般设置两个阈值:一个警告阈值,比如预算的 60%,触发时推送告警,并给正在执行的 Agent 注入提示,要求它简化后续步骤;另一个是熔断阈值,比如预算的 100%,触发时强制终止所有正在运行的 Agent 任务,并返回一条“预算超限,请稍后重试”的降级响应。
这个机制的灵感来自金融领域的熔断器模式,但用在 Agent 上有一个额外的价值:你不仅能控制费用,还能反向定位问题。每次计量打点都记录当前 Agent 的循环轮数、最近一次调用的工具、最近一段上下文摘要。一旦触发熔断,这条链路记录就是排查事故的第一手资料,到底哪一步开始空转、哪个工具调用最多,一目了然。
实操上我推荐用装饰器或者中间件模式来做计量,而不是在每个 Agent 代码里手动埋点。比如写一个 call_model 的包装函数,所有模型请求都走它,在包装函数里统计 token、计算费用、检查是否达到熔断阈值。这样无论业务代码怎么变,计量逻辑都不会漏。如果你的 Agent 是跑在现成的框架上,那就看框架是否暴露了请求回调或事件钩子,很多框架都有,挂上钩子就行。
4. 常见问题与排查技巧实录
4.1 循环调用问题速查表
下面这份速查表来自我过去一年排查过的真实案例,按现象、原因、定位方法、解决方案四栏整理,遇到类似问题时可以直接照着查。
| 现象 | 可能原因 | 定位方法 | 解决方案 |
|---|---|---|---|
| Agent 反复调用同一个工具,且结果相同 | 模型认为信息不足,实际已有信息够用 | 对比连续 N 轮工具返回的文本指纹 | 加重复检测,连续 3 轮重复则强制收敛 |
| Agent 每次调用不同的工具,但目标漂移 | 上下文过长导致模型遗忘原始目标 | 检查上下文截断策略,翻日志看每轮决策时是否还带着用户原问题 | 明确在每一次模型请求里保留用户核心指令,或用额外的目标注入 |
| 工具返回报错,Agent 不停重试 | 永久性错误被当作瞬时错误处理 | 查看错误码和重试次数统计 | 做错误分类,永久错误直接终止该路径 |
| Agent 运行很久,但循环次数不高 | 单次工具调用耗时过长 | 给每个工具调用加耗时日志 | 设置单次工具调用超时,超时后降级处理 |
| 任务看似正常结束,但费用异常高 | 模型在最后几轮来回否定自己的结论 | 查看最终回答前的几轮决策记录 | 设置“结论稳定性”检查,连续 2 轮结论一致则提前终止 |
| 多 Agent 协作时子 Agent 互相等待 | 子 Agent 之间没有协作超时 | 检查消息队列堆积情况和子 Agent 心跳日志 | 给每个子 Agent 加执行预算,超预算由调度主 Agent 接管 |
| 日志报“Agent execution terminated due to error.” | 框架或代码里发生未捕获异常导致执行终止 | 查看异常堆栈和终止前的最后动作 | 区分框架层错误和业务层错误,确保兜底逻辑在异常时仍能执行 |
这个表格里的每一行都是我或身边同行踩过的坑。特别是“多 Agent 协作”那一行,现在很多人提倡多 Agent 架构,但多 Agent 意味着每层都可能循环,子 Agent 之间互相传递结果,任何一个环节失去收敛性,整个编排就变成了一场无限开会讨论,烧钱速度比单 Agent 快一个数量级。
4.2 排查技巧:三条实战心得
除了上面的速查表,我再分享三个真正提升排查效率的技巧。
第一条:给每个 Agent 加上唯一的 request_id,并让它贯穿所有日志和计量记录。听起来老生常谈,但真到了排查循环问题的时候,没有 request_id 你会疯掉的。因为循环调用涉及的日志分散在模型请求、工具调用、下游服务三层,没有统一标识,你根本拼不出一个完整的执行链路。我在自己项目里做了一个 LogContext 工具,用 contextvars 在异步任务里自动携带 request_id,所有日志行都自动带上,排查时一条 grep 命令就能拉出全部链路。
第二条:在开发环境里故意制造死循环。听起来很反直觉,但我建议每个 Agent 项目在测试阶段都写一个“沙盘任务”,专门触发循环。比如让 Agent 去查一个永远返回“处理中”的假接口,或者输入一个自相矛盾的任务指令,看你的兜底方案会不会在预期时间内触发。我见过太多人写了兜底逻辑但从来没测试过,上线后才发现兜底代码本身有 bug,比如循环计数器在异步任务里被并发修改导致失效。提前做故障演练,比上线后救火强太多。
第三条:重视工具返回的结构化设计。很多循环调用其实是工具返回格式不佳导致的。如果工具返回的是大段非结构化文本,模型每次都要花大量 token 去解析,而且可能解析出不同的结论,就很容易反复尝试。我建议对所有工具返回设计一个统一的 status + data + message 结构,status 明确表示这次调用是成功、部分成功还是失败,data 放结构化结果,message 放人话说明。模型一眼就能判断结果是否可用,决策自然更快。另外,在工具返回里加上一个字段,明确告诉模型“此结果已是当前条件下最完整的数据”,能从源头减少模型“再查一次”的冲动。
4.3 从源头减少循环:提示词与工具设计上的预防
最后聊一个容易被人忽略的方向:很多循环调用不是运行时的问题,而是设计阶段埋下的隐患。与其在循环发生后靠兜底止损,不如在提示词和工具设计层面提前降低循环发生的概率。
提示词层面,我总结了几个关键要点。第一,在 System Prompt 里明确写清楚“终止条件”,比如“当工具返回的内容足够回答用户问题时,必须停止调用工具并直接给出回答”。很多模型并不是故意空转,而是它的训练目标倾向于“尽可能提高答案质量”,表现为不停地补充信息。你要用直接指令把这个行为刹住。第二,给模型一个“放弃选项”,比如提示“如果你认为当前问题无法通过已有工具解决,请直接说明无法解决,不要重复尝试”。这个看似简单的授权,能有效减少模型死磕一个不可能任务的情况。
工具设计层面,前面已经提到结构化返回,这里补充一点:工具参数要尽量给模型“确定性”。如果你的工具支持模糊查询,模型可能会反复尝试不同的模糊参数来碰运气,比如先按订单号查、再按手机号查、再按时间范围查。如果工具说明里明确写了“只支持精确匹配,其他方式请勿尝试”,模型就不会乱试。这就是给工具设置边界,边界本身就是防循环的有效手段。
我还建议在工具描述字段里加上“何时不该调用此工具”的说明。比如某个查询工具的描述里写出“注意:当用户问题不涉及数据查询时,不要调用本工具”。这样模型在每一轮决策时,会多一层自我约束。实测下来,这一步能把低质量工具调用的比例降低 20% 左右。
整体看下来,循环调用的治理不是某一行代码能解决的,它需要你在架构设计、代码兜底、提示词约束三个层面同时发力。把硬性终止、收敛检测、异常兜底、成本监控全部做扎实,再配合工具设计上的预防,你的 Agent 才能既聪明又省钱。
我在实际项目里把这套方案全部落地之后,最直观的感受是:同类任务的平均 token 消耗下降了大概一半,账单再也没出现过无征兆的飙升。更省心的是,以前半夜收到告警说 Agent 跑飞了,现在基本不用管,兜底逻辑自己就把问题处理掉了。你会慢慢理解,Agent 开发里最贵的其实不是模型调用,而是那些你不知道它正在发生的失控。把这条底线守住,其他优化才有意义。