下面给你一套完整的、可直接运行的 Spring Boot + Spring AI Alibaba ReAct Agent 项目代码
二、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
4.0.0
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>react-agent-demo</artifactId>
<version>1.0.0</version>
<name>react-agent-demo</name>
<description>Java ReAct Agent Demo</description>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M3</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
如果你的模型不是 OpenAI 兼容接口,可以把 spring-ai-openai-spring-boot-starter 换成对应模型实现。
三、application.yml
server:
port: 8080
spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${AI_API_KEY}
chat:
options:
model: qwen-plus
启动前配置环境变量:
export AI_API_KEY=你的模型API密钥
四、启动类
package com.example.agent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ReactAgentApplication {
public static void main(String[] args) {
SpringApplication.run(ReactAgentApplication.class, args);
}
}
五、定义工具
ReAct Agent 的核心能力来自工具。这里定义两个示例工具:订单查询 和 天气查询。
OrderTools.java
package com.example.agent.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class OrderTools {
private static final Map<String, String> MOCK_ORDERS = Map.of(
"1001", "已发货",
"1002", "待支付",
"1003", "已完成"
);
@Tool(description = "根据订单号查询订单状态")
public String queryOrderStatus(
@ToolParam(description = "订单号,例如 1001") String orderId) {
String status = MOCK_ORDERS.get(orderId);
if (status == null) {
return "未找到订单:" + orderId;
}
return "订单号:" + orderId + ",状态:" + status;
}
}
WeatherTools.java
package com.example.agent.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class WeatherTools {
private static final Map<String, String> MOCK_WEATHER = Map.of(
"北京", "晴,26℃",
"上海", "多云,24℃",
"杭州", "小雨,22℃"
);
@Tool(description = "查询指定城市的天气信息")
public String getWeather(
@ToolParam(description = "城市名称,例如 北京") String city) {
String weather = MOCK_WEATHER.get(city);
if (weather == null) {
return "暂不支持查询城市:" + city;
}
return city + "的天气:" + weather;
}
}
六、Agent 配置
AgentConfig.java
package com.example.agent.config;
import com.example.agent.tool.OrderTools;
import com.example.agent.tool.WeatherTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AgentConfig {
@Bean
public ToolCallbackProvider agentTools(OrderTools orderTools,
WeatherTools weatherTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(orderTools, weatherTools)
.build();
}
@Bean
public ChatClient chatClient(ChatModel chatModel,
ToolCallbackProvider agentTools) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个智能助手,能够通过工具查询订单状态和天气信息。" +
"请先分析用户需求,再决定是否调用工具,最后给出完整回答。")
.defaultToolCallbacks(agentTools.getToolCallbacks())
.build();
}
}
这里的关键是:
ChatClient chatModel -> tools
模型负责推理,工具负责执行。模型判断需要工具时,会自动触发工具调用。
七、Agent 服务层
ReactAgentService.java
package com.example.agent.agent;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ReactAgentService {
private final ChatClient chatClient;
public ReactAgentService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String run(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}
这个类就是 ReAct Agent 的入口。
它背后的执行逻辑是:
用户输入
↓
模型思考是否需要工具
↓
需要工具 → 调用工具 → 拿到结果 → 继续思考
↓
不需要工具 → 输出最终答案
八、Controller 接口
AgentController.java
package com.example.agent.controller;
import com.example.agent.agent.ReactAgentService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/agent")
public class AgentController {
private final ReactAgentService reactAgentService;
public AgentController(ReactAgentService reactAgentService) {
this.reactAgentService = reactAgentService;
}
@PostMapping("/chat")
public AgentResponse chat(@RequestBody AgentRequest request) {
String answer = reactAgentService.run(request.getMessage());
return new AgentResponse(answer);
}
public record AgentRequest(String message) {
}
public record AgentResponse(String answer) {
}
}
九、测试请求
启动项目后,调用接口:
curl -X POST http://localhost:8080/agent/chat
-H "Content-Type: application/json"
-d '{"message":"帮我查询订单号1001的状态"}'
返回示例:
{
"answer": "订单号1001的状态是已发货。"
}
再测试天气:
curl -X POST http://localhost:8080/agent/chat
-H "Content-Type: application/json"
-d '{"message":"北京今天天气怎么样?"}'
返回示例:
{
"answer": "北京的天气是晴,26℃。"
}
十、ReAct 执行流程
这个项目的执行流程如下:
用户输入:帮我查询订单号1001的状态
↓
ChatClient 发送请求给大模型
↓
模型判断需要调用 queryOrderStatus 工具
↓
框架自动执行 OrderTools.queryOrderStatus("1001")
↓
工具返回:订单号:1001,状态:已发货
↓
模型结合工具结果生成最终回答
↓
返回给前端
这就是一个典型的 ReAct 流程:
Thought → Action → Observation → Final Answer
你说得对,上面那版确实没有体现 ReAct 的核心循环,只是用 Spring AI 的 ChatClient 做了一次工具调用,本质上是"模型调工具",不是真正的 ReAct。
真正的 ReAct 必须显式体现:
Thought
Action
Observation
Final Answer
并且是循环执行的。
下面给你一个真正体现 ReAct 循环的 Java 版本,适合博客讲解原理。
一、ReAct 的核心循环
ReAct 不是普通工具调用,而是这样一个循环:
模型思考:我需要做什么?
模型决定调用工具:Action
系统执行工具:Observation
把 Observation 放回上下文
模型继续思考
直到模型输出 Final Answer
伪代码是这样的:
while (true) {
模型思考并输出下一步;
if (输出的是 Final Answer) {
返回最终答案;
}
if (输出的是 Action) {
执行工具;
把工具结果加入上下文;
继续下一轮循环;
}
}
这才是 ReAct。
二、项目结构
react-agent-demo/
├── ReactAgentApp.java
├── ReActAgent.java
├── Tool.java
├── ToolRegistry.java
├── tools/
│ ├── OrderQueryTool.java
│ └── WeatherQueryTool.java
└── LlmClient.java
三、工具接口
Tool.java
public interface Tool {
String getName();
String getDescription();
String execute(String input);
}
每个工具都要实现这个接口。
四、工具注册中心
ToolRegistry.java
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ToolRegistry {
private final Map<String, Tool> tools = new HashMap<>();
public ToolRegistry(List<Tool> toolList) {
for (Tool tool : toolList) {
tools.put(tool.getName(), tool);
}
}
public Tool get(String name) {
return tools.get(name);
}
public String buildToolDescription() {
StringBuilder sb = new StringBuilder();
for (Tool tool : tools.values()) {
sb.append("- ")
.append(tool.getName())
.append(": ")
.append(tool.getDescription())
.append("n");
}
return sb.toString();
}
}
它负责:
注册工具
查找工具
生成工具描述给模型
五、订单查询工具
OrderQueryTool.java
public class OrderQueryTool implements Tool {
@Override
public String getName() {
return "query_order";
}
@Override
public String getDescription() {
return "根据订单号查询订单状态,输入参数是订单号,例如 1001";
}
@Override
public String execute(String input) {
if ("1001".equals(input)) {
return "订单1001状态:已发货";
}
if ("1002".equals(input)) {
return "订单1002状态:待支付";
}
return "未找到订单:" + input;
}
}
六、天气查询工具
WeatherQueryTool.java
public class WeatherQueryTool implements Tool {
@Override
public String getName() {
return "get_weather";
}
@Override
public String getDescription() {
return "查询城市天气,输入参数是城市名,例如 北京";
}
@Override
public String execute(String input) {
if ("北京".equals(input)) {
return "北京天气:晴,26℃";
}
if ("上海".equals(input)) {
return "上海天气:多云,24℃";
}
return "暂不支持查询城市:" + input;
}
}
七、ReAct 核心 Agent
ReActAgent.java
import java.util.ArrayList;
import java.util.List;
public class ReActAgent {
private final LlmClient llmClient;
private final ToolRegistry toolRegistry;
private static final int MAX_STEPS = 5;
public ReActAgent(LlmClient llmClient, ToolRegistry toolRegistry) {
this.llmClient = llmClient;
this.toolRegistry = toolRegistry;
}
public String run(String question) {
List<String> history = new ArrayList<>();
history.add("Question: " + question);
for (int step = 1; step <= MAX_STEPS; step++) {
String prompt = buildPrompt(history);
System.out.println("========== Step " + step + " ==========");
System.out.println("Prompt:n" + prompt);
String llmOutput = llmClient.call(prompt);
System.out.println("LLM Output:n" + llmOutput);
if (llmOutput.contains("Final Answer:")) {
return extractFinalAnswer(llmOutput);
}
if (llmOutput.contains("Action:")) {
String action = extractAction(llmOutput);
String actionInput = extractActionInput(llmOutput);
Tool tool = toolRegistry.get(action);
if (tool == null) {
history.add("Observation: 工具不存在:" + action);
continue;
}
String observation = tool.execute(actionInput);
history.add("Thought: " + extractThought(llmOutput));
history.add("Action: " + action);
history.add("Action Input: " + actionInput);
history.add("Observation: " + observation);
continue;
}
history.add("Thought: " + llmOutput);
}
return "超过最大推理步数,未能得到最终答案。";
}
private String buildPrompt(List<String> history) {
StringBuilder sb = new StringBuilder();
sb.append("你是一个 ReAct Agent。n");
sb.append("你可以通过工具解决用户问题。nn");
sb.append("可用工具:n");
sb.append(toolRegistry.buildToolDescription());
sb.append("n");
sb.append("你必须按以下格式输出:nn");
sb.append("Thought: 思考下一步该做什么n");
sb.append("Action: 工具名称n");
sb.append("Action Input: 工具输入参数nn");
sb.append("或者:nn");
sb.append("Thought: 我已经获得足够信息n");
sb.append("Final Answer: 最终答案nn");
sb.append("历史对话:n");
for (String line : history) {
sb.append(line).append("n");
}
return sb.toString();
}
private String extractFinalAnswer(String output) {
return output.substring(output.indexOf("Final Answer:") + "Final Answer:".length())
.trim();
}
private String extractThought(String output) {
return extractBetween(output, "Thought:", "Action:");
}
private String extractAction(String output) {
return extractBetween(output, "Action:", "Action Input:");
}
private String extractActionInput(String output) {
if (output.contains("Action Input:")) {
return output.substring(output.indexOf("Action Input:") + "Action Input:".length())
.trim();
}
return "";
}
private String extractBetween(String text, String start, String end) {
int startIndex = text.indexOf(start);
if (startIndex == -1) {
return "";
}
startIndex += start.length();
int endIndex = text.indexOf(end, startIndex);
if (endIndex == -1) {
return text.substring(startIndex).trim();
}
return text.substring(startIndex, endIndex).trim();
}
}
这个类就是 ReAct 的核心。
它的关键逻辑是:
把问题放进 history
构造 prompt 发给模型
解析模型输出
如果是 Final Answer,结束
如果是 Action,执行工具
把 Observation 放回 history
继续下一轮
八、LLM 客户端
LlmClient.java
public interface LlmClient {
String call(String prompt);
}
这里先写一个模拟实现,方便博客演示。
MockLlmClient.java
public class MockLlmClient implements LlmClient {
private int callCount = 0;
@Override
public String call(String prompt) {
callCount++;
if (callCount == 1) {
return "Thought: 用户想查询订单1001的状态,我需要调用订单查询工具。n" +
"Action: query_ordern" +
"Action Input: 1001";
}
return "Thought: 我已经获得了订单状态信息,可以回答用户。n" +
"Final Answer: 订单1001的状态是已发货。";
}
}
真实项目中,这里应该调用大模型接口,比如 OpenAI、通义千问、DeepSeek、GLM 等。
九、启动类
ReactAgentApp.java
import java.util.List;
public class ReactAgentApp {
public static void main(String[] args) {
ToolRegistry toolRegistry = new ToolRegistry(
List.of(
new OrderQueryTool(),
new WeatherQueryTool()
)
);
LlmClient llmClient = new MockLlmClient();
ReActAgent agent = new ReActAgent(llmClient, toolRegistry);
String answer = agent.run("帮我查询订单1001的状态");
System.out.println("最终答案:" + answer);
}
}
十、运行效果
输出大致如下:
========== Step 1 ==========
Prompt:
你是一个 ReAct Agent。
你可以通过工具解决用户问题。
可用工具:
query_order: 根据订单号查询订单状态,输入参数是订单号,例如 1001
get_weather: 查询城市天气,输入参数是城市名,例如 北京
你必须按以下格式输出:
Thought: 思考下一步该做什么
Action: 工具名称
Action Input: 工具输入参数
或者:
Thought: 我已经获得足够信息
Final Answer: 最终答案
历史对话:
Question: 帮我查询订单1001的状态
LLM Output:
Thought: 用户想查询订单1001的状态,我需要调用订单查询工具。
Action: query_order
Action Input: 1001
执行工具:query_order,输入:1001
========== Step 2 ==========
Prompt:
...
历史对话:
Question: 帮我查询订单1001的状态
Thought: 用户想查询订单1001的状态,我需要调用订单查询工具。
Action: query_order
Action Input: 1001
Observation: 订单1001状态:已发货
LLM Output:
Thought: 我已经获得了订单状态信息,可以回答用户。
Final Answer: 订单1001的状态是已发货。
最终答案:订单1001的状态是已发货。
十一、博客里可以这样解释
这个版本真正体现了 ReAct 的循环:
用户提问
↓
模型思考
↓
模型决定调用工具
↓
系统执行工具
↓
工具结果进入上下文
↓
模型继续思考
↓
输出 Final Answer
它和 Spring AI ChatClient 自动工具调用的区别是:
方式 特点
Spring AI 自动工具调用 框架帮你处理循环,代码简洁
手写 ReAct 循环 能清楚看到 Thought、Action、Observation、Final Answer
十二、真实项目改造建议
如果要接真实大模型,只需要替换 LlmClient:
public class QwenLlmClient implements LlmClient {
@Override
public String call(String prompt) {
// 调用通义千问、OpenAI、DeepSeek 等接口
return "模型返回内容";
}
}
然后在启动类里替换:
LlmClient llmClient = new QwenLlmClient();
这样整套 ReAct Agent 就能跑起来了。