本章节将通过从零手搓一套 SimpleReactAgent(基于原生Spring AI来实现),带你深入理解 ReAct 在真实工程环境中的工作原理。
相比直接使用现成的 Agent 框架,这种方式能够让你清楚地看到:
- 模型每一轮是如何做决策的
- 工具调用是如何被触发和执行的
- ReAct 循环究竟是由模型驱动,还是由代码驱动
当你真正走完整个实现过程后,ReAct 将不再是一个“黑盒概念”,而是一套你可以随意拆解和重构的工程模式。
在真正动手写代码之前,有一个非常重要的问题需要先回答清楚:
一个 ReAct Agent,在工程结构上到底由哪些核心模块组成?
为此,我们可以先结合之前的 ReAct Agent 结构图,从整体视角再理解一下完整的执行流程。

构成组件
在明确了 ReAct Agent 的整体结构之后,我们可以开始从代码层面定义 SimpleReactAgent 的核心组成。 结合前面结构图中展示的执行逻辑,一个基础且完整的 ReAct Agent 至少需要包含以下几类核心要素: - 用户输入(Query)与行为约束(Prompt) - 用于推理与决策的 Model - 用于执行具体动作的 Tools 基于这一结构,我们首先在 SimpleReactAgent 中定义对应的核心属性:
public
你是一个严格遵循
}
接下来我们设计一下我们的系统提示词 REACT_AGENT_SYSTEM_PROMPT,定义它的角色,调用规则,输出要求、规则等等。
public
## 角色
你是一个严格遵循
## 工具调用规则(极其重要)
## 工具执行结果
系统会自动将工具执行结果作为
## 最终答案规则
## 强制要求(必须遵守)
## 反思机制
如果在反思过程中,助手判断当前回答未能完全满足用户问题,或者达到最大反思轮次,你必须遵循以下规则:
我们可以看出这个提示词,他的核心就是,让大模型在输出的时候,如果是需要调工具则需要输出 tool_call 字段,然后工具的执行结果,由我们程序自己会注入到上下文之中,当没有 tool_call 的时候说明不需要再调用工具了,输出即结论。 接下来就是初始化了,我们同样需要构造出 ChatClient 才可以。
private
}
这个地方就有个重点概念,就是 internalToolExecutionEnabled。
internalToolExecutionEnabled
在初始化 ChatClient 时,有一个参数虽然看起来只是一个配置项,但实际上对 Agent 的整体行为具有决定性影响,这个参数就是 internalToolExecutionEnabled。在前面的课程中我们已经介绍过 Function Call 的基本机制。默认情况下,ChatClient 内部是具备自动工具调用能力的:当模型输出 ToolCall 后,框架会自动完成工具匹配、执行以及结果注入,并继续后续流程。这种模式对于单轮、一次性的工具调用场景非常友好,但它隐含了一个前提——工具调用只是模型推理过程中的内部实现细节。而 ReAct 并不是这样工作的。在 ReAct 模式下,工具调用不再是一次性的行为,而是 Agent 明确调度的一步执行动作。Agent 需要清楚地知道:当前是否进入了工具阶段、调用了哪些工具、工具执行完成后是否要继续下一轮推理。如果仍然依赖 ChatClient 的内部自动执行机制,这些关键的执行边界都会被框架吞掉,Agent 也就失去了对整个流程的掌控能力。因此,在 SimpleReactAgent 中我们需要显式关闭 ChatClient 的内部工具执行逻辑:
ToolCallingChatOptions
这一行配置的含义就是:模型只负责表达“我想调用什么工具”,而不再负责调用工具本身。工具什么时候被调用、调用多少次、调用完成后如何处理结果,全部交由开发者代码显式控制。也正是通过这一点,你才能将原本框架自动调用工具的执行模式,转变为多轮决策与行动的 ReAct 模式。 可以说,internalToolExecutionEnabled(false) 是整个 SimpleReactAgent 能够成立的前提条件。没有它,你得到的只会是一个会使用工具的 ChatBot;而有了它,Agent 才真正掌握了行动控制权,从而具备实现完整 ReAct 循环的能力。
主流程
下面的代码就是展示了一个完整的 React 流程。 是的,你没有看错:实现一套基础且完整的 ReAct 模式,核心主流程代码只需要 100 行。
/**
public
}
// 带会话id
public
}
public
messages
messages
messages
messages
chatMemory
round
log
messages
你已达到最大推理轮次限制。
请基于当前已有的上下文信息,
直接给出最终答案。
禁止再调用任何工具。
如果信息不完整,请合理总结和说明。
chatMemory
messages
chatResponse
log
messages
toolCall
log
result
safeJson
messages
}
我们再从结构图的视角重新审视这段代码,会发现 ReAct 的三个核心阶段在这里一一对应: - Reasoning由模型完成:chatClient.prompt().messages(messages).call() - Act通过hasToolCalls()判断是否进入工具阶段,并解析模型给出的 ToolCall,然后调用ToolCallback.call 方法,实现自主工具调用。 - Observation执行完工具,并将 ToolResponseMessage 注入回 messages 而整个 ReAct 的“循环”本身,并不依赖任何框架,而是由一个最简单的while(true)循环来驱动。 这一点非常重要,也说明了 ReAct 不是一种模型能力,而是一种由代码驱动的执行模式。
构建上下文
List
boolean
// ===== 加载历史记忆 =====
if
messages
}
// ===== 加载 System Prompt(仅新会话,防止重复)=====
if
messages
messages
}
messages
ReAct 的一切,都是从上下文开始的。messages不仅仅是聊天记录,也是 Agent 的状态容器。它完整保存了:历史会话记忆、React 模式提示词、系统提示词、用户问题、工具决策tool_calls、工具执行结果。messages会在整个 ReAct 循环中不断地被扩展。
进入循环
while
round
log
messages
你已达到最大推理轮次限制。
请基于当前已有的上下文信息,
直接给出最终答案。
禁止再调用任何工具。
如果信息不完整,请合理总结和说明。
进入循环首先会有一个maxRounds的判断,如果maxRounds设置的是小于等于0的则表示无限制循环,接下来会首先在每一轮开始,判断当前迭代轮次是否已经超过maxRounds了,如果达到,则强制利用当前的迭代信息输出答案。这边有个ensureToolCallsClosed需要注意,这是一个坑,Openai规范要求,必须带有tool_call的AssistantMessage后面跟着的是ToolResponseMessage,否则就会报错400,所以这个地方,我们需要做一个兼容性的处理,给最后一个tool_call拼上一个空的结果即可。 org.springframework.ai.retry.NonTransientAiException: 400 - {"error":{"message":"<400> InternalError.Algo.InvalidParameter: An assistant message with \"tool_calls\" must be followed by tool messages responding to each \"tool_call_id\". The following tool_call_ids did not have response messages: message[7].role","type":"invalid_request_error","param":null,"code":"invalid_parameter_error"},"id":"chatcmpl-1714d553-c4c9-40ba-be9c-fc7101c593a0","request_id":"1714d553-c4c9-40ba-be9c-fc7101c593a0"}
private
responses
messages
}
接下来就是正式进入循环,首先需要调用一次模型请求,主要目的有两个: - 把当前完整上下文交给模型 - 让模型基于“已知信息”做一次决策
读取模型决策结果
模型的决策结果会有两种形态返回,一种是带有 tool_call 的表示还需要继续调用工具,另一种是不带有 tool_call 的,表示目前的信息不需要调用工具了,可以直接返回最终结论了。
String
AssistantMessage
if
}
调用工具
messages
chatResponse
result
messages
}
在模型返回 ToolCall 之后,Agent 并不会立刻执行工具,而是先将包含 ToolCall 的 AssistantMessage 追加到 messages 中。这一步的目的,是把模型本轮的“行动决策”补充为上下文的一部分,使得后续的推理过程可以完整感知自己已经做过哪些尝试。ToolCall 在这里并不代表执行结果,而仅仅是模型表达出来的行动意图,只有当这一意图被记录进上下文后,整个 ReAct 的状态才是连续且可回溯的。随后,Agent 遍历所有 ToolCall,显式查找并调用对应的工具实现,工具执行过程中出现的异常也由 Agent 统一兜底处理。每一次工具调用的真实结果,都会被封装为 ToolResponseMessage 并再次写入 messages,作为下一轮推理的 Observation 输入给模型。通过这种方式,模型始终只负责决策是否行动,而 Agent 则完整掌控行动的执行与结果回流,从而在一个 while 循环中自然地形成了 ReAct 的 Act → Observation 闭环。
Builder
为了方便使用,这里采用 Builder 模式来构建 SimpleReactAgent,一方面避免构造函数参数过多带来的可读性问题,另一方面也方便在不影响主流程的情况下逐步扩展 Agent 能力。
public
// if (tools == null || tools.isEmpty()) {
// throw new IllegalArgumentException("tools 不能为空!");
// }
}
效果演示
这边我准备了2个工具,一个工具是查天气,一个是搜索引擎(两个工具返回的数据都是模拟数据)
public
opts
opts
opts
请你根据北京今天的天气、未来七天的天气趋势、以及上海今天的天气,并搜索北京天气的预警情况,生成一份不少于
}
再尝试下历史记忆:

总结
整体来看,SimpleReactAgent 的结构非常直观:messages 用来承载整个对话和状态,ChatModel 只负责做决策,ToolCallback 提供具体的执行能力,通过 Agent 显式控制工具的调用与结果回传,在一个简单的循环中就完成了完整的 ReAct 推理过程。后续的课程中,我也会继续带大家一步步增强这个 Agent,让大家在理解其结构清晰性的同时,也能切身体会到它良好的扩展能力。虽然在具体实现上与 Alibaba 官方的 ReactAgent 在智能体结构、调度方式和状态管理等方面存在一些差异,但在核心思想上是一致的:都遵循“模型负责思考,Agent 负责行动”的原则。因此,一旦你理解了 SimpleReactAgent 的设计,再去看官方实现或其他 ReAct 框架,整体使用和理解成本都会非常低。