Agent 工具循环调用的解决办法
目录
最近笔者在做自己的Agent项目,就遇到了工具循环调用的问题。模型陷入了一个奇怪的循环:反复调用相同的工具,参数几乎一样,结果也一样,但它就是停不下来。这个问题的根源在于 AgentLoop 的设计本身。循环的退出条件是"模型不再调用工具",但如果模型一直觉得自己还没做完,循环就不会停。我们需要在循环里加一些机制,让 Agent 在打转的时候能被拉住。
循环回顾
先回顾一下 AgentLoop 的基本结构:
python
def agent_loop(query, max_turns=20):
messages = [{"role": "user", "content": query}]
for turn in range(max_turns):
resp = client.messages.create(
model="claude-sonnet-4-20250514",
messages=messages,
tools=tools,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
break
tool_results = []
for block in resp.content:
if block.type == "tool_use":
output = dispatch_tool(block.name, block.input)
tool_results.append(make_tool_result(block.id, output))
messages.append({"role": "user", "content": tool_results})
else:
print("达到最大轮次,停止执行")
max_turns=20 是最基础的保护。但这个保护太粗暴了:20 轮正常推理可能刚好够用,而一个死循环可能在第 5 轮就已经开始了。等到第 20 轮才停,中间 15 轮的 Token 全浪费了。所以我们需要更精细的检测手段。
为什么会陷入循环调用
模型陷入循环的原因通常有几种:
工具返回了模型无法处理的信息。 比如执行一个命令报了权限错误,模型决定换一种写法再试,结果还是权限错误。它看到了错误信息,但没有"理解"这是权限问题而非写法问题,于是不断换写法重试。
任务本身没有终止信号。 比如"帮我优化这段代码",模型改了一版,觉得还能再改,于是继续改。每一轮它都觉得自己在"优化",但其实改动越来越小,甚至在来回改。
上下文太长导致早期信息被忽略。 模型在第 3 轮已经确认文件存在,但到了第 15 轮,这条信息被推到了上下文的边缘,它又去检查了一遍文件是否存在。
这些情况的共同点是:模型每一轮都认为自己在做"正确的事",但从外部视角看,它其实是在原地踏步。
三种检测手段
重复调用检测
最直接的检测方式:记录最近几次工具调用的签名,如果连续几次完全相同,说明 Agent 卡住了。
python
import hashlib
from collections import deque
class LoopDetector:
def __init__(self, window=3):
self.recent_calls = deque(maxlen=window)
def record(self, tool_name: str, tool_input: dict) -> bool:
"""记录一次工具调用,返回是否检测到循环"""
# 把工具名和参数序列化后取哈希,作为调用的签名
call_str = f"{tool_name}:{sorted(tool_input.items())}"
signature = hashlib.md5(call_str.encode()).hexdigest()
self.recent_calls.append(signature)
# 窗口填满后,检查是否全部相同
if len(self.recent_calls) == self.recent_calls.maxlen:
if len(set(self.recent_calls)) == 1:
return True
return False
window=3 表示检测最近 3 次调用。如果 3 次调用的签名完全一样,判定为循环。这个窗口大小可以根据场景调整:太小容易误判(有些任务确实需要连续调用同一个工具),太大则浪费几轮才检测到。
把它集成到 AgentLoop 里:
python
def agent_loop(query, max_turns=20):
messages = [{"role": "user", "content": query}]
detector = LoopDetector(window=3)
for turn in range(max_turns):
resp = client.messages.create(
model="claude-sonnet-4-20250514",
messages=messages,
tools=tools,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
break
tool_results = []
for block in resp.content:
if block.type == "tool_use":
output = dispatch_tool(block.name, block.input)
tool_results.append(make_tool_result(block.id, output))
# 记录调用并检测循环
if detector.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 (末尾多个空格),签名不同但本质一样。
可以加一层模糊匹配:
python
class FuzzyLoopDetector:
def __init__(self, window=3, similarity_threshold=0.9):
self.recent_calls = deque(maxlen=window)
def _normalize(self, tool_name: str, tool_input: dict) -> str:
"""标准化调用参数,去掉首尾空格等干扰"""
command = tool_input.get("command", "")
if isinstance(command, str):
command = command.strip()
return f"{tool_name}:{command}"
def record(self, tool_name: str, tool_input: dict) -> bool:
normalized = self._normalize(tool_name, tool_input)
self.recent_calls.append(normalized)
if len(self.recent_calls) == self.recent_calls.maxlen:
if len(set(self.recent_calls)) == 1:
return True
return False
标准化之后再去比较,可以过滤掉空格、大小写这类微小差异。
累计调用次数检测
还有一种情况:模型没有连续调用同一个工具,但它在整个任务中反复调用了十几次同一个工具。这不算"连续重复",但同样说明有问题。
python
class CallCounter:
def __init__(self, max_calls_per_tool=10):
self.counts = {}
self.max_calls = max_calls_per_tool
def record(self, tool_name: str, tool_input: dict) -> bool:
"""记录调用次数,返回是否超过阈值"""
self.counts[tool_name] = self.counts.get(tool_name, 0) + 1
if self.counts[tool_name] > self.max_calls:
return True
return False
这个检测粒度更粗,但能兜底一些"非连续但高频"的循环。
三种阻断策略
检测到循环之后,下一步是决定怎么办。直接中断是最简单的,但是会直接截断前端AI消息的输出,用户看到的是一个输出一半的消息,体验感极差。
软阻断:提示模型
前面代码里已经展示了这个思路。检测到循环后,往工具结果里注入一条提示,告诉模型它在重复。模型收到提示后,有机会改变策略。
python
if detector.record(block.name, block.input):
output = (
f"原始输出: {output}\n\n"
f"---\n"
f"[系统提示] 你已经连续多次执行相同的工具调用,结果没有变化。"
f"请重新评估当前情况,考虑换一种方式,或者向用户说明遇到的困难。"
)
这种方式的好处是不中断 Agent 的推理流程,给它一个自我修正的机会。
硬阻断:强制退出
如果软阻断之后模型还是继续循环,就需要硬阻断了。
python
def agent_loop(query, max_turns=20):
messages = [{"role": "user", "content": query}]
detector = LoopDetector(window=3)
soft_block_count = 0
MAX_SOFT_BLOCKS = 2 # 最多容忍两次软阻断
for turn in range(max_turns):
resp = client.messages.create(
model="claude-sonnet-4-20250514",
messages=messages,
tools=tools,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
break
tool_results = []
for block in resp.content:
if block.type == "tool_use":
output = dispatch_tool(block.name, block.input)
if detector.record(block.name, block.input):
soft_block_count += 1
if soft_block_count >= MAX_SOFT_BLOCKS:
return "任务无法完成:Agent 陷入重复调用循环,请检查任务描述或提供更多上下文。"
output = (
f"原始输出: {output}\n\n"
f"[系统提示] 检测到重复调用,请调整策略。"
)
tool_results.append(make_tool_result(block.id, output))
messages.append({"role": "user", "content": tool_results})
先软阻断两次,给模型自我修正的机会。如果两次之后还在循环,直接退出并告知用户。
用 Hook 拦截
如果你的 Agent 框架支持 Hook,循环检测可以做成一个 PreToolUse Hook,和主循环逻辑解耦:
python
# 循环检测 Hook
detector = LoopDetector(window=3)
def detect_loop(tool_name, tool_input):
if detector.record(tool_name, tool_input):
return "检测到重复调用,请换一种方式处理。"
return None
register_hook("PreToolUse", detect_loop)
Hook 的好处是可插拔。开发阶段开着循环检测,测试阶段可以关掉,不影响主循环代码。而且你可以把检测逻辑做得很复杂(比如加上语义相似度判断),主循环完全不需要改动。
和其他机制的配合
循环检测不是孤立的,它需要和其他保护机制配合使用。

| 机制 | 作用时机 | 粒度 | 优点 | 缺点 |
|---|---|---|---|---|
| max_turns | 循环外 | 粗 | 简单可靠 | 浪费中间轮次 |
| 重复调用检测 | 每次工具调用 | 细 | 及时发现 | 窗口大小需调参 |
| 累计次数检测 | 每次工具调用 | 中 | 兜底高频调用 | 不区分正常/异常 |
| Hook 拦截 | 每次工具调用 | 细 | 可插拔、可组合 | 依赖框架支持 |
实际项目中,这几个机制通常是叠加使用的。max_turns 是最后的兜底,重复调用检测是主要手段,Hook 负责把检测逻辑和主循环解耦。
更好的做法
循环检测是事后补救。更好的做法是从源头减少循环发生的概率。
优化 System Prompt。 在系统指令里明确告诉模型:如果连续两次尝试同一个操作结果不变,应该停下来分析原因,而不是继续尝试。这类指引能从源头降低循环的概率。
控制工具返回的信息量。 工具返回的结果越清晰,模型越容易判断下一步该怎么做。比如一个命令执行失败,返回的信息里如果包含"权限不足",模型就知道换个思路;如果只返回一个模糊的"执行失败",模型可能会反复换写法重试。
上下文压缩。 对话太长时,早期的关键信息可能被淹没。定期压缩上下文,让模型始终能看到最重要的信息,减少因为"忘了之前的结论"而重复操作的情况。
小结
Agent 工具循环调用的本质是:模型每一轮都认为自己在做正确的事,但从外部看它在原地踏步。解决方案是在循环里加一层"旁观者",通过签名比对检测重复调用,先提示模型自行调整,不行就强制中断。这个旁观者可以用独立模块实现,也可以做成 Hook 插入循环。