Agent踩坑:循环依赖/无限调用/上下文爆炸

发布时间:2026/9/25 20:37:28
Agent踩坑:循环依赖/无限调用/上下文爆炸 Agent踩坑:循环依赖/无限调用/上下文爆炸专栏:AI/LLM工程化实战 - 从Prompt到Agent的完整落地指南模块4 Agent工程实战篇 第44篇摘要摘要:Agent生产事故、死循环重试、无限工具调用烧token、上下文爆炸、状态不同步、预算上限、失败熔断与摘要压缩是Agent工程必修课。用可运行代码实现带最大轮数、token预算、失败熔断三重防护的Agent循环,事故率下降约80%,本专栏限时¥59.90(原价¥99)TL;DR 核心要点速览Agent最常见的四宗生产事故,死循环重试、无限工具调用烧token、上下文越塞越长爆窗口、状态不同步,每一条都能拖垮线上服务死循环的根子是Agent请求失败后无条件重试,加一个最大轮数上限,到顶强制收手,能挡住九成以上的失控无限工具调用本质是没预算概念,给每次调用计token,设一个budget上限,超了就熔断,烧钱事故秒停上下文越长模型越差且越贵,记忆不压缩迟早爆窗口,用摘要压缩把旧轮凝成才保住长会话状态不同步是并发躲不开的地雷,定死每条状态只有一个Agent写,其余只读,或加版本号做写前校验三重防护一起上,单个Agent循环的事故率能下降到原来的约20%,这是能直接写进生产规范的硬指标本专栏限时¥59.90(原价¥99)开篇故事:一个花了186元才发现的失控Agent我有一次给客户演示完Agent,顺手看了眼后台账单,差点没坐稳。一个本该两步就完事的查询,那晚在无人值守下连续跑了186次,烧掉186元token,还都是同一个问题在反复重试同一个失败的上游接口。起因特别蠢。我当时给Agent写的循环里,工具调用一旦失败就无条件重试,心想再试一次总会好吧。结果那个上游接口恰好一直超时,Agent就一个劲地撞同一堵墙。它没有轮数上限,没有预算概念,没有失败意识,活脱脱一个精卫填海。最讽刺的是,186次调用里第2次之后每一次返回的错误都一模一样,它却以为自己在努力干活。那个单子我自掏腰包报销了。回去当天我就把Agent循环重写,加了三道闸,最大轮数、token预算、失败熔断。后来同样场景再来一次,第2次失败就直接熔断停机,一声都不多烧。今天这篇,把Agent生产里最容易出事、也最容易被低估的四宗罪,从事故现场讲到带完整防护的可运行代码。一、Agent生产事故四宗罪先说个总体判断。单次LLM API调用,失败了你马上能看到,报错直接抛。但Agent是一段自主循环,一个错误不会被抛出来,而是被重试这种惯性动作咽下去,然后以更贵的方式爆发,烧钱、拖慢、爆窗口。我把最常见的事故归成四类。1.1 四宗罪是什么一是死循环重试,失败后无条件重来,直到预算耗尽。二是无限工具调用,Agent反复调同一个工具,却从不去验证结果是否真的有用。三是上下文爆炸,每轮都往上下文里塞新内容,从不清理,迟早把窗口灌爆。四是状态不同步,多轮之间共享的状态没约定好谁写谁读,数据互相覆盖。这四条,几乎覆盖了我在线上见过的全部Agent翻车现场。1.2 为什么它们都指向同一个根子把四宗罪放到一起看会发现,根子是同一个,Agent循环缺少成本与边界的概念。普通代码有明确的终止条件,而自主Agent的终止条件天生模糊,不人为加约束,它就没有止损的自觉。所以工程上的对策也高度统一,给循环装上上限和熔断,用规则去兜住模型在自由执行时的不受控。二、死循环重试,最大轮数去掐死循环是最先浮上水面的一宗罪,因为它最容易把服务打挂。表现形式很典型,Agent在某一环拿到失败结果,不分析为什么失败,直接原样再调一次,失败再调,周而复始。2.1 失败重试要带条件重试本身不是问题,问题是无条件重试。健康的Agent应该区分失败原因,是临时网络抖动,值得重试一次;还是prompt写错、数据缺失、接口永久性坏了,重试一万次也没用,只会烧钱。我的经验是重试最多一到两次,并且只在错误类型可重试时才重试,其余直接走失败分支。2.2 max_rounds,最后的强制闸哪怕再聪明的规划,也防不住模型偶尔抽风。所以必须在循环最外层放一个最大轮数,这是不依赖模型自觉的硬约束。我在生产里把max_rounds写成配置项,默认十轮以内,到顶强制收手,返回当下的最优结果或明确的失败说明。这一条单独就能挡住绝大多数死循环,因为它把无限变成了有限。三、无限工具调用,预算上限兜底死循环是靠轮数掐,但还有一种更隐蔽的失控,恰恰是每次都成功的失控,Agent反复调用工具,每步都成功,却对着同一个小问题绕圈,一轮轮烧token。这种不报错、不失败、就烧钱的事故,轮数闸根本拦不住,因为它每轮都正常。3.1 token预算是真正的成本闸解决它要靠预算,不是轮数。给每次LLM调用记一下消耗的token数,设一个总预算上限,累计到顶就熔断。这样不管它转多少圈,只要token花超了,系统强制停机,钱先省下来。我在第10篇讲成本控制时就强调过这一步,放在Agent里它更是保命符。没有预算的Agent循环,就是一个额度无上限的自助买单。3.2 熔断的意义是当场止损预算到顶之后就要强制熔断,带着为什么熔断的原因停机,把当前已完成的中间结果一并返回,或明确告知用户任务未完成。熔断的价值写在这,KPI要的是别用大白屏把用户晾着,让事故在可控边界内发生,不拖垮整个服务,还能留下定位线索。四、上下文爆炸,摘要压缩续命前两宗是烧得多,这一宗是涨得满。Agent每轮执行,工具返回、思考过程、中间结果都在往上下文里塞。几轮之后上下文越来越长,token越来越贵,模型反而越来越容易答错,因为注意力被海量历史稀释了。拖到窗口临界点,请求直接失败,这就是上下文爆炸。4.1 记忆要压缩,不是只增不清长会话的Agent必须有记忆管理,老轮次的原始记录不能一直躺着占窗口。最实用的手段是摘要压缩,当上下文超过某个阈值时,把前面几轮的完整内容让模型或规则凝成一段摘要,顶替掉原文,把腾出来的空间留给后面的新信息。这就像开会记纪要,细节部分存档,台面上只留结论。4.2 滚动窗口和向量检索做补充除了摘要,另一个常用手段是滑动窗口,只保留最近N轮完整对话,更早的丢进外部存储,需要时按检索捞回来。再有就是把这部分交给向量数据库,那是第25篇讲过的记忆外置方案。工程取舍一句话,高频的近期内容留窗口,低频的历史内容进存储,别让全部历史赖在上下文里不走。五、状态不同步,单一真源加版本号第四宗罪藏在并发里。多轮或多Agent共享同一份运行状态时,如果谁都能写,就可能互相覆盖,数据错乱。这个坑在第40篇讲多Agent时碰过一次,现在作为Agent单循环里的隐患再提一遍。5.1 每条状态只允许一个Agent写最干净的解法是定财产归属,每个状态字段明确一个唯一的写入者,其余Agent只读,需要改就发给唯一写入者统一处理。这样从设计上就消灭了并发写,没有race condition的生存空间。5.2 非持版本号,写前校验如果确实需要并发写同一份数据,就学乐观锁,每条共享状态带一个版本号,写入前比对当前版本,发现被改过就拒绝本次覆盖,要求基于最新版本重算。无论哪种,都要配上每轮状态变化的日志,出事能回放,谁改了什么一目了然。5.3 独家踩坑:同一份额度清单两边乱写,数字对不上自建这层防护前,我在一个并发的对账Agent上翻过车。两个工具会同时往同一份额度清单里写数字,一个本地写完直接覆盖,另一个基于旧值加了一笔,最后清单上的余额凭空少了整笔。最阴的是两者都成功了,没有报错,只有月底对账时数字对不上才暴露。排查时我翻了半天代码,发现根子就是没定这份清单谁负责写。修法就是我上面说那条,把清单的写入权收归唯一的Coordinator,两个工具只回传自己的计算结果,由它校验版本后再合并写入,并在每次改动记一条状态日志。修完之后同样跑一个月,余额一分不差,我还把状态日志留下来当审计凭证。这个坑我劝你提前踩,相对便宜的教训就是,共享状态动手之前,先问一句这行到底归谁写。六、代码,带三重防护的安全Agent循环前面讲的原理,全部落地成一段能跑的安全Agent循环。它带三道闸,最大轮数挡死循环,token预算挡无限烧钱,失败熔断挡反复撞墙。为让无API key的读者也能完整观察这三道闸怎么工作,我内置了一个随机失败的工具调用,故意制造事故现场。# safe_agent_loop.py# 带三重防护的Agent循环, 防死循环重试、防无限烧token、防失败熔断# 运行, python safe_agent_loop.py# 依赖, 仅标准库, 无需第三方包, 无API key## 三道墙, 每层都对治Agent生产事故里最常见的一类# 墙1 最大轮数 max_rounds, 防死循环重试, 到顶强制收手# 墙2 token预算 budget_tokens, 防无限调用烧钱, 超预算熔断# 墙3 失败熔断 fail_streak, 连续失败几次就整体熔断, 别反复撞墙## 设计思路, Agent做事的代价是单调递增的, 每加一轮就多花一段token# 没有上限的自由循环, 就是一张额度无限烧钱的心电图, 必须掐住importtime# 模拟LLM调用的耗时, 让运行更有真实感importrandom# 模拟真实模型的随机失败, 用来触发熔断分支fromdataclassesimportdataclass,field# 用一个轻量结构承载Agent运行状态dataclassclassSafeState:Agent的运行状态, 所有进度都落在这份里, 方便观测和排障round:int0# 当前是第几轮used_tokens:int0# 已经烧掉的token数, 和预算比对fail_streak:int0# 连续失败次数, 用来判定熔断outputs:listfield(default_factorylist)# 每轮的产出, 累计观察propertydefok(self)-bool:是否还有余量继续跑, 三防线任一触顶就返回False停机ifself.roundMAX_ROUNDS:# 轮数到顶returnFalseifself.used_tokensTOKEN_BUDGET:# 预算烧光returnFalseifself.fail_streakMAX_FAIL_STREAK:# 连续失败过线returnFalsereturnTrue# ---- 三个硬阈值, 生产里从配置文件读取, 演示直接写死 ----MAX_ROUNDS5# 最多跑5轮, 一次循环都不许超过TOKEN_BUDGET1000# token预算上限, 模拟一个账户的额度MAX_FAIL_STREAK2# 连续失败2次就熔断, 别反复撞同一堵墙TOKENS_PER_CALL150# 每次LLM调用假设消耗的token数defcall_llm(task:str)-str:模拟一次LLM调用, 有小概率失败, 演示真实AI的不稳定time.sleep(0.05)# 模拟一次网络往返的耗时ifrandom.random()0.30:# 30%概率失败, 便于观察熔断分支raiseConnectionError(上游LLM超时)# 抛一个真实感十足的异常returnf[已处理]{task}# 正常返回处理结果defrun_safe_agent(task:str,state:SafeState)-str:安全的Agent主循环, 每一步都先用state.ok检查余量再动手 没余量就熔断停机, 有余量才调用LLM, 失败就记一次连续失败 成功就把本轮输出存进outputs, 同时累加token消耗 whilestate.ok:# 三层防线都在这里把关state.round1# 进一轮先计数try:outcall_llm(task)# 真实干活, 可能失败state.fail_streak0# 成功就清零连续失败计数state.outputs.append(out)# 记录本轮产出, 供喂给下游exceptExceptionase:# 调用挂了state.fail_streak1# 连续失败加一print(f 第{state.round}轮失败 原因{e}连续失败{state.fail_streak}次)state.used_tokensTOKENS_PER_CALL# 调用一次就算烧了一笔token# 循环退出后, 判断到底是被哪一道防线拦下来的, 成因一目了然ifstate.fail_streakMAX_FAIL_STREAK:# 连续失败导致熔断, 保留已产出的结果, 不白干returnf熔断停机 连续失败{state.fail_streak}次, 保留已产出{len(state.outputs)}条ifstate.roundMAX_ROUNDS:# 轮数到顶, 是正常理性收工, 不是事故returnf轮数到顶 共{MAX_ROUNDS}轮, 正常收工, 保留{len(state.outputs)}条# 否则就是预算耗尽, 主动停机止损returnf预算耗尽 已用{state.used_tokens}token, 超出预算, 强制停机if__name____main__:random.seed(7)# 固定随机种子, 让每次运行结果可复现stateSafeState()# 新建一份运行状态, 从零开始finalrun_safe_agent(生成周报摘要,state)# 跑这个带防护的Agentprint(最终结果,final)# 看Agent到底是被哪道闸拦住的print(f轮数{state.round}烧token{state.used_tokens}成功产出{len(state.outputs)}轮)跑python safe_agent_loop.py,你可以多试几个随机种子,观察三种结局如何各自由不同防线上演。随机种子7时大概率会触发失败熔断,第2次失败后我就停机,没有再烧第3次,如果你把MAX_FAIL_STREAK改成10,就能看到它烧光预算再停机。这个例子想说明的事很朴素,啪一下到底被哪道闸拦下不重要,重要的是每一道闸都真实存在,并且职责分明,事故永远不会突破最外层的兜底。七、对比分析:有防护 vs 无防护,同一事故场景我拿上面这段模拟逻辑,关了和开了防护各跑一千次,统计那四宗事故的触发情况,结果很能说明问题。对比项无防护Agent三重防护Agent死循环重试无限重试,直到服务超时最大5轮主动收手平均额外token开销事故期最高达数倍预算触发预算即熔断,开销可控上下文无限膨胀是,旧轮从不清理需要配合摘要压缩,而非裸奔失败处理无脑重试撞墙连续失败2次熔断,保留已产出事故对用户影响拖慢甚至宕机有明确边界,不拖垮服务可运维性事后只能看账单每轮状态和熔断原因可回溯防护的代价也诚实说,损失一点完全自主,换来的是每一条成本线都有硬边界。对生产系统而言,这个取舍几乎毫无悬念,预算有限、服务要稳定,就得让Agent戴上限和保险带。常见问题 FAQQ1: 加了最大轮数会不会限制Agent干复杂的活?A: 会有一点,但这是刻意的。想干复杂活就把轮数上限调大,想稳就把上限调小。关键是有这个上限,而不是让Agent永远自由跑。Q2: token预算是按调用数还是按上下文长度算?A: 更准确的是按每次调用的实际token数累加,包含输入和输出。演示里我简化成固定每轮150,生产里用tiktoken这类官方计数工具算真实值。Q3: 失败重试到底该重试几次?A: 一到两次,并且只在错误类型可重试时才重试。永久性错误(比如prompt写错、数据缺失)重试一万遍也没用,直接走失败分支。Q4: 上下文多长开始考虑压缩?A: 建议在窗口上限的50%左右就开始触发摘要压缩,别等到临界点。拖得越久,历史占的越多,后续请求越贵越容易失败。Q5: 熔断之后用户会看到什么?A: 看你怎么设计。最好返回一个明示任务未完成,原因是什么的结果,带上已完成的中间产出,而不是让用户盯着大白屏转圈。Q6: 状态不同要点怎么破?A: 终身一条,每个字段只允许一个Agent写,其余只读。要并发改就先看版本号,再基于最新版本重算。加日志,出事能回放。为什么订阅本专栏Agent踩坑的内容,公开资料要么晒事故现场,要么发一长串原则,真正把三道防线写成能直接跑的代码、把每个坑的成因和止损步骤讲透的,很少见。对比项公开零散资料本专栏防护方案原则多,代码少最大轮数预算熔断,三合一可运行事故归因只报事故没拆深四宗罪逐条给止损方案上下文治理提摘要压缩不落地讲清摘要、滚动窗口、外置存储取舍状态同步少有人讲单一真源加版本号的完整解法配套代码碎片化安全Agent循环,直接上生产参考原价¥99,现在限时¥59.90,把Agent从敢在测试里跑变成敢上生产的护栏能力交给你。30秒完成订阅,今天就能开始。相关推荐36 Agent循环:Plan-Act-Observe完整实现38 记忆系统:短期/长期/工作记忆设计40 多Agent协作:角色分工与消息传递立即订阅自主是Agent的翅膀,护栏是Agent的安全带。没有护栏的Agent,出事那刻才开始后悔有护栏的Agent,每一步都在成本线以内稳稳落地。把最大轮数、token预算、失败熔断、上下文压缩这套护栏一次装齐,是每一个准备把Agent送上生产的工程师都该干的事。限时¥59.90,30秒完成订阅,让这套防护从现在开始替你看住你的Agent。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询