夜雨聆风学习资料网

ARTICLE · 1167693

你的AI Agent总在第二步死循环?ReAct机制我拆开给你看

你的AI Agent总在第二步死循环?ReAct机制我拆开给你看

你的AI Agent总在第二步死循环?ReAct机制我拆开给你看

你有没有遇到过这种情况:让AI Agent去查一下"明天北京天气怎么样,适合穿什么衣服",它第一次调用天气API成功了,然后就开始无限循环调天气。

或者更隐蔽的:模型给你返回一个看起来很有道理的回答,但其实它根本没拿到最新数据,全凭"幻觉"在编。

上周OpenAI把Agents API免费开放那天,我盯着它内部的agent loop实现看了两个小时。说实话,本质上就是那个2022年提出的老古董——ReAct。只不过这次包装得更漂亮,加了自动上下文压缩、子Agent协调、MCP内置这些新东西。

但骨架没变。

今天这篇文章,我就把ReAct的骨架拆开讲透。看完你应该能回答三个问题:

1为什么Agent要"循环"而不是"一步到位"?
2这个循环每一步都在做什么?
3作为Java工程师,怎么自己实现一个最简版本?

先说清楚: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:

注意几个关键点:

1History字段:把之前的循环结果带进来,让模型知道"我已经做了什么、看到了什么"。
2格式强制:明确告诉模型必须用 Thought:Action:Final Answer: 这几个关键字开头,不然它会乱写。
3Observation由系统填:模型自己不用瞎编Observation,必须等真实工具调用结果。

这个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设计有三条铁律:

1参数描述要具体到"闭着眼睛也能填对"。不要写"order_id": "订单ID",要写"order_id": "用户的订单编号,格式为8-15位数字,例如12345"。
2枚举值要明确列出。如果参数只能是"已发货/未发货/退货中"这三个值,schema里就要写成enum,不要让模型自由发挥。
3必填/可选要标注清楚。Spring AI的@ToolParam(description="...", required=true) 就是干这个的。

我看过太多Agent翻车,最后追查都是Schema设计问题——模型"以为"某个参数必填,结果传了null,或者"以为"某个字段是枚举,结果传了个不在schema里的字符串。

阶段3:Observation(观察)

这一步是工具的真实返回结果。它必须被注入回prompt,作为下一轮Thought的输入。

有几种特殊情况要处理:

1工具调用失败:别让模型继续往下走,要么重试要么降级。我一般会在Observation里加一行"Error: ...",让模型决定要不要换工具。
2工具返回空结果:同样要明确告诉模型"工具返回了空数组,可能查不到",让模型思考要不要换关键词。
3工具返回结果太大:截断。我一般保留前2000字符,剩下的丢到向量数据库里,让模型按需retrieve。

终止条件:什么时候跳出循环

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 · 极光技术熊 —

相关学习资料