ReAct 循环中陷入"工具调用死循环"

【今日问题】

Agent 在 ReAct 循环中陷入"工具调用死循环"------反复调用同一个搜索工具 8 次,每次只是换个近义词,最终因为 token 耗尽而崩溃,却从未给出用户想要的答案。


【真实场景】

某 SaaS 公司为内部客服搭建了一个"合同条款查询 Agent"。产品经理给它配了一个 search_contract 工具,用来在企业知识库中检索合同模板。

上线第三天,一个销售同事问了句:

"帮我把去年签的那个 SaaS 订阅合同的自动续费条款找出来。"

Agent 开始工作了:

ini 复制代码
第 1 轮:search_contract(query="SaaS 订阅合同 自动续费") → 返回 5 条结果
第 2 轮:search_contract(query="SaaS 订阅协议 自动续约") → 返回 3 条结果
第 3 轮:search_contract(query="软件即服务 合同 续费条款") → 返回 4 条结果
第 4 轮:search_contract(query="自动续费条款 SaaS 协议") → 返回 2 条结果
第 5 轮:search_contract(query="SaaS 合同 续约 自动") → 返回 3 条结果
第 6 轮:search_contract(query="续费 条款") → 返回 6 条结果
第 7 轮:search_contract(query="自动续费") → 返回 4 条结果
第 8 轮:search_contract(query="续费") → 返回 7 条结果

然后,上下文窗口爆了 。Agent 崩溃,返回 Token limit exceeded。用户等了 45 秒,什么都没得到。

产品经理怒了:"这智能体怕不是个智障吧?搜了 8 次同一个东西!"


【思考盲区】

在开始拆解之前,先看看你脑子里有没有这些"直觉陷阱":

盲区 你的直觉 实际情况
"给它更多工具就好了" 工具越多,能力越强 工具多了模型反而更"贪心",总想再试一个
"把 max_iterations 调大" 多跑几轮总能出结果 每一轮都在消耗 token,且模型不知道"何时该停"
"写更好的 prompt 就行" 提示词是银弹 Prompt 只能影响意图,不能阻止模型"上瘾"
"这是一个 bug" 这是代码逻辑错了 这是涌现行为------模型在 ReAct 框架下自然产生的策略退化
"换个更强的模型" GPT-5 就不会这样 更强的模型可能搜得更"花哨",但停不下来的问题依然存在

核心误区:你以为 Agent 是"聪明但偶尔犯错",其实它在 ReAct 循环里更像一个"只知道锤子的人,看什么都是钉子"。


【逐步拆解】

1. 现象复现(最小示例)

用 LangChain 搭建一个极简复现场景:

ini 复制代码
from langchain.agents import AgentExecutor, create_react_agent
from langchain_community.tools import DuckDuckGoSearchRun
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
​
# 只给一个搜索工具
search = DuckDuckGoSearchRun()
tools = [search]
​
llm = ChatOpenAI(model="gpt-4o", temperature=0)
​
# ReAct prompt(简化版)
prompt = PromptTemplate.from_template("""
You have access to: {tools}
Use this format:
Question: the input question
Thought: think about what to do
Action: the tool to use
Action Input: the input to the action
Observation: the result
... (repeat Thought/Action/Observation)
Thought: I now know the final answer
Final Answer: the final answer
​
Question: {input}
{agent_scratchpad}
""")
​
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent,
    tools=tools,
    max_iterations=10,     # ← 危险!给足了"弹药"
    verbose=True,
    handle_parsing_errors=True
)
​
# 触发死循环的典型问题
result = executor.invoke({
    "input": "2024年中国新能源汽车出口数据是多少?"
})

你会发现 Agent 的行为模式:

  • 搜了 → 觉得不够精确 → 换个词再搜
  • 又搜了 → 还是觉得信息不完整 → 再换个角度
  • 无限循环,直到 token 打满

2. 根因分析

让我们用两个比喻来理解:


比喻 1:超市找酱油

你去超市买酱油。正常人是:走到调味料区 → 看到酱油 → 拿一瓶 → 结账走人。

但 Agent 的行为是:

  • 走到调味料区,看到"生抽""老抽""味极鲜"......觉得信息还不够
  • 走到隔壁货架,发现"蒸鱼豉油""煲仔饭酱油"......更困惑了
  • 再走到进口区,看到"日式酱油""泰式鱼露"......
  • 最后超市关门了(token 耗尽),你两手空空

Agent 的问题不是"找不到",而是不知道什么时候已经找到了


比喻 2:侦探查案

ReAct 框架就像一个侦探:观察现场(Observation)→ 推理(Thought)→ 采取行动(Action)。

好侦探知道:证据够了,可以结案了。 烂侦探(我们的 Agent)会:反复勘察同一个现场,每次都认为"可能还有遗漏的线索"。


技术根因

层面 问题
Prompt 层 ReAct 模板没有"收敛信号"------没有明确告诉模型什么情况下该输出 Final Answer
工具设计层 每次搜索结果都是"新信息",模型无法判断"我已经搜够了"
执行框架层 max_iterations 是硬上限,不是智能停止条件
模型认知层 LLM 天生有"信息贪"------给定更多轮次,它倾向于填满

3. 解决方案(三种方案对比)

方案 A:收敛式 Prompt(成本最低,推荐先试)

在 Prompt 中加入明确的"收敛信号":

ini 复制代码
convergence_prompt = PromptTemplate.from_template("""
You are a precise research assistant. Follow these rules STRICTLY:
​
1. You may use tools to gather information.
2. AFTER each tool call, ask yourself: "Do I have enough to answer the user?"
3. If the answer is YES → IMMEDIATELY output Final Answer. Do NOT search again.
4. You may search AT MOST 3 times. After 3 searches, you MUST give the best answer
   you can with whatever information you have.
5. If the first search result already contains the answer, DO NOT search again.
   Just use it.
​
Available tools: {tools}
​
Format:
Question: {input}
Thought: ...
Action: ...
Observation: ...
... (max 3 rounds)
Thought: I have enough information
Final Answer: ...
​
{agent_scratchpad}
""")

优点 :零额外成本,立即可用 缺点:依赖模型遵循指令的能力,小模型可能无视


方案 B:信息增益门控(推荐生产环境)

在每次工具调用后,计算"这次搜索到底有没有带来新信息":

python 复制代码
import hashlib
from difflib import SequenceMatcher
​
class GatedAgentExecutor:
    """
    在工具调用之间插入"信息增益检查",如果新一轮搜索
    没有显著新信息,直接终止并返回已有结果。
    """
    
    def __init__(self, similarity_threshold=0.7, max_stagnant_rounds=2):
        self.similarity_threshold = similarity_threshold
        self.max_stagnant_rounds = max_stagnant_rounds
        self.previous_observations = []
        self.stagnant_count = 0
    
    def has_new_information(self, new_observation: str) -> bool:
        """
        判断新搜索结果是否带来了实质性新信息。
        用最长公共子序列的倒数作为"新信息量"的度量。
        """
        if not self.previous_observations:
            self.previous_observations.append(new_observation)
            return True
        
        # 合并之前所有观察结果
        combined_previous = " ".join(self.previous_observations)
        similarity = SequenceMatcher(None, combined_previous, new_observation).ratio()
        
        if similarity > self.similarity_threshold:
            self.stagnant_count += 1
            print(f"⚠️  信息重复度 {similarity:.0%}, 停滞计数: {self.stagnant_count}")
            return False
        
        # 有新信息,重置停滞计数
        self.stagnant_count = 0
        self.previous_observations.append(new_observation)
        return True
    
    def should_terminate(self, current_observation: str, accumulated_results: list) -> bool:
        """综合判断是否应该终止搜索"""
        
        # 条件1:信息停滞(连续 N 轮无新信息)
        if self.stagnant_count >= self.max_stagnant_rounds:
            print("🛑 信息停滞,终止搜索")
            return True
        
        # 条件2:已积累足够多的结果(搜索结果已经丰富)
        if len(accumulated_results) >= 5:
            print("🛑 已积累足够信息,终止搜索")
            return True
        
        # 条件3:单次搜索结果已经包含了明确的答案(简单启发式)
        answer_indicators = ["是的", "不,", "根据", "第一条", "答案是", "具体来说"]
        if any(indicator in current_observation for indicator in answer_indicators):
            print("🛑 当前结果已包含明确答案,终止搜索")
            return True
        
        return False
​
​
# === 集成到 LangChain AgentExecutor ===
from langchain.agents import AgentExecutor
from langchain.callbacks.base import BaseCallbackHandler
​
class ConvergenceCallback(BaseCallbackHandler):
    """在 Agent 每轮迭代后进行检查"""
    
    def __init__(self, gate: GatedAgentExecutor):
        self.gate = gate
        self.accumulated = []
        self.force_stop = False
    
    def on_agent_action(self, action, **kwargs):
        """工具调用前记录"""
        pass
    
    def on_tool_end(self, output, **kwargs):
        """工具返回后检查"""
        self.accumulated.append(output)
        if not self.gate.has_new_information(output):
            # 标记需要干预
            pass
        if self.gate.should_terminate(output, self.accumulated):
            self.force_stop = True

优点 :精确控制,不依赖模型自觉 缺点:实现复杂,相似度阈值需要针对具体场景调参


方案 C:结构化输出强制收敛(2025 新范式)

利用模型的 Structured Output / JSON Mode 能力,强制每轮输出包含"是否继续"的决策:

python 复制代码
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import Literal, Optional
​
class AgentDecision(BaseModel):
    """Agent 每轮的决策结构"""
    reasoning: str = Field(description="当前轮次的推理过程")
    decision: Literal["search", "answer"] = Field(
        description="继续搜索还是给出最终答案"
    )
    search_query: Optional[str] = Field(
        description="如果要搜索,具体搜什么"
    )
    final_answer: Optional[str] = Field(
        description="如果决定回答,答案是什么"
    )
    confidence: int = Field(
        description="对当前信息的信心程度,1-10",
        ge=1, le=10
    )
​
# 使用结构化输出
llm_with_structure = ChatOpenAI(model="gpt-4o").with_structured_output(AgentDecision)
​
class StructuredReActAgent:
    """
    每轮都要求模型做出显式的"搜 vs 答"的抉择,
    并且当 confidence >= 7 时自动终止。
    """
    
    def run(self, query: str, max_rounds: int = 5):
        context = f"用户问题: {query}\n当前已获取信息: 暂无"
        
        for round_num in range(1, max_rounds + 1):
            decision: AgentDecision = llm_with_structure.invoke(
                f"{context}\n\n请决定: 继续搜索还是给出最终答案?"
            )
            
            print(f"第 {round_num} 轮: decision={decision.decision}, "
                  f"confidence={decision.confidence}")
            
            if decision.decision == "answer" and decision.confidence >= 7:
                return decision.final_answer
            
            if decision.decision == "search":
                # 执行搜索,更新 context
                result = self.search(decision.search_query)
                context += f"\n搜索结果 [{decision.search_query}]: {result}"
            
            # 最后一轮强制输出
            if round_num == max_rounds:
                return llm_with_structure.invoke(
                    f"{context}\n\n这是最后一轮,你必须给出最终答案。"
                ).final_answer

优点 :最优雅,把"何时停止"变成了一个明确的分类决策 缺点:需要模型支持 structured output,老旧模型不适用


三种方案对比总结

维度 A:收敛 Prompt B:信息增益门控 C:结构化输出
实现复杂度 ⭐⭐⭐ ⭐⭐
对小模型友好度 ⭐⭐ ⭐⭐⭐
不依赖模型自觉 部分
额外 token 消耗 几乎为零 中(相似度计算)
可控性 最高
适用场景 原型验证 生产高可靠 前沿探索

推荐策略:先用 A 快速验证,上线前切到 B 或 C(取决于你的模型是否支持 structured output)。


【避坑指南】

  1. max_iterations 不是安全网,是死亡线。 把它设到 10,Agent 就会真的跑满 10 轮。建议设为 3~5,并在第 N-1 轮 Prompt 中注入"你必须现在回答"的指令。
  2. 不要在工具描述里写"如果找不到就再搜一次"。 很多人在工具的 description 里加类似 "If you don't find the answer, try a different query" 的提示------这等于在教模型"上瘾"。
  3. 观察结果(Observation)太长会淹没收敛信号。 如果每次搜索返回 5000 字,模型根本记不住"我该停了"。截断 Observation,只保留最相关的片段(比如用 max_tokens=500 做摘要)。
  4. temperature=0 不代表行为确定。 很多人以为 temperature=0 就不会出死循环------错。死循环的原因是 ReAct 框架的结构性缺陷,不是温度的随机性。
  5. 小模型(7B-13B)更容易陷入死循环。 因为小模型的指令遵循能力弱,Prompt 里的收敛信号可能被完全忽视。这种情况下优先用方案 B(门控)。

【你来做一做】

现在给你一个动手练习:

场景 :把上面的方案 B(信息增益门控)改造一下,让它不是简单地比较"文本相似度",而是用一个轻量级本地模型(比如 Ollama 上的 qwen2.5:3b)来判断"这一轮搜索结果是否带来了新信息"。

提示

ini 复制代码
# 把 difflib.SequenceMatcher 替换成一个 LLM 调用
def has_new_information_with_llm(previous_results: list[str], new_result: str) -> bool:
    prompt = f"""
    你是一个信息评估助手。判断以下新搜索结果是否包含之前结果中没有的新信息。
    
    【之前的结果汇总】
    {' '.join(previous_results[-3:])}   # 只看最近 3 轮
    
    【新的搜索结果】
    {new_result}
    
    只回答 YES 或 NO。YES 表示有新信息,NO 表示信息重复。
    """
    # 你的代码:调用本地 Ollama 模型
    pass

思考题

  1. 用 LLM 做信息增益判断 vs 用文本相似度,各自的 trade-off 是什么?(延迟、成本、准确性)
  2. 如果 LLM 本身也"犯傻"说 NO(但实际有新信息),会有什么后果?如何兜底?
相关推荐
沉默王二1 小时前
老板:“请说出一个录用你的理由。”我脱口而出:“每个月 AI 支出都超过我的生活费了!”老板愣了一下,随即哈哈大笑:“好吧,你被录用了。”
人工智能·ai编程·claude
Mr_愚人派2 小时前
当"Claude"不再是 Claude:一次第三方 API 代理引发的 AI 身份伪造排查实录
人工智能·安全
Lee川2 小时前
Memory 模块深度解析(面试向)
人工智能·面试
MacroZheng2 小时前
Claude Code官方桌面端正式发布,夯爆了!
java·人工智能·后端
IT_陈寒3 小时前
React的useEffect依赖数组把我坑惨了,真相其实很简单
前端·人工智能·后端
Kapaseker3 小时前
什么?Stack Overflow 给 AI 做了个 Stack Overflow
人工智能
aneasystone本尊3 小时前
让小龙虾自己写手册:Skill Workshop
人工智能
火山引擎开发者社区4 小时前
一篇看懂 VKE AI Profiling:AI 应用性能分析优化实战
人工智能
IT乐手4 小时前
马斯克的AI模型Grok,竟然帮美军炸了伊朗?!
人工智能