ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent循环调用烧钱?硬性护栏与软性监控双管齐下

Agent循环调用烧钱?硬性护栏与软性监控双管齐下 1. 问题背景Agent 的隐形烧钱黑洞做 Agent 开发的朋友应该都有过这种体验早上起来看了眼账单发现昨晚测试跑了几次费用高得离谱。查日志一看好家伙Agent 在一个循环里转了二三十圈每圈都在调大模型接口每次调用都在烧 token。而你只是让它做个简单的网页信息提取。这不是个别现象。我在实际使用主流 Agent 框架时踩过几次坑之后才意识到循环调用是 Agent 项目里最大的“费用漏点”之一而且非常隐蔽——它不会让程序报错但会让你的账户余额肉眼可见地往下掉。更麻烦的是这类问题在开发阶段往往不显眼因为本地调试时 token 消耗小、单次调用量低一上生产或者跑长任务问题就彻底暴露了。先说清楚“循环调用”到底指什么。在 Agent 系统里模型通过“推理—行动—观察—再推理”的循环来完成任务每一步都要调用一次大模型接口。正常流程下Agent 得到足够信息后就会收敛并输出结果但现实里非常容易出现以下情况模型反复调用同一个工具、反复得到同样的中间结果、或者因为上下文过长而遗忘掉自己已经尝试过的方案于是循环往复直到把上下文窗口塞满或者达到预设步数上限。如果你做过 Agent 开发一定有印象很多框架的默认配置里循环上限是25步甚至更多。每一步背后是数千甚至上万 token 的消耗一轮循环烧掉几万 token 是常有的事。按 GPT-4o 或 Claude 的价格算一次失控循环可能就是几美元。而这仅仅是一次运行。如果你的系统有并发、有用户流量这个数字会被放大得非常快。所以问题就变成了我们能不能在循环失控之前用一套兜底机制把它拦住答案是可以的而且实现思路并不复杂。这篇文章就聊两种主流方案——硬性兜底结构性限制和软性兜底Agent 自身的自我监控以及我在实际项目中用到的具体实现细节。2. 为什么会发生循环调用三步定位根因2.1 模型层面的“惯性遗忘”大模型在长任务执行中常常会遗忘早期已尝试过的方案。举个例子我做过一个电商数据分析 Agent它需要调用订单查询工具、库存工具、价格工具。正常情况下查完订单再查库存就行但有一次它反复调用订单工具三次每次拿到同样的数据依然没有进入下一步。看日志发现模型每一次的 reasoning 都在分析“我需要查询订单信息来了解销售情况”完全没意识到上一步已经拿过了相同结果。这种现象的本质是模型在超长上下文中对“已发生的事实”的注意力衰减。当轮次推进、历史对话变长模型对早前工具输出的记忆变得模糊以至于重复发起相同调用。框架层如果不加状态追踪模型自己在纯靠注意力机制的时候就很容易陷入这种短时记忆缺失的循环。2.2 工具设计层面的“无效重复”另一种常见成因是工具的输出没有真正改变环境状态。比如你做了一个“获取天气”工具每次调用都返回同样的数据模型如果判断“得到的结果不全”就会反复重试同一工具。此时模型不是犯傻而是它认为“多试几次”能拿到不同结果——但工具的幂等性决定了结果永远一样。说白了Agent 的行为模式很大程度上是被工具的输出信号塑造的。如果工具的输出足够明确例如直接返回“查询成功数据如下”模型会自然继续下一步。但如果输出模棱两可比如只返回了部分字段模型就会倾向再来一轮。这个看起来像模型的问题根源却在你设计的工具接口上。2.3 编排层面的“步数上限失守”再往下挖一层框架层面的失误。很多 Agent 框架默认给了较高的最大步数25 或 30初衷是保证复杂任务有足够空间完成。但在实际执行中模型恰恰会利用这个空间“拖延”——不是因为恶意而是因为它遇到不确定时倾向于多尝试几次而不是果断给出结论。这三层因素叠加循环调用就变得非常常态。理解了根因再来说兜底方案就有的放矢了。3. 兜底方案一硬性护栏Hard Guardrails硬性护栏的思路很简单在框架外部给 Agent 的执行过程套上不可逾越的限制一旦越过就立即终止并向用户返回明确提示。3.1 最大迭代次数的合理设置这个参数很多框架都有默认值如 LangChain 的max_iterations、AutoGen 的max_consecutive_auto_reply。但问题在于默认值往往不适合你的具体业务。以我实践的经验如果任务是单一工具调用比如“查询订单状态”最大迭代次数设4~5 步就足够了。模型第一次推理 调用工具 观察结果 生成回答正常是 3~4 步。超过 5 步还完不成基本可以认定任务出了异常。如果是多工具协同的复杂任务比如“分析用户行为并生成报告”可以放宽到8~10 步但要配合后续的重复检测机制防止模型在 10 步里绕圈。设置逻辑只有一个公式预期正常步数 × 1.5~2 倍 最大迭代次数。这样既给了模型足够的容错空间又不会让失控循环烧掉过多 token。3.2 全局超时控制为整个 Agent 执行加“闹钟”迭代次数是对“步数”的限制但每步之间的大模型调用可能因为网络、限流等原因卡住很久所以还需要一个时间维度上的兜底。我在生产环境用的方案是给整个 Agent 执行过程包一层看门狗定时器。用 Python 实现核心逻辑大致如下import signal class TimeoutException(Exception): pass def timeout_handler(signum, frame): raise TimeoutException(Agent 执行超时已强制终止) def run_agent_with_timeout(agent_func, timeout_seconds120): signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout_seconds) try: result agent_func() signal.alarm(0) # 取消定时 return result except TimeoutException: log_warning(Agent 执行超过 %s 秒触发兜底终止, timeout_seconds) return generate_timeout_response()这里有几个注意点超时时间的选择不固定而是要测量正常任务的平均耗时再乘 1.5 倍作为兜底阈值。简单任务 30 秒复杂任务 120 秒起步实测后调整。signal 模块的局限只能在主线程起作用如果你的 Agent 运行在子线程或异步环境中需要用async版本的定时器或改用concurrent.futures的超时机制。3.3 执行日志与上下文窗口监控硬性护栏不只是“终止执行”更重要的是尽早识别异常并介入。我常用的办法是维护一份“执行痕迹”列表记录每一步的工具名、输入摘要、输出摘要。每走一步检查两个指标末尾 N 步是否有重复动作比如连续 3 次调用同一个工具且输入参数一致基本可以判定为循环。已生成的历史消息占的最大上下文比例超过 80% 意味着模型很快会因上下文溢出而表现异常。一旦命中立即触发提前终止不必傻等步数上限。这一步非常关键——能多省下后面那 10 轮无效调用的钱。4. 兜底方案二软性监控模型自我监督硬性护栏是“一刀切”粗暴但可靠。软性监控是让模型自己意识到“我在循环”从而主动收敛。两种方案互补使用效果最好。4.1 给系统提示注入“循环自检指令”在 Agent 的 System Prompt 里加入一段明确的自检逻辑。我用的版本大致是在执行过程中每完成一步行动请回顾你的上一步行动 - 如果你发现即将进行的动作与之前某一步完全相同包括工具名和核心参数说明你可能陷入循环。 - 此时你应该停止执行直接基于已有信息给出最终答案并在回答开头注明“注意检测到重复行动已提前结束”。这个方法在国内大模型和国外主流模型上都实测过效果非常明显。原因是模型的注意力机制虽然会遗忘早期细节但对最近一两步的记忆是非常清醒的。给它一个明确的“检测信号”它就能很自然地完成判断。但注意一个坑Prompt 里写“循环检测”这类抽象概念模型往往理解不到位。必须配合举例说明“什么情况算是重复”比如“第一次调用 search_tool 查询‘张三’结果为空第二次仍调用 search_tool 查询‘张三’这就是重复”。例子越具体模型越容易对齐。4.2 短时记忆窗口裁剪让模型“忘记”无效尝试如果模型反复尝试同样的路径一个激进但有效的方案是把它历史中无效尝试的细节截断只保留结论摘要。比如模型在前三步尝试了三种不同的方法都没拿到数据传统做法是把这三轮对话完整保留在上下文里。这会让模型每轮都在“回顾”这些失败不自觉地再次尝试。而我们的做法是保留“已尝试方法总结”这样一个短摘要然后裁剪掉所有中间报错细节。这样有两个好处一是上下文缩短费用直接降低二是模型不再被冗余信息“带偏”更容易收敛到新方案上。这个方案我觉得对所有 Agent 项目都有借鉴价值尤其是任务链比较长、工具调用比较频繁的场景。核心观察就是模型的行为会受历史上下文影响我们主动管理历史内容比被动等它“遗忘”更可控。4.3 基于执行痕迹的提前终止判断这是硬性方案中“重复检测”的升级版不只是检测“连续重复”而是检测“模式重复”。我设计过一个简单的评分公式重复分数 最近3次动作中相同工具调用次数 × 0.4 最近3次输入参数的余弦相似度均值 × 0.6 如果 重复分数 0.7判定为循环触发提前终止参数向量可以用简单的字符串 Hashing 得到。这套公式成本极低、计算量几乎为零却能有效捕捉“模型换了说法但其实是同一个行动”的场景。实测下来这个方案能拦截大约 70% 的潜在循环失控剩下 30% 靠硬性步数上限兜底。两套搭配基本能把“循环烧钱”控制在一个可接受的范围内。5. 实操过程一个完整的兜底改造实录上面讲了很多原理接下来我整理一个实际改造流程。这是我为一个内部 Agent 系统添加兜底能力的完整记录你可以直接参考。5.1 现状评估与改造目标这个 Agent 项目主流程用户输入 → 意图识别工具 select_skill → 调用对应业务工具 → 汇总输出结果业务工具包括订单查询、库存查询、物流跟踪三个总上下文窗口为 128K单次任务平均耗时约 20 秒平均步数 4 步。痛点现象处理含大量 SKU 的任务时模型经常在订单查询和库存查询之间来回切换最多出现过 19 轮调用、消耗 8 万 token 的极端情况。改造目标有三点将循环导致的无效 token 消耗降低 80% 以上。异常任务必须在 60 秒内以优雅响应结束不能直接白屏报错。不改变正常任务的完成质量。5.2 具体改造步骤第一步调整框架步数上限修改主 Agent 的配置参数agent_config { max_iterations: 8, # 从默认 25 降到 8 max_execution_time: 60, # 整轮执行时间上限 60 秒 early_stopping_on_repeat: True, }这一步先解决“最大空转空间”的问题。25 步降为 8 步意味着就算模型陷入循环最多烧 8 轮的 token而不是 25 轮。第二步叠加重复调用检测器在框架的每次工具调用回调中植入检测逻辑def on_tool_call(tool_name, tool_input): recent_steps.append({ tool: tool_name, input: tool_input }) # 检测最近5步中是否存在超过2次相同工具相同输入 duplicate_found False for step in recent_steps[-5:]: count sum(1 for s in recent_steps[-5:] if s[tool] step[tool] and s[input] step[input]) if count 3: duplicate_found True break if duplicate_found: interrupt_agent(检测到重复工具调用停止当前循环) return build_fallback_response()注意这里比较的是“工具名 输入参数”完全一致而不是只看工具名。因为有些场景下模型确实需要连续调用同一工具去获取不同页的数据比如翻页查询只有参数也相同才算真重复。第三步系统提示注入自检指令在 Agent 的 System Prompt 末尾拼接一段自检逻辑这段对中文大模型的响应稳定性帮助非常大你在执行任务时应保持效率。若你发现 1. 你已经使用同一工具和同一参数发起过相同请求 2. 你反复得到相同结果后仍想再尝试一次 满足任一情况请立即停止行动阶段直接根据已有信息输出最终结果。第四步上下文裁剪策略这部分有点复杂只说一下核心思路。我在工具调用记录里给每条记录打了一个“有效产出”标记。每轮行动结束后如果工具返回的数据被后续行动引用标记为“有效”否则标记为“无效”。当无效记录累积达到 3 条时触发一次裁剪把最旧的无效记录的完整内容替换为一句摘要例如“已尝试通过订单查询渠道获取库存信息失败”。这个策略的效果是模型不会反复看到完整失败细节从而更容易跳出“执着于旧方案”的陷阱。实测中这一步对降低重复调用率贡献非常明显。5.3 实测效果与数据对比改造后在相同数据集上重新跑测试结果如下指标改造前改造后变化平均步数4.74.1-13%最大步数198-58%平均单任务 token 消耗12.4K6.8K-45%平均单任务耗时22s17s-23%触发兜底终止次数012增加预期内任务完成率96.3%95.1%-1.2%可接受值得注意的一点任务完成率有小幅下降。原因是之前一些任务会在循环中偶然“撞对”答案而截断后模型会更快地停止尝试自然少了一些随机成功的机会。解决办法是对重要任务单独保留更高的步数上限比如 12 步并用更精细的重复检测来防失控而不是一刀切降低所有任务的步数。6. 常见问题与排查技巧实录6.1 为什么我的重复检测误杀了很多正常任务这是我在实践中遇到最多的问题。误杀的典型场景模型在查询数据时先按“商品名”查了一次拿不到完整信息又换了排序参数查了一次结果被判定为“重复调用”而终止。排查后发现根因是我的输入比较逻辑是“全参数精确匹配”但模型两次调用虽然工具名相同查询条件却有意做了变化。这类“变体请求”是正常行为不该被拦截。解决方式是把匹配逻辑升级为“核心参数匹配”在比较前先忽略与主业务无关的参数位如 page、sort 等只看业务关键的查询词是否一致。6.2 兜底终止后用户那边会看到什么很多刚开始做兜底的开发者容易忽略这点兜底触发后直接抛异常或返回空内容给用户体验非常差。用户并不知道后台发生了什么只知道“东西坏了”。正确做法是设计一个优雅兜底响应模板。例如“我尝试了多种方式仍然无法完成该请求。可能的原因是数据源暂时不可用。建议你稍后重试或换个更具体的问法。”兜底响应的核心原则是三条承认失败但不推卸、给出合理的归因说明、提供一个明确的后续动作建议。6.3 兜底方案本身会不会增加成本会但很轻微。重复检测逻辑是本地代码运行不调大模型接口所以成本几乎为零。Prompt 自检指令增加的那几十个 token相比循环烧掉的几万 token完全不是一个量级。不过要注意一点如果把“自检指令”写得过于啰嗦每次正常任务都会多花这部分 token 费用。建议控制在 150 字以内精确表达即可。同时应该用 A/B 测试验证自检指令对正常任务完成率的影响避免过度约束模型行为。6.4 不同 Agent 框架下这些方案如何落地我用过的几个主流框架情况大致如下LangChain可以在AgentExecutor里直接设max_iterations并且自定义early_stopping_method回调。实现重复检测最顺畅。AutoGen通过max_consecutive_auto_reply限制连续回复次数但更灵活的重复检测需要写custom_trigger或代理终止条件函数。Dify / Coze 等低代码平台目前内置的防护比较基础通常只有“最大轮次”设定。想做精细的兜底多半得脱离平台单独写一层服务。如果你在框架之上有自己的执行调度层那最推荐的做法是把兜底检测放在调度层统一实现不依赖具体框架是否支持——这样无论底层 Agent 怎么变化兜底逻辑都稳定生效。6.5 兜底触发后如何事后追溯每次触发兜底一定要记录足够的现场信息否则出问题了根本没法复盘。我的记录字段如下。字段说明示例trigger_type触发的兜底类型hard_timeout/repeat_detected/max_stepstrigger_step触发时已执行的步数7recent_actions最近 5 步的动作摘要search_tool(张三), search_tool(张三)...token_used已消耗的 token32105model_output模型最后输出的文本我需要重新查询...user_input触发任务的原始输入查询张三的订单信息有了这些记录就可以定期分析哪类任务最容易触发循环、是工具设计问题还是 Prompt 问题、是否需要针对性地调整工具返回格式。我的习惯是每周汇总一次把触发频率最高的几种任务专门优化。几轮下来循环触发率能下降一大半。7. 兜底体系的设计原则与扩展方向在多个项目里不断完善这套兜底方案之后我自己总结了几条原则分享给正在设计 Agent 的同行。第一兜底宁可过度不可缺失。一开始的兜底可以设得严格一点多拦掉一些“看似正常”的调用等熟悉了系统的行为模式再逐步放宽限制。这比放任自流、事后看账单懊悔要划算得多。第二优先级永远是硬性护栏 软性监控 工具层优化。软性监控依赖模型的“自觉”但它不是百分百可靠——模型有时会忽略 Prompt 指令。硬性护栏则是代码层面的强制约束无论如何都会生效。如果只做软性监控迟早会在某个意想不到的场景上翻车。第三兜底方案要随着模型升级而调整。不同版本的大模型对重复调用的“自觉性”差异很大。某些模型对 System Prompt 的遵循度高几乎不会出现循环另一些模型则很容易陷入重复尝试。这意味着兜底参数不是一劳永逸的换模型版本后需要重新压测和调参。第四在更复杂的多 Agent 协作场景中兜底需要更全面的设计。如果做过多 Agent 项目一定见过这种场景Agent A 向 Agent B 发消息B 返回结果后 A 认为信息不足再次向 B 发出几乎相同的请求——这种循环不仅烧钱还会陷入不同 Agent 间无限对话的泥潭。这时候除了单 Agent 的步数限制还需要增加“跨 Agent 消息重复检测”和“全局会话级别超时”。一条经验之谈多智能体场景中的兜底比单 Agent 更难架设复杂度是几何级上升的务必留出充足的测试预算。另外提一句关于成本可视化的建议兜底方案做得再好如果线上看不到“每个任务烧了多少钱、多少 token”你始终是“事后看账单”。我现在的做法是为每个任务记录 token 消耗明细并打上“正常 / 预警 / 异常”三级标签。任务结束后如果消耗超出该任务类型的 P90 线自动推送告警到飞书群。这会让你第一时间发现异常流量而不是等月账单出来才拍大腿。8. 最后分享一个小技巧在做循环检测时很多人只关注“工具名是否重复”但实测发现一个更有效的信号模型在循环中产出的“思考文本”往往高度相似。我把每一轮的 reasoning 段落用简单的文本相似度做比对发现相似度超过 0.85 时几乎就意味着模型在原地转圈比检测工具调用更灵敏、更早。实现起来也不复杂用difflib.SequenceMatcher或者sklearn的TfidfVectorizer cosine_similarity都行。把推理文本每轮存下来下一轮开始时和上一轮做对比超过阈值就直接终止执行。这个技巧我在多个 Agent 项目里都用过是拦截循环比较可靠的一招。Agent 的循环调用问题本质上是“模型不确定性 × 系统缺乏约束”造成的成本失控。只要把结构性护栏和软性监控组合用起来大部分风险都能被挡在爆发之前。对于正在做 Agent 项目的朋友我的建议很简单先加上硬性步数和超时控制再把重复检测补上最后把异常日志和告警跑起来。这三步做完你的 Agent 成本安全感会提升不少。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进