ARTICLE · 1167693
你的AI Agent总在第二步死循环?ReAct机制我拆开给你看
你的AI Agent总在第二步死循环?ReAct机制我拆开给你看
你有没有遇到过这种情况:让AI Agent去查一下"明天北京天气怎么样,适合穿什么衣服",它第一次调用天气API成功了,然后就开始无限循环调天气。
或者更隐蔽的:模型给你返回一个看起来很有道理的回答,但其实它根本没拿到最新数据,全凭"幻觉"在编。
上周OpenAI把Agents API免费开放那天,我盯着它内部的agent loop实现看了两个小时。说实话,本质上就是那个2022年提出的老古董——ReAct。只不过这次包装得更漂亮,加了自动上下文压缩、子Agent协调、MCP内置这些新东西。
但骨架没变。
今天这篇文章,我就把ReAct的骨架拆开讲透。看完你应该能回答三个问题:
先说清楚:ReAct到底是个啥
ReAct这个名字拆开就是Reason + Act,意思是"推理 + 行动"。
论文是2022年10月发的,距今快4年了。核心思想极其简单:
让大模型在每一步里先思考(Thought),再决定要不要调用工具(Action),调用完看看结果(Observation),然后再思考,直到能给出最终答案。
它和"一步到位"调用最大的区别是:中间有反馈。
传统Function Calling是这样的:
用户提问 → 模型决定调用工具 → 返回结果 → 模型直接给最终答案ReAct是这样的:
用户提问 ↓思考:这个问题需要查什么? ↓行动:调用工具A ↓观察:A返回了什么 ↓思考:还需要查什么? ↓行动:调用工具B(可能和A不一样) ↓观察:B返回了什么 ↓思考:信息够了 ↓给出最终答案你可能会问:传统调用不也是这样吗?差别在哪?
差别在"思考"这一步是显式存在的。模型必须先输出"我现在在想什么",然后才能调用工具。这看起来是小事,但实际效果天差地别。
我举个真实的例子。
去年我给团队搭过一个客服Agent,处理"我的订单怎么还没发货"这类问题。早期我们用的就是直接Function Calling,效果很差:
用户:我的订单12345怎么还没发货?模型:调用 query_order(order_id="12345")结果:订单状态=已发货,快递单号=SF1234567890模型:您的订单已发货,快递单号是SF1234567890。看起来没问题对吧?但实际生产中,30%的订单是"已发货但物流停滞"的,光查订单状态不够,还得查物流详情。模型不会自动加这一步,只能查完订单就说"已发货",用户体验极差。
换成ReAct之后,prompt变成:
你需要一步步思考:当前已知信息是什么?还缺什么?是否需要再调用工具?Thought: 用户问订单12345的发货情况。我需要先查订单状态。Action: query_order(order_id="12345")Observation: 订单状态=已发货,快递单号=SF1234567890Thought: 已发货但用户说没收到,可能物流有问题。应该再查物流详情。Action: query_logistics(tracking_number="SF1234567890")Observation: 物流状态=运输中,已在分中转停留48小时Thought: 信息够了,可以给用户解释了。Final Answer: 您的订单已发货(单号SF1234567890),但目前在分中转停留超过48小时...你看,模型自己推理出了"还需要查物流"这一步。这一步如果是直接Function Calling,它永远不会做。
这就是ReAct的核心价值:让模型在循环里持续思考,而不是一次性决策。
三阶段拆开看:Thought、Action、Observation
阶段1:Thought(思考)
这一步是ReAct的灵魂。它的prompt模板长这样:
Answer the following questions as best you can. You have access to the following tools:[工具列表,包含每个工具的名称、参数、用途]Use the following format:Thought: 你需要解释当前的情况,以及下一步计划做什么Action: 你要调用的工具,格式必须是 JSON,例如 {"name": "工具名", "args": {"参数名": "参数值"}}Observation: 工具返回的结果(这一行你不需要写,系统会自动填)... (这个 Thought/Action/Observation 可以重复N次)Thought: 我现在可以给出最终答案了Final Answer: 给用户的最终回答Begin!Question: [用户的问题]History: [之前几轮的Thought/Action/Observation]Thought:注意几个关键点:
Thought:Action:Final Answer: 这几个关键字开头,不然它会乱写。这个prompt看着朴素,但实测中有个常见坑:History太长会爆token。
我做过一个客服Agent,循环平均跑5-6轮,每轮Thought + Action + Observation大概300 token,加起来History就有1800 token。如果用户问题本身长一点,很快就到上下文窗口边缘。
解决办法有两种: - 滑动窗口:只保留最近3-4轮的History - 摘要压缩:让模型自己把前几轮总结成一段话塞进History
OpenAI Agents API用的就是后者,叫"自动上下文压缩"。
阶段2:Action(行动)
这一步是模型真正调用工具的部分。它的输出必须是结构化的JSON:
class="language-json">{"name": "query_order","args": {"order_id": "12345" }}作为Java工程师,你应该特别关注Tool Schema的设计。这是模型能不能正确调用的关键。
Schema设计有三条铁律:
"order_id": "订单ID",要写"order_id": "用户的订单编号,格式为8-15位数字,例如12345"。@ToolParam(description="...", required=true) 就是干这个的。我看过太多Agent翻车,最后追查都是Schema设计问题——模型"以为"某个参数必填,结果传了null,或者"以为"某个字段是枚举,结果传了个不在schema里的字符串。
阶段3:Observation(观察)
这一步是工具的真实返回结果。它必须被注入回prompt,作为下一轮Thought的输入。
有几种特殊情况要处理:
终止条件:什么时候跳出循环
ReAct有个致命问题:它不知道什么时候停。
理论上,最终回答的开头是 Final Answer:。模型只要输出这个,就表示它认为任务完成了。
但实际上,模型经常会:
Final Answer: 但其实信息不够工程上必须加硬性终止条件。我一般设三道防线:
class="language-java">"color:#6a9955">#6a9955">// 防线1:最大循环次数int MAX_ITERATIONS = 10;if (iterations >= MAX_ITERATIONS) {return"抱歉,我处理这个问题需要的时间太长了,请换个问法。";}"color:#6a9955">#6a9955">// 防线2:模型主动终止(出现 Final Answer)if (output.startsWith("Final Answer:")) {return output.substring("Final Answer:".length()).trim();}"color:#6a9955">#6a9955">// 防线3:检测死循环(连续N次调用同一个工具)if (lastNCallsAreSame(3)) {return"看起来我卡住了。换个问题试试?";}Spring AI 2.0引入的ToolCallingAdvisor.maxToolCalls默认就是这种思路,每个工具最多调40次,总共150次上限。
Java实现:100行代码搭一个最小ReAct
光讲原理不写代码不是我的风格。下面是我去年给团队培训时写的一个简化版,核心就100行,能跑通但不做任何错误处理:
class="language-java">public class SimpleReActAgent { private final ChatModel chatModel; private final List<Tool> tools; public String run(String userQuestion) { StringBuilder history = new StringBuilder(); history.append("Question: ").append(userQuestion).append("\n");for (int i = 0; i < 10; i++) {"color:#6a9955">#6a9955">// 第一步:让模型思考 + 决定动作String prompt = buildPrompt(history.toString());String response = chatModel.call(prompt);"color:#6a9955">#6a9955">// 第二步:判断是否已经能给出最终答案if (response.contains("Final Answer:")) {return extractFinalAnswer(response); }"color:#6a9955">#6a9955">// 第三步:解析 Action,调用工具String actionJson = extractAction(response);String toolName = parseToolName(actionJson);String toolArgs = parseToolArgs(actionJson);String observation = invokeTool(toolName, toolArgs);"color:#6a9955">#6a9955">// 第四步:把 Thought/Action/Observation 加进 history history.append("Thought: ").append(extractThought(response)).append("\n"); history.append("Action: ").append(actionJson).append("\n"); history.append("Observation: ").append(observation).append("\n"); }return"达到最大循环次数,任务未完成"; }}完整的prompt模板:
class="language-java">private String buildPrompt(String history) {return""" Answer the following questions. Use the tools if needed. Tools available: - query_order(order_id: str): 查询订单状态 - query_logistics(tracking_number: str): 查询物流详情 - send_email(to: str, subject: str, body: str): 发送邮件 Format: Thought: 你的思考过程 Action: {"name": "工具名", "args": {"参数": "值"}} Observation: 工具结果(系统自动填) ... (可重复多次) Final Answer: 最终回答 Begin! """ + history + "Thought:";}这个版本看着简陋,但它把ReAct的所有核心机制都跑通了。把它接上Spring AI或者LangChain4j,加上错误处理和Token限制,就能上生产。
工程实践中的5个坑
讲完原理和最小实现,再说说我在生产环境踩过的坑。
坑1:Token爆炸
如前所述,History会随着循环增长。我有个金融客户的Agent,平均循环7轮,History占了4000+ token,加上system prompt和用户问题,很快就到8k窗口的边缘。
解法:每5轮做一次摘要,让模型把前5轮总结成一段话塞进History。
坑2:模型幻觉Observation
早期版本有个bug:模型有时会自己编Observation,假装工具返回了结果。
解法:在prompt里明确说"Observation必须由系统填,你不能在Thought后面直接写Observation"。并且解析时严格校验,只有Action:后面紧跟的内容才会被当成工具调用。
坑3:工具调用失败导致无限重试
模型看到Observation是错误信息后,经常会自动重试同一个工具,然后再次失败,再次重试...
解法:工具失败后记录次数,超过3次就强制终止循环,让模型用已有信息给出不完整但诚实的回答。
坑4:循环里的权限泄露
如果你的工具列表里有"删除用户数据""修改密码"这类敏感工具,攻击者可以构造prompt让Agent循环调用它们。
解法:Spring AI 2.0.1修的CVE-2026-59318就是这个问题的解法——给每个工具调用加maxToolCalls上限,并且让高敏感工具调用必须经过人工审批。
坑5:并行工具调用导致状态混乱
有些模型支持一次返回多个Action(比如"同时查订单和查物流")。这种并行调用在ReAct里其实是个反模式,因为后续Observation的处理逻辑会变得复杂。
解法:暂时不支持并行调用,所有Action串行执行。LangGraph4j里也是这么处理的。
Atlas的判断
讲完原理和实现,我想说几句大实话。
第一,ReAct不性感,但它是基石。2026年所有主流Agent框架(Spring AI、LangChain4j、LangGraph、AutoGen)的agent loop核心都是ReAct。换句话说,你把ReAct搞懂,再看任何框架的源码都会觉得很轻松。
第二,"循环次数"是ReAct Agent最重要的超参数。我没看到任何框架把它当成核心配置项暴露出来,Spring AI有maxToolCalls但默认值偏大(40)。生产环境我建议从10开始调,根据业务场景加压测。
第三,不要让Agent自己决定"何时停止循环"。模型说"Final Answer"不代表真的能给最终答案了。要么加硬性终止条件,要么加"自信度评分"——让模型对Final Answer打分,低于阈值就强制再查一轮。
第四,OpenAI Agents API本质上是ReAct + 自动压缩 + MCP集成 + 多Agent协调。把这些东西拆开看,没有一个是新发明。值钱的是它把这些组件做成了开箱即用的云服务。
写到这里,你应该理解ReAct机制了。如果你之前一直觉得"AI Agent很神秘",希望这篇文章能帮你祛魅——它真的就是一个循环,加上几个工程上的注意事项而已。
下周我会写一篇RAG系统的分块策略深度解析。关注「极光技术熊」,更新不迷路。
互动话题:你在用ReAct类Agent时,遇到过哪些"模型死循环"的诡异案例?是怎么解决的?评论区聊聊,让我也学习学习。
觉得有用的话,点个在看让更多Java工程师看到。
— Atlas · 极光技术熊 —















































































