【AI应用开发】Agent 无限 loop(反复调用同一个工具)如何规避?

【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 不会主动停下来,要靠系统强制停下来。

相关推荐
zzzll11111 小时前
Ollama:本地运行大型语言模型的轻量级解决方案
人工智能·语言模型·自然语言处理
qq_348231851 小时前
Agent落地
人工智能
JaydenAI1 小时前
[AG-UI详解-08]AG-UI客户端工具 V.S. LangChain的Headless工具
ai·langchain·agent·ag-ui·maf
银-豆豆1 小时前
Python爬虫
开发语言·爬虫·python
seacracker1 小时前
HPC+AI混合负载存储碎片化严重?PowerFS Filer Raft强一致四组件统一架构一套搞定
人工智能·架构
IDIOT___IDIOT1 小时前
JSON-RPC 2.0 与 MCP 协议:从消息格式到进程通信 大模型MCP
rpc·大模型·json·agent·mcp
HySpark1 小时前
语音输入法实践:Rust 集成 Paraformer 推理引擎踩坑与优化过程
人工智能·后端·语音识别·熙瑾会悟
不爱土豆唯爱马铃薯1 小时前
用MonkeyCode做一个野外数据采集表 | MonkeyCode实操系列EP06
人工智能·数据分析
小月土星1 小时前
LeetCode 142. 环形链表 II 超详细题解(快慢指针原理+逐行代码拆解)
算法