首页
看点啥
插画图片
首页 看点啥 ReAct 循环中陷入"工具调用死循环"

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

2026-07-31 0

ReAct 循环中陷入"工具调用死循环"的重点在于把前置条件、操作顺序和容易误判的地方分清楚。

【今日问题】

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

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

【真实场景】

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

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

Agent 开始工作了:

第 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 搭建一个极简复现场景:

from langchain.agents import AgentExecutor, create_react_agentfrom langchain_community.tools import DuckDuckGoSearchRunfrom langchain_openai import ChatOpenAIfrom 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 questionThought: think about what to doAction: the tool to useAction Input: the input to the actionObservation: the result... (repeat Thought/Action/Observation)Thought: I now know the final answerFinal Answer: the final answerQuestion: {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 的行为模式:

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

2. 根因分析

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

比喻 1:超市找酱油

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

但 Agent 的行为是:

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

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

比喻 2:侦探查案

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

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

技术根因:

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

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

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

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

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 answeryou 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 informationFinal Answer: ...{agent_scratchpad}""")

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

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

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

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

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

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

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

from langchain_core.pydantic_v1 import BaseModel, Fieldfrom typing importLiteral, OptionalclassAgentDecision(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)classStructuredReActAgent:""" 每轮都要求模型做出显式的"搜 vs 答"的抉择, 并且当 confidence >= 7 时自动终止。 """defrun(self, query: str, max_rounds: int = 5):context = f"用户问题: {query}n当前已获取信息: 暂无"for round_num inrange(1, max_rounds + 1):decision: AgentDecision = llm_with_structure.invoke(f"{context}nn请决定: 继续搜索还是给出最终答案?" )print(f"第 {round_num} 轮: decision={decision.decision}, "f"confidence={decision.confidence}")if decision.decision == "answer"and decision.confidence >= 7:return decision.final_answerif decision.decision == "search":# 执行搜索,更新 contextresult = self.search(decision.search_query)context += f"n搜索结果 [{decision.search_query}]: {result}"# 最后一轮强制输出if round_num == max_rounds:return llm_with_structure.invoke(f"{context}nn这是最后一轮,你必须给出最终答案。" ).final_answer

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

三种方案对比总结

维度A:收敛 PromptB:信息增益门控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)来判断"这一轮搜索结果是否带来了新信息"。

提示:

# 把 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(但实际有新信息),会有什么后果?如何兜底?
喜欢(0)

上一篇

Claude Code Router 接入过程的爬坑记录

Claude Code Router 接入过程的爬坑记录

下一篇

学完AI编程能做什么?就业、自由职业、创业三层新形态

学完AI编程能做什么?就业、自由职业、创业三层新形态
猜你喜欢