【AI应用开发】Agent 无限 loop(反复调用同一个工具)如何规避?
1. Agent Loop 的四种模式
模式A: 完全重复 --- query_order(id="001") × 8 次,参数完全相同
模式B: 渐进重复 --- search_docs("退货")→search_docs("退货流程")→search_docs("如何退货")
模式C: 乒乓循环 --- query_order → search_kb → query_order → search_kb → ...
模式D: 发散循环 --- 不断换工具但都不解决问题,广度优先式尝试
2. 根因分析
| 根因 | 说明 |
|---|---|
| LLM 不记忆历史 | 上下文截断后忘记早期步骤 |
| 工具返回无完成标记 | LLM 不知道什么算"够了" |
| Prompt 鼓励过度探索 | "如果不够详细请再次查询"是坏指令 |
| 工具设计缺陷 | 缺少分页标记,LLM 以为还有下页 |
3. 七层防御体系
第1层: 步数上限 (max_steps=15) ← 最简单有效
第2层: 重复检测熔断 (相同调用≤2次) ← 模式A专用
第3层: Token 预算 (超80%预警) ← 兜底保护
第4层: 语义收敛检测 (余弦相似度>0.95) ← 模式B/C/D
第5层: 智能终止信号 (完成标记/答案已覆盖) ← 主动终止
第6层: 超时控制 (全局120s/单步30s) ← 最后防线
第7层: 监控告警 (实时指标+自动降级) ← 持续改进
4. 第一层:步数硬限制
python
class StepLimiter:
def __init__(self, default_max=15, adaptive=True):
self.limits = {
"single_query": 3, # 单次查询
"multi_step": 8, # 多步执行
"research": 20, # 深度研究
}
self.default_max = default_max
def get_limit(self, task_type=None):
return self.limits.get(task_type, self.default_max)
def check(self, step, max_steps):
if step >= max_steps:
return False, "达最大步数,请立即给最终答案"
if step >= max_steps * 0.8:
return True, f"仅剩 {max_steps - step} 步,请尽快总结"
return True, ""
5. 第二层:重复检测熔断
python
class DuplicateDetector:
def __init__(self):
self.history = []
self.max_exact = 2 # 完全相同最多2次
self.max_similar = 3 # 相似调用最多3次
self.circuit_open = False
def check(self, step, tool_name, args, result_hash):
# 检测1: 完全相同
same = [h for h in self.history
if h["tool"] == tool_name and h["args"] == args]
if len(same) >= self.max_exact:
self.circuit_open = True
return f"⛔ 熔断: '{tool_name}' 相同参数已调{len(same)}次"
# 检测2: 乒乓 ABAB
if len(self.history) >= 3:
last4_names = [h["tool"] for h in self.history[-3:]] + [tool_name]
if last4_names[0] == last4_names[2] and last4_names[1] == last4_names[3]:
return f"⚠️ 检测到乒乓循环: {last4_names[0]}↔{last4_names[1]}"
# 检测3: 结果停滞
same_tool = [h for h in self.history if h["tool"] == tool_name]
if len(same_tool) >= 2 and all(h["result_hash"] == result_hash for h in same_tool[-2:]):
return "⚠️ 结果连续不变,继续调用无意义"
self.history.append({
"step": step, "tool": tool_name,
"args": args, "result_hash": result_hash
})
return None
6. 第三层:Token 预算管理
python
class TokenBudget:
def __init__(self, max_total=16000, reserve_output=2000):
self.max_total = max_total
self.reserve = reserve_output
self.used = 0
def can_continue(self):
remaining = self.max_total - self.used
if remaining <= self.reserve:
return False, "Token预算耗尽,必须立即生成最终答案,禁止调用工具"
if self.used / self.max_total > 0.8:
return True, "Token已用80%,请精简"
return True, ""
def spend(self, tokens):
self.used += tokens
7. 第四层:语义收敛检测
python
import numpy as np
class SemanticConvergence:
"""检测 Agent 输出是否还在取得进展"""
def __init__(self, embed_model, threshold=0.95):
self.embed = embed_model
self.threshold = threshold
self.history = [] # [(step, embedding)]
def check(self, step, output_text):
emb = self.embed.encode(output_text[:500])
self.history.append((step, emb))
if len(self.history) < 3:
return True, ""
recent = [h[1] for h in self.history[-3:]]
sims = []
for i in range(len(recent) - 1):
sim = np.dot(recent[i], recent[i+1]) / (
np.linalg.norm(recent[i]) * np.linalg.norm(recent[i+1])
)
sims.append(sim)
if all(s > self.threshold for s in sims):
return False, "连续步骤结果高度相似,信息增益趋零,请停止并总结"
return True, ""
8. 第五层:智能终止信号
python
class TerminationManager:
def should_terminate(self, context):
"""综合判断是否该停止"""
reasons = []
# 1. LLM 主动说完成了
last_msg = self._last_assistant_msg(context["messages"])
if last_msg and any(w in last_msg for w in ["已完成", "以上就是", "总结"]):
reasons.append("LLM声明完成")
# 2. 问题已被覆盖回答
if self._question_covered(context):
reasons.append("问题已完整回答")
# 3. 工具返回完成标记
for r in context["tool_results"][-2:]:
if isinstance(r.get("result"), dict):
if r["result"].get("completion_marker", {}).get("task_completed"):
reasons.append(f"工具{r['tool']}标记完成")
# 4. 用户取消
last_user = self._last_user_msg(context["messages"])
if last_user and any(c in last_user for c in ["取消", "停止", "算了"]):
reasons.append("用户取消")
return len(reasons) > 0, "; ".join(reasons) if reasons else ""
def _last_assistant_msg(self, msgs):
for m in reversed(msgs):
if m.get("role") == "assistant" and m.get("content"):
return m["content"]
return None
def _last_user_msg(self, msgs):
for m in reversed(msgs):
if m.get("role") == "user":
return m.get("content", "")
return ""
def _question_covered(self, context):
# 简化: 检查问题关键词是否在结果中出现
q = context.get("original_question", "")
all_text = json.dumps(
[r.get("result", "") for r in context["tool_results"]],
ensure_ascii=False
)
keywords = [w for w in q if '\u4e00' <= w <= '\u9fff']
if keywords:
covered = sum(1 for k in set(''.join(keywords)[:10]) if k in all_text)
return covered >= 7
return False
9. 第六层:超时控制
python
import asyncio
class TimeoutGuard:
def __init__(self, global_timeout=120, step_timeout=30, llm_timeout=20):
self.global_timeout = global_timeout
self.step_timeout = step_timeout
self.llm_timeout = llm_timeout
async def run(self, agent_fn, user_input):
try:
return await asyncio.wait_for(
agent_fn(user_input), timeout=self.global_timeout
)
except asyncio.TimeoutError:
return {
"success": False,
"result": "处理超时,请缩小问题范围重新提问。",
"reason": "global_timeout"
}
10. 综合防御框架
python
class LoopSafeAgent:
"""集成所有防御机制的 Agent"""
def __init__(self, llm, tools, embed_model):
self.llm = llm
self.tools = tools
self.step_limiter = StepLimiter()
self.dup_detector = DuplicateDetector()
self.token_budget = TokenBudget()
self.convergence = SemanticConvergence(embed_model)
self.termination = TerminationManager()
self.timeout = TimeoutGuard()
self.terminated_early = False # ← 关键:标记是否提前终止
async def run(self, user_input, task_type=None):
state = {"messages": [], "tool_results": [],
"current_step": 0, "original_question": user_input}
max_steps = self.step_limiter.get_limit(task_type)
for step in range(1, max_steps + 1):
state["current_step"] = step
# 1. 步数检查
can, warning = self.step_limiter.check(step, max_steps)
if not can:
self.terminated_early = True
return self._force_summarize(state, warning)
if warning:
state["messages"].append({"role": "system", "content": warning})
# 2. Token 预算检查
can, msg = self.token_budget.can_continue()
if not can:
return self._force_summarize(state, msg)
# 3. 执行步骤
response = await self._execute_step(state)
# 4. 如果调用了工具,记录并检测
if response.tool_calls:
for tc in response.tool_calls:
result = await self._call_tool(tc)
# 重复检测
loop_warning = self.dup_detector.check(
step, tc.name, tc.args,
hashlib.md5(json.dumps(result).encode()).hexdigest()
)
if loop_warning:
state["messages"].append({
"role": "system", "content": f"⚠️ {loop_warning}"
})
if self.dup_detector.circuit_open:
self.terminated_early = True # ← 熔断强制终止
return self._force_summarize(state, "工具熔断")
# 语义收敛检测
progress, msg = self.convergence.check(
step, str(result)[:500]
)
if not progress:
state["messages"].append({
"role": "system", "content": f"⚠️ {msg}"
})
state["tool_results"].append({
"step": step, "tool": tc.name,
"args": tc.args, "result": result
})
# 5. 终止条件检查
should_stop, reason = self.termination.should_terminate(state)
if should_stop and response.finish_reason == "stop":
return response.content
# 更新 Token 预算(估算)
self.token_budget.spend(len(str(state["messages"][-1])) // 4)
# 达到最大步数
return self._force_summarize(state, "达到最大步数")
def _force_summarize(self, state, reason):
"""强制总结已获取的信息"""
if not state["tool_results"]:
return "抱歉,处理过程中遇到问题,请重新描述您的问题。"
summary_prompt = (
f"执行被终止,原因: {reason}。"
f"请基于以下已获取的信息,给用户一个完整的回答:\n\n"
+ json.dumps(
[{"步骤": r["step"], "工具": r["tool"],
"结果": str(r["result"])[:300]}
for r in state["tool_results"]],
ensure_ascii=False, indent=2
)
)
return self.llm.chat(summary_prompt)
核心思想:
不要把防止 Loop 仅当作 Prompt 工程问题------必须在代码层面建立多层防线。LLM 不会主动停下来,要靠系统强制停下来。