Agent 工具循环调用的解决办法目录循环回顾为什么会陷入循环调用三种检测手段三种阻断策略和其他机制的配合更好的做法小结最近笔者在做自己的Agent项目就遇到了工具循环调用的问题。模型陷入了一个奇怪的循环反复调用相同的工具参数几乎一样结果也一样但它就是停不下来。这个问题的根源在于 AgentLoop 的设计本身。循环的退出条件是模型不再调用工具但如果模型一直觉得自己还没做完循环就不会停。我们需要在循环里加一些机制让 Agent 在打转的时候能被拉住。循环回顾先回顾一下 AgentLoop 的基本结构defagent_loop(query,max_turns20):messages[{role:user,content:query}]forturninrange(max_turns):respclient.messages.create(modelclaude-sonnet-4-20250514,messagesmessages,toolstools,)messages.append({role:assistant,content:resp.content})ifresp.stop_reason!tool_use:breaktool_results[]forblockinresp.content:ifblock.typetool_use:outputdispatch_tool(block.name,block.input)tool_results.append(make_tool_result(block.id,output))messages.append({role:user,content:tool_results})else:print(达到最大轮次停止执行)max_turns20是最基础的保护。但这个保护太粗暴了20 轮正常推理可能刚好够用而一个死循环可能在第 5 轮就已经开始了。等到第 20 轮才停中间 15 轮的 Token 全浪费了。所以我们需要更精细的检测手段。为什么会陷入循环调用模型陷入循环的原因通常有几种工具返回了模型无法处理的信息。比如执行一个命令报了权限错误模型决定换一种写法再试结果还是权限错误。它看到了错误信息但没有理解这是权限问题而非写法问题于是不断换写法重试。任务本身没有终止信号。比如帮我优化这段代码模型改了一版觉得还能再改于是继续改。每一轮它都觉得自己在优化但其实改动越来越小甚至在来回改。上下文太长导致早期信息被忽略。模型在第 3 轮已经确认文件存在但到了第 15 轮这条信息被推到了上下文的边缘它又去检查了一遍文件是否存在。这些情况的共同点是模型每一轮都认为自己在做正确的事但从外部视角看它其实是在原地踏步。三种检测手段重复调用检测最直接的检测方式记录最近几次工具调用的签名如果连续几次完全相同说明 Agent 卡住了。importhashlibfromcollectionsimportdequeclassLoopDetector:def__init__(self,window3):self.recent_callsdeque(maxlenwindow)defrecord(self,tool_name:str,tool_input:dict)-bool:记录一次工具调用返回是否检测到循环# 把工具名和参数序列化后取哈希作为调用的签名call_strf{tool_name}:{sorted(tool_input.items())}signaturehashlib.md5(call_str.encode()).hexdigest()self.recent_calls.append(signature)# 窗口填满后检查是否全部相同iflen(self.recent_calls)self.recent_calls.maxlen:iflen(set(self.recent_calls))1:returnTruereturnFalsewindow3表示检测最近 3 次调用。如果 3 次调用的签名完全一样判定为循环。这个窗口大小可以根据场景调整太小容易误判有些任务确实需要连续调用同一个工具太大则浪费几轮才检测到。把它集成到 AgentLoop 里defagent_loop(query,max_turns20):messages[{role:user,content:query}]detectorLoopDetector(window3)forturninrange(max_turns):respclient.messages.create(modelclaude-sonnet-4-20250514,messagesmessages,toolstools,)messages.append({role:assistant,content:resp.content})ifresp.stop_reason!tool_use:breaktool_results[]forblockinresp.content:ifblock.typetool_use:outputdispatch_tool(block.name,block.input)tool_results.append(make_tool_result(block.id,output))# 记录调用并检测循环ifdetector.record(block.name,block.input):tool_results.append(make_tool_result(block.id,检测到重复调用请换一种方式处理或者直接告诉用户当前遇到的问题。))messages.append({role:user,content:tool_results})检测到循环后不是直接中断而是给模型一个提示你已经连续三次做了同样的事换个思路或者告诉用户。这样模型有机会自行调整比直接杀掉循环更温和。相似调用检测有时候参数不完全一样但非常接近。比如模型第一次执行cat /tmp/test.txt第二次执行cat /tmp/test.txt末尾多个空格签名不同但本质一样。可以加一层模糊匹配classFuzzyLoopDetector:def__init__(self,window3,similarity_threshold0.9):self.recent_callsdeque(maxlenwindow)def_normalize(self,tool_name:str,tool_input:dict)-str:标准化调用参数去掉首尾空格等干扰commandtool_input.get(command,)ifisinstance(command,str):commandcommand.strip()returnf{tool_name}:{command}defrecord(self,tool_name:str,tool_input:dict)-bool:normalizedself._normalize(tool_name,tool_input)self.recent_calls.append(normalized)iflen(self.recent_calls)self.recent_calls.maxlen:iflen(set(self.recent_calls))1:returnTruereturnFalse标准化之后再去比较可以过滤掉空格、大小写这类微小差异。累计调用次数检测还有一种情况模型没有连续调用同一个工具但它在整个任务中反复调用了十几次同一个工具。这不算连续重复但同样说明有问题。classCallCounter:def__init__(self,max_calls_per_tool10):self.counts{}self.max_callsmax_calls_per_tooldefrecord(self,tool_name:str,tool_input:dict)-bool:记录调用次数返回是否超过阈值self.counts[tool_name]self.counts.get(tool_name,0)1ifself.counts[tool_name]self.max_calls:returnTruereturnFalse这个检测粒度更粗但能兜底一些非连续但高频的循环。三种阻断策略检测到循环之后下一步是决定怎么办。直接中断是最简单的但是会直接截断前端AI消息的输出用户看到的是一个输出一半的消息体验感极差。软阻断提示模型前面代码里已经展示了这个思路。检测到循环后往工具结果里注入一条提示告诉模型它在重复。模型收到提示后有机会改变策略。ifdetector.record(block.name,block.input):output(f原始输出:{output}\n\nf---\nf[系统提示] 你已经连续多次执行相同的工具调用结果没有变化。f请重新评估当前情况考虑换一种方式或者向用户说明遇到的困难。)这种方式的好处是不中断 Agent 的推理流程给它一个自我修正的机会。硬阻断强制退出如果软阻断之后模型还是继续循环就需要硬阻断了。defagent_loop(query,max_turns20):messages[{role:user,content:query}]detectorLoopDetector(window3)soft_block_count0MAX_SOFT_BLOCKS2# 最多容忍两次软阻断forturninrange(max_turns):respclient.messages.create(modelclaude-sonnet-4-20250514,messagesmessages,toolstools,)messages.append({role:assistant,content:resp.content})ifresp.stop_reason!tool_use:breaktool_results[]forblockinresp.content:ifblock.typetool_use:outputdispatch_tool(block.name,block.input)ifdetector.record(block.name,block.input):soft_block_count1ifsoft_block_countMAX_SOFT_BLOCKS:return任务无法完成Agent 陷入重复调用循环请检查任务描述或提供更多上下文。output(f原始输出:{output}\n\nf[系统提示] 检测到重复调用请调整策略。)tool_results.append(make_tool_result(block.id,output))messages.append({role:user,content:tool_results})先软阻断两次给模型自我修正的机会。如果两次之后还在循环直接退出并告知用户。用 Hook 拦截如果你的 Agent 框架支持 Hook循环检测可以做成一个 PreToolUse Hook和主循环逻辑解耦# 循环检测 HookdetectorLoopDetector(window3)defdetect_loop(tool_name,tool_input):ifdetector.record(tool_name,tool_input):return检测到重复调用请换一种方式处理。returnNoneregister_hook(PreToolUse,detect_loop)Hook 的好处是可插拔。开发阶段开着循环检测测试阶段可以关掉不影响主循环代码。而且你可以把检测逻辑做得很复杂比如加上语义相似度判断主循环完全不需要改动。和其他机制的配合循环检测不是孤立的它需要和其他保护机制配合使用。机制作用时机粒度优点缺点max_turns循环外粗简单可靠浪费中间轮次重复调用检测每次工具调用细及时发现窗口大小需调参累计次数检测每次工具调用中兜底高频调用不区分正常/异常Hook 拦截每次工具调用细可插拔、可组合依赖框架支持实际项目中这几个机制通常是叠加使用的。max_turns 是最后的兜底重复调用检测是主要手段Hook 负责把检测逻辑和主循环解耦。更好的做法循环检测是事后补救。更好的做法是从源头减少循环发生的概率。优化 System Prompt。在系统指令里明确告诉模型如果连续两次尝试同一个操作结果不变应该停下来分析原因而不是继续尝试。这类指引能从源头降低循环的概率。控制工具返回的信息量。工具返回的结果越清晰模型越容易判断下一步该怎么做。比如一个命令执行失败返回的信息里如果包含权限不足模型就知道换个思路如果只返回一个模糊的执行失败模型可能会反复换写法重试。上下文压缩。对话太长时早期的关键信息可能被淹没。定期压缩上下文让模型始终能看到最重要的信息减少因为忘了之前的结论而重复操作的情况。小结Agent 工具循环调用的本质是模型每一轮都认为自己在做正确的事但从外部看它在原地踏步。解决方案是在循环里加一层旁观者通过签名比对检测重复调用先提示模型自行调整不行就强制中断。这个旁观者可以用独立模块实现也可以做成 Hook 插入循环。