☰
大模型Agent死循环:根因、监控与工程终止方案全解析
2026/10/19 2:44:02 网站建设 项目流程

1. 先搞明白:Agent死循环到底是怎么发生的

做Agent开发的朋友,多少都遇到过这种场景:凌晨两点,你正准备收工,运维群里突然炸锅——某条线上Agent任务跑了三个小时还没结束,日志里刷屏的是同一句话。你点开面板一看,好家伙,这个Agent正在第47次调用同一个工具,甚至调用的参数都一样,唯一不同的是报错信息后面多了一串乱码。跑着跑着就死循环了,这不是偶发现象,而是所有非确定性系统里最经典的故障之一。

先说清楚一个概念。这里说的Agent,不是聊天机器人那样"你问一句它答一句",而是指具备多步骤自主决策能力的智能体:它能自己拆解任务、选择工具、执行操作、观察结果,再根据结果决定下一步。现在主流的实现都基于大语言模型,靠模型在每一个决策点生成下一步动作。问题恰恰出在这个"自主"上:模型本身是无状态的,它不记得自己上一步干了什么,也不知道自己已经重复多少遍了。每一步它都只是根据当前输入生成一个看似合理的输出,如果这个输出又把环境状态推回了上一个相似的位置,循环就产生了。

1.1 一次典型死循环的完整过程

我举个例子,模拟项目X里有个客服Agent,任务是帮用户催发货。

第一步,它去查订单系统,结果说订单号不存在。正常的逻辑应该去问用户要新订单号,但模型没有这个意识,它觉得"查不到就再查一次,可能是系统延迟"。于是第二步,它又调了同一个查询工具,传的还是那个不存在的订单号,结果当然还是查不到。第三步,模型在这个上下文里发现"今天系统确实经常延迟,我再刷新一次"——注意,模型已经给自己编了一个合理的解释,所以它又调了一次。第四步、第五步、第六步,全是一样的操作。真实日志里最夸张的案例,某个Agent在13分钟内把同一个只读接口调了300多次,直到触发我们预设的硬性次数上限才停下来,令牌损耗接近一个中型知识库的全文长度。

这类死循环有个统一特征:每一步单看都是合理的,连起来看就是荒谬的。模型没有全局视角,它只能看到当前窗口里的几百个token,这些token里可能包含"查询失败""可能延迟""再试一次"这样的信号,它就会顺着这个信号往下走,完全不知道这份日志已经是第10次重复了。

1.2 死循环的三层根因

真正死循环的根因,我拆成三层来看:

第一层是意图保留失败。任务拆解后,每一步执行完,Agent需要把"当前进度、剩余目标、已尝试方案"传递到下一步。现在大多数Agent框架会用一个大字符串把所有历史消息拼在一起喂给模型,但模型对超长上下文的注意力是稀疏的,它会漏掉关键约束,比如"客户强调加急"或者"这个接口已经试过3次了"。约束一丢,上下文就变成了一个无向图,模型随便选一条边就往下走,走回来也不知道。

第二层是工具反馈不可判别。工具返回的结果里缺少结构化信息,比如"查询失败"和"系统繁忙"返回的都是同一段文本,或者错误码都是500。模型无从判断这次失败到底是环境问题还是参数问题,只能盲目重试。试想一下,如果工具每次都返回一个状态码码表,告诉你这是参数错还是权限错,Agent就能在"参数错"时立刻转向澄清参数,而不是重试一百遍。

第三层是终止条件缺失。很多Agent框架默认给的循环上限要么没有,要么给一个很大的数。30次、50次看起来很够用,但一次死循环在极端情况下可能只需要900个token就能完成一圈,50次也就是几万个token的事——大模型几分钟就能烧完。如果没有硬超时和重复检测机制,这个循环就会一直跑到预算干枯为止。

这三层根因对应三种解法:约束注入、反馈标准化、工程兜底。后面的篇幅我会一个一个展开。

2. 最常见的循环诱因:工具调用与上下文失控

死循环不是抽象概念,它有非常具体的触发场景。我排查过的案例里,90%以上集中在两类诱因:工具调用失败后的盲目重试,以及上下文增长导致的意图漂移。这两个问题单独看都不严重,组合在一起就是恶性循环。

2.1 工具调用失败后的"重试陷阱"

先说工具调用。很多Agent框架会在系统提示词里写一句"如果工具调用失败,可以重试获取最新数据"。出发点是好的,因为线上确实有瞬时故障,重试一次可能有结果。但问题在于,这句话没有一个"最大重试次数"的边界,模型会把"可以重试"理解成"应该无限重试"。

我见过一个更隐蔽的陷阱:工具本身的幂等性问题。比如一个"生成优惠券"的工具,第一次调用其实已经成功了,只是返回响应超时,Agent以为失败了,又调了一次,结果生成了两张券。这种情况从Agent视角看不构成循环,但从业务视角看,它造成了重复操作。我们需要给工具调用加两层防护:外层是次数限制,内层是幂等键。次数限制好理解,关键是幂等键——调用工具时生成一个唯一的请求ID,工具侧缓存这个ID,如果重复出现就直接返回上次结果,而不是再执行一次业务逻辑。这样一来,即使Agent发了疯一样连调十次,真正落库的也只有一次。

工具反馈这块还有一个容易被忽略的细节:返回内容的长度。某个金融问答Agent调用行情接口,接口返回的JSON里嵌套了30天K线数据,动辄十几KB。这些数据全部塞进上下文后,会冲淡前面已经产生的推理过程。模型在下一轮生成时,注意力全被最新一段行情数据吸引,就开始围绕"要不要再拉一次行情"打转。我的建议是,工具返回的内容一定要做裁剪:只保留任务当前需要的摘要字段,历史数据压缩成指标值,能不进上下文的就不进。

2.2 上下文越长,意图越容易漂移

上下文失控是另一个重灾区。我这里说的上下文,包括系统提示词里的任务目标,也包括整个对话历史。很多Agent框架为了让模型"记住"更多信息,倾向于把每轮工具调用、思考过程、中间结果全部拼进上下文。随着运行轮数增加,上下文越来越长,模型对早期目标的理解就会衰减。

你可以做个简单实验:把一段包含目标指令和20轮历史对话的文本喂给模型,然后问它最初的目标是什么。20轮以内基本能答对,超过30轮就开始丢三落四,超过50轮就直接开始编造了。这不是模型变笨了,而是注意力分配本身就有极限。死循环正是钻了这个空子:当Agent遗忘初始目标时,它就会把最近一次失败当作新的目标,接着就是"查一下→失败→再查→再失败"的无限循环。

解决思路是状态外置,而不是把所有信息都塞进对话历史。具体做法是把任务拆成三步:初始请求、当前进度、下一步计划。初始请求保持在系统提示词里不动,当前进度用一个结构化字段动态更新,下一步计划只保留未来2-3步的操作。模型每次生成时看到的不是流水账,而是一张清晰的"待办清单",这样就算上下文再长,核心目标也不会丢。

我在实际项目里喜欢给Agent的上下文加一个"重复预警段"。这个段是动态生成的,内容大致是:[Task Memory] 当前已执行步骤:1.查询订单 2.重新查询订单 3.再次查询订单。已尝试次数:3。与第1次操作完全重复。建议转向:询问用户输入正确的订单号,或检查系统状态。这段文字放在上下文末尾,模型就能重新聚焦。效果非常明显,某些场景下重复调用率能降80%。

3. 如何提前发现死循环:监控与定位手段

死循环最气人的一点是:它往往不是一上来就循环的,而是前几步还正常,中间某一步开始拐进死胡同。这种情况下,如果等到用户投诉才发现问题,Agent已经烧掉不少预算了。所以监控和定位手段必须前置。

3.1 事后"考古":回放Log找重复

先说事后排查。我说的"事后"不是等用户投诉,而是当Agent运行时,以每次运行单元为单位,把完整的决策轨迹记录下来。一个运行单元,就是一次用户请求从进入到结束的全过程。记录的内容包括:时间戳、当前步骤序号、模型输入的长度、工具名称、工具参数、工具返回的状态、模型输出的动作类型。

有了这个日志结构,回放就很容易。我判断死循环有一套简单粗暴但有效的算法:轨迹指纹对比。对每一步生成一个指纹,比如工具名 + 参数哈希 + 返回状态码,然后在轨迹里搜索重复子序列。如果发现同一指纹连续出现3次以上,就判定为疑似循环。这个方法不需要模型参与,纯字符串匹配,线上跑起来开销极小。

伪代码如下:

def detect_loop(trace, min_repeat=3): seen = set() for i, step in enumerate(trace): fingerprint = hash((step.tool_name, step.args_snapshot)) if fingerprint in seen: # 统计从当前位置开始的连续重复次数 repeat = 0 for j in range(i, len(trace)): if hash((trace[j].tool_name, trace[j].args_snapshot)) == fingerprint: repeat += 1 else: break if repeat >= min_repeat: return i, repeat seen.add(fingerprint) return None

这段代码是我在某个创新项目的Agent运行器里实际用过的,效果还不错。它只能检测"完全重复",如果Agent每次换了个不存在的订单号继续查,指纹就不一样了,这种属于"语义循环",需要下一层的语义判断才能识别。

语义循环可以用一个更轻的办法兜底:统计最近5步里工具调用的主题分布。如果5步里有4步在调用同一类工具(比如"查询库存"),并且返回结果都没有对任务进度产生实质推进,就值得人工看一眼了。实质推进怎么判断?可以简单定义为"工具返回结果导致了下游分支变化",这个信息很多日志系统都能拟合出来。

3.2 实时告警:怎么判断Agent"卡住"了

事后定位够了之后,接下来是实时告警。我现在看监控指标,重点不是看单步耗时,而是看几个组合指标:

第一个是单位时间内的工具调用频率。正常情况下,一个多步骤Agent每5-10秒调一次工具是合理的。如果单分钟调用次数超过20次,或者连续3分钟每分钟都超过10次,基本可以断定进入循环了。第二个是连续相同动作占比。最近10步里,动作类型(不是参数)重复率超过60%,说明有机械重复的风险,即使不是完全相同的循环,也离死循环不远了。第三个是无效操作比。工具调用里有超过30%返回的是"数据未变化""查询无结果""操作失败"这类非成功状态,且没有触发Agent转向,就需要告警了。

这些指标的阈值不能设得太激进。我一开始设的告警阈值是"连续3步相同",结果线上大量误报,因为有些任务的前置检查本来就是重复做三次。后来调成"5步内有4步相同、且返回状态相同",误报率才降下来。告警之后怎么办?我的经验是:不要自动杀掉Agent,而是把一个人工确认指令插进当前运行管线里。因为线上Agent很多都在处理真实业务,突然强杀可能导致事务不一致,让人看一眼再决定更稳妥。

还有一种做法是请求级超时熔断。在Agent运行器的编排层配置一个整体超时时间,比如单次运行最长30分钟。快到时间了直接终止,并且把当前轨迹存档,供后续复盘。这个参数的确定,一定要结合业务场景调。我见过一个数据采集Agent,正常执行一次要25分钟,如果硬设30分钟,它偶尔正常跑完但统计失败率会变得异常高。所以超时时间要给正常用时的1.5-2倍才合理。

4. 实战拆解:三种场景下的终止方案

分析做完,接下来就是动手解决问题。所有终止方案里,我的核心原则只有一个:Engineer for failure。不要假设模型每次都聪明到知道何时停止,而是在架构层面允许它失败但不至于失控。

4.1 兜底:max_iterations与硬超时

最基础也最不能少的,是迭代次数上限和硬超时。这块看起来简单,真正做得严谨的不多。

我推荐的参数设计是分层的,而不是拍脑袋给一个总数。第一层是单类型操作上限:同一工具连续调用不得超过5次。这个用于拦截"查询同一个订单号查了10遍"这种机械重复。第二层是全局步数上限:整个任务流程最多运行30步,到了直接收敛。第三层是时间硬上限:任务的wall time超过45分钟,立刻熔断。

每一层触发后的动作也不一样。单类型操作上限触发时,不是直接终止,而是强制Agent进入"澄清模式"——要求它先向用户确认意图或者换一个工具。全局步数上限触发时,进入"收尾模式",不再允许调用工具,只允许模型基于已有信息生成最终答复。时间硬上限触发时,直接终止并告警,交由人工接管。

这套分层逻辑的好处是,模型多了一个"改错"的机会,而不是被一刀切干掉。我在几个项目里看到过,强制进入澄清模式后,Agent反而找回了最初目标,后面的流程变得正常了。

4.2 循环识别与语义回溯

次数上限是保险丝,真正能让Agent"聪明地走出来"的,是循环识别机制。

循环识别不只限于前面说的指纹对比,还可以做一个轻量级的语义回溯:在上下文的末尾附上最近N步的执行摘要,然后给模型一次"回顾"的机会。具体做法是,当工具调用失败且下一步是重复动作时,注入一段提示:

注意:以下动作与第<step>步完全相同,且未获得新的有效信息。 请判断是否应该继续重复该操作;如果不应重复,请选择一个完全不同的操作。 要求:直接回答 "continue" 或 "switch" 并说明理由。

这个注入不复杂,但效果出奇好,因为模型虽然不记得历史,但你提醒它"回头看"时它还是能对齐上下文与当前状态的。我实测的效果是,约60%的机械重复能在第3次调用前被拦截下来,剩下的还是靠次数上限截断。

另外一种语义回溯方法是摘要压缩。每隔5步,用模型对话历史里已产生的关键决策点总结成2-3句话,替代原始历史加入上下文。这就相当于让Agent每5步"回看一次全局",避免前期目标在长上下文中被淹没。我做过对比实验:不压缩的历史中,第20步时模型对初始目标的还原度显著下降;压缩之后,即使到第40步,还原度仍能保持在初始水平。

4.3 状态机与呼吸灯式等待

还有一个容易被忽视的循环诱因:Agent调了一个需要异步执行的工具,工具返回了PENDING状态,Agent却把它当成"失败"来重试。比如它请求生成一个报告,服务端返回"任务已提交,预计5分钟后完成",模型看不懂这个状态,以为是报错,于是又提交了一遍。大量重复就是这样发生的。

解决思路是给Agent设计一个等待状态。工具返回PENDING时,不是让模型立刻决定下一步,而是显式告诉模型:"当前任务处于等待状态,不需要额外操作,请返回等待指令。"在代码层面,工具调用结果里增加一个status字段,取值为pending、success、failed,Agent框架根据status决定如何进入下一步,而不是全交给模型自由发挥。

这里我加了一个"呼吸灯"机制,就是当Agent进入等待状态时,框架会在上下文中持续注入一个静态文本"等待结果中,请勿重复调用",并且以每分钟一次的频率拉取异步结果。一旦结果就绪,静态文本被替换成真实结果,模型立即恢复推理。这个机制让我负责的某调度类Agent重复调用率降了很多,因为大部分"循环"根本不是模型不想停,而是它不知道自己在等什么。

5. 绕开弯路:设计阶段就该做对的几件事

前面聊的都是运行时怎么防循环。说实话,等Agent真跑起来再抢救,已经算是事后诸葛了。真正的高手是在设计阶段就把循环的滋生土壤掐掉,让Agent不容易走进死胡同。这部分我想聊几件在系统设计上容易做错的几件事。

5.1 给Agent一块"情绪面板":请求级状态注入

我刚开始做Agent的时候,和大多数人一样,所有状态都靠模型从上下文里自行推断。后来发现有一个巨大的问题:模型对"自己已经试过某方案但没有效果"这件事缺乏感知。它只有"我下一步要做什么",没有"我目前处于什么状态"。

后来我给Agent的运行时加了一块"状态面板",也叫请求级元状态。这块面板不参与模型业务推理,但它会以固定格式显示在系统提示词末尾:

[Current State] - Step: 8 / 30 - Last Action: query_order(order_id=12345) - Last Result: not_found - Repeated Action Count for query_order: 3 - Remaining Tools: verify_user, ask_user_for_input, escalate

这样做有几个好处。第一,模型不需要从漫长的历史里"回忆起"自己干了什么,状态面板替它记好了。第二,模型能看到"重复次数"这个数字,数字一旦超过阈值,模型自己就能意识到"这条路走不通了"。第三,面板里列出了剩余的可选操作,相当于给模型划了个圈,它不会天马行空地乱转。

这个"状态面板"的灵感来自一个很朴素的经验:人在迷路时,不是靠回忆走过哪条路走出来的,而是靠"我现在在哪里、还有哪些路可以走"这一张地图走出来的。给Agent一张地图,比让它自己画地图靠谱得多。

落地的时候有一点要特别注意:状态面板里的内容必须是结构化生成的,而不是模型自己生成的。不然模型会在状态面板里撒谎,例如明明已经重复调了5次,它却写"已尝试次数:0",那就麻了。

5.2 让工具自己说"再做一次也不会更好"

工具定义是设计阶段真正拉开差距的地方。同样的查询工具,一种定义返回"查询成功,订单状态为已发货",另一种返回"查询成功,订单状态为已发货。提示:该订单状态稳定,重复查询不会获得新信息,请勿重复调用"。后者就好得多。

这个方案不只用在查询场景。凡是Agent可能反复调用的工具,我都建议在返回结果里加一个terminal_hint字段。这个字段表示:当前结果是否已经达到最终状态,重复调用是否可能产生新结果。对"查余额""查时间""查配置"这类幂等工具,terminal_hint固定为true;对"异步任务状态查询"这类工具,terminal_hint由服务端判断。

我有段时间懒得写这些提示,觉得模型能自己理解。后来在排查一个死循环案例时发现,模型把所有返回结果都当作"中间状态",于是它每隔几秒就重新查一次,直到压垮下游服务。从那以后我立了个规矩:工具返回必须明确告诉模型"你再调用我多少次,我还是这个答案",把这个作为工具开发的标准要求写进了团队的规范文档。

5.3 协议上的护栏:response_format与终止标记

最后是协议层。我把Agent的每一步输出都规范成结构化JSON,其中包含一个should_continue字段,它的值只能是continue或terminate。当模型判断任务已完成、无法完成、或需要用户介入时,它必须显式输出terminate。

这个设计看起来简单,但它把"要不要结束"从模型的潜意识里抽出来,变成了一个显式的决策点。模型在生成最后一步时,就必须思考"当前是否达成了目标",而不是稀里糊涂地再生成下一个tool_call。我在某个模拟项目里做了一组对照实验:加了should_continue字段后,Agent的平均运行步数缩减了近40%,原因是模型比人类想象的要懒——只要给它一个体面的"退出按钮",它会更倾向于在合适的时候退出,而不是硬撑着继续干。

另一个协议上的护栏是输出格式强约束。我见过不少死循环是模型生成的JSON格式崩坏导致的:比如括号不匹配,然后解析器抛错,框架自动重试,模型再生成一遍同样的格式错误,形成了"解析器—模型"层面的循环。解决方法是提示词里给JSON Schema示例,同时在解析失败时,不是重试原输出,而是把这条错误输出注入上下文提醒模型"你刚才的格式不符合要求,原因是什么"。这样一来,模型下次生成的正确率会显著提升。

6. 常见问题速查与个人踩坑记录

到这一个章节,我把平时排查Agent死循环时常遇到的情形整理成了一张速查表和一些个人心得。表格不是万能的,但拿着它能少走不少弯路。

6.1 Agent死循环排查速查表

症状可能根因快速验证方式对应解法
同工具连续高频调用工具反馈无区分度/失败码相同查看相邻步骤的参数哈希与返回码工具返回增加状态码分类 + 单工具调用上限
上下文越长,越频繁重复模型注意力衰减导致目标遗忘在第20、40步分别询问模型任务目标摘要压缩 + 状态外置 + 状态面板
模型不断调用异步查询工具PENDING状态被误判为失败查看工具返回里是否有pending关键字等待状态机 + 呼吸灯机制
模型反复生成解析失败输出JSON格式崩坏/解析器重试查看日志中parse error出现的频率输出Schema强约束 + 解析失败信息注入
模型卡在某一步无法前进可用操作列表未显式暴露检查状态面板是否有剩余工具清单在提示词中保留可选项列表
任务快完成时突然跑偏结论已生成但未显式声明终止查看最后几步是否仍输出tool_call增加should_continue显式标记

这张表我贴在了团队的告警墙上,每次遇到新循环问题就对照一下。它的好处是让排查从"凭感觉"变成了"按套路走",效率至少提升一半。

6.2 避坑清单:五种最像"真理"的错觉

最后分享几个我踩过的坑,每一条背后都有一个真实的加班夜晚。

第一坑:"模型不会傻到重复调用同样的失败接口"。它真的会。模型的每一步都只基于当前上下文做判断,它没有记忆,也没有羞耻感。你不给它护栏,它就敢用同一个错误参数永远试下去。所以我现在的原则是:凡是能用工程手段挡住的,绝不指望模型自觉。

第二坑:"把max_iterations设大一点,任务就能跑完"。恰恰相反。设太大只会让它把预算烧在循环里。我现在的做法是把上限设成"理论上限的1.2倍加一点缓冲",然后把多余预算留给重试机制,而不是给模型无限发挥。

第三坑:"死循环只是偶发问题,不用专门设计"。概率比你想的高。一旦Agent的决策空间足够大、工具足够多,循环出现的可能性按指数级增长。我见过一个配置了6个工具的Agent,在24小时的压测里出现了17次循环。不事先设计好终止机制,就是在给自己埋雷。

第四坑:"给模型更强一点的推理能力,它就能自己走出来"。推理能力解决的是"能不能想明白",循环问题解决的是"有没有退出机制"。你给Agent配上再强的模型,它想明白了也可能收不住手。就像一个分析能力很强的人,面对一个没有尽头的任务,他也能不眠不休地干下去,直到累倒。

第五坑:"循环检测就是查重复,重复了就终止"。别只做完全重复检测。Agent更常见的是"每次换一个参数重复同一类操作",这种语义层面的循环,靠字符串匹配根本查不出来。一定要配合状态面板里的重复次数计数和工具类型占比分析一起用。

写到这里,有个小技巧再分享一次:我现在所有的Agent项目,上线前必做一次"疲倦测试"。人为设计一个总是返回同一错误的假工具,让Agent跑5轮,观察它会不会在循环中游荡。大多数循环问题都在这个测试里现了原形。这个测试成本极低,但价值远超我在任何文档里看到过的模型评估指标。如果你们的Agent系统还没做过类似测试,建议尽快补上,能救不少周末。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询