A08.手把手教你实现 ReAct 智能体编程范式.md
Spring AI 实战:手把手教你实现 ReAct 智能体编程范式

摘要:本文通过一个完整的 Spring AI 项目实例,深入浅出地讲解 ReAct(Reasoning + Acting)智能体编程范式的核心工作原理。我们将从零开始实现一个轻量级的 ReAct Agent,并通过流式处理优化用户体验,让你彻底掌握智能体开发的核心技术。
📖 目录
什么是 ReAct 范式?
ReAct(Reasoning + Acting)是一种先进的智能体编程范式,它将推理(Reasoning)和行动(Acting)有机结合,让 AI 系统能够像人类一样思考并执行任务。

图 1: ReAct 核心循环流程图 - Thinking → Acting → Observing 顺时针闭环,展示智能体的基础运行模型
ReAct的核心循环
┌─────────────┐
│ Thinking │ ← 分析当前情况,决定下一步行动
└──────┬──────┘
│
▼
┌─────────────┐
│ Acting │ ← 调用工具执行具体操作
└──────┬──────┘
│
▼
┌─────────────┐
│ Observing │ ← 观察工具执行结果
└──────┬──────┘
│
└──────→ 返回 Thinking,继续下一轮循环
为什么需要 ReAct?
传统的 AI 对话系统存在以下局限:
- ❌ 无法主动获取外部信息
- ❌ 无法执行实际操作
- ❌ 缺乏逻辑推理链条
- ❌ 难以处理复杂多步任务
而 ReAct 范式通过思考 - 行动 - 观察的循环机制,完美解决了这些问题。
项目架构总览
让我们先看看项目的整体结构:
├── A06Application.java # Spring Boot 主应用
├── advisor/
│ └── MyLoggingAdvisor.java # 自定义日志顾问
└── react/
├── service/
│ └── LlmService.java # LLM 服务封装
├── simple/
│ ├── CalculatorTools.java # 计算器工具集
│ ├── SimpleReActAgent.java # 轻量级 ReAct 实现
│ └── SimpleReActRunner.java # 演示运行器
└── stream/
├── StreamReActAgent.java # 流式 ReAct 实现
└── StreamReActRunner.java # 流式演示运行器
技术栈
- 框架: Spring Boot + Spring AI
- 模型: 支持多种大模型(默认使用 Qwen2.5-7B-Instruct)
- 工具: 基于
ToolCallback机制的工具调用系统 - 响应式: Reactor(流式处理)
核心组件详解
1️⃣ 工具定义:CalculatorTools
首先,我们需要定义一些工具供 AI 调用(也用于后续的测试示例)。 这里提供了几个基本的计算和一个模拟的天气查询工具,通过SpringAI的@Tool进行工具声明定义
package com.git.hui.springai.app.react.simple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import java.util.List;
public class CalculatorTools {
private static final Logger log = LoggerFactory.getLogger(CalculatorTools.class);
@Tool(description = "执行加法运算,返回两个数的和")
public double add(@ToolParam(description = "第一个加数") double a,
@ToolParam(description = "第二个加数") double b) {
log.debug("[🔨] 执行加法:{} + {}", a, b);
return a + b;
}
@Tool(description = "执行减法运算,返回两个数的差")
public double subtract(@ToolParam(description = "被减数") double a,
@ToolParam(description = "减数") double b) {
log.debug("[🔨] 执行减法:{} - {}", a, b);
return a - b;
}
@Tool(description = "执行乘法运算,返回两个数的积")
public double multiply(@ToolParam(description = "第一个乘数") double a,
@ToolParam(description = "第二个乘数") double b) {
log.debug("[🔨] 执行乘法:{} * {}", a, b);
return a * b;
}
@Tool(description = "执行除法运算,返回两个数的商")
public double divide(@ToolParam(description = "被除数") double a,
@ToolParam(description = "除数") double b) {
log.debug("[🔨] 执行除法:{} / {}", a, b);
if (b == 0) {
throw new ArithmeticException("除数不能为零");
}
return a / b;
}
@Tool(description = "查询天气信息")
public String weather(@ToolParam(description = "城市名称") String city) {
log.debug("[🔨] 执行天气查询:{}", city);
List<String> temperatures = List.of("25°C", "27°C", "23°C", "21°C", "19°C");
List<String> weathers = List.of("晴天", "阴天", "雨天", "雷雨", "雪天");
return "当前" + city + "的天气为:" +
weathers.get((int) (Math.random() * weathers.size())) +
" ,气温为:" + temperatures.get((int) (Math.random() * temperatures.size()));
}
/**
* 获取所有工具回调
*/
public List<ToolCallback> getTools() {
ToolCallback[] toolCallbacks = MethodToolCallbackProvider.builder()
.toolObjects(this)
.build()
.getToolCallbacks();
return List.of(toolCallbacks);
}
}
关键点解析:
@Tool注解:标记这是一个工具方法,description会被大模型理解@ToolParam注解:描述参数含义,帮助大模型正确使用MethodToolCallbackProvider:自动将方法转换为工具回调- 工具设计原则:单一职责、明确描述、类型安全
2️⃣ 自定义日志顾问:MyLoggingAdvisor
为了更好地观察 ReAct 过程,我们实现了一个自定义的日志顾问,用于请求前后交互信息
完整的代码实现请到文末的项目源码进行查看
public class MyLoggingAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
StringBuilder sb = new StringBuilder("\n[log] USER INPUT⬇️:");
if (this.showSystemMessage && chatClientRequest.prompt().getSystemMessage() != null) {
sb.append("\n [log] SYSTEM: ").append(first(chatClientRequest.prompt().getSystemMessage().getText(), 300));
}
if (this.showAvailableTools) {
Object tools = "No Tools";
if (chatClientRequest.prompt().getOptions() instanceof ToolCallingChatOptions toolOptions) {
tools = toolOptions.getToolCallbacks().stream().map(tc -> tc.getToolDefinition().name()).toList();
}
sb.append("\n [log] TOOLS: ").append(ModelOptionsUtils.toJsonString(tools));
}
List<Message> msgList = chatClientRequest.prompt().getInstructions();
Message lastMessage = null;
for (int i = msgList.size() - 1; i >= 0; i--) {
Message message = msgList.get(i);
if (message instanceof UserMessage || message instanceof ToolResponseMessage) {
lastMessage = message;
break;
}
}
if (lastMessage == null) {
lastMessage = new UserMessage("");
}
if (lastMessage.getMessageType() == MessageType.TOOL) {
ToolResponseMessage toolResponseMessage = (ToolResponseMessage) lastMessage;
for (var toolResponse : toolResponseMessage.getResponses()) {
var tr = toolResponse.name() + ": " + first(toolResponse.responseData(), 1000);
sb.append("\n [log] TOOL-RESPONSE: ").append(tr);
}
} else if (lastMessage.getMessageType() == MessageType.USER) {
if (StringUtils.hasText(lastMessage.getText())) {
sb.append("\n [log] TEXT: ").append(first(lastMessage.getText(), 1000));
}
}
log.debug("[log] before: {}", sb);
return chatClientRequest;
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
StringBuilder sb = new StringBuilder("\nASSISTANT: ");
if (chatClientResponse.chatResponse() == null || chatClientResponse.chatResponse().getResults() == null) {
sb.append(" [log] No chat response ");
log.debug("[log] after: {}", sb);
return chatClientResponse;
}
for (var generation : chatClientResponse.chatResponse().getResults()) {
var message = generation.getOutput();
if (message.getToolCalls() != null) {
for (var toolCall : message.getToolCalls()) {
sb.append("\n [log] TOOL-CALL: ")
.append(toolCall.name())
.append(" (")
.append(toolCall.arguments())
.append(")");
}
}
if (message.getText() != null) {
if (StringUtils.hasText(message.getText())) {
sb.append("\n [log] TEXT: ").append(first(message.getText(), 1200));
}
}
}
log.debug("[log] after: {}", sb.toString().replaceAll("\n", "\t"));
return chatClientResponse;
}
}
作用:
- ✅ 记录每次请求的完整上下文
- ✅ 追踪工具调用链
- ✅ 调试和排查问题
3️⃣ LLM 服务封装:LlmService
统一管理 ChatClient 的创建和配置(支持根据切换使用不同的模型进行响应)
@Service
public class LlmService {
@Autowired
private ChatModel chatModel;
private Map<String, ChatClient> chatClientMap = new ConcurrentHashMap<>();
public ChatClient getChatClient(String modelName) {
if (StringUtils.isBlank(modelName)) {
modelName = "Qwen/Qwen2.5-7B-Instruct";
}
return chatClientMap.computeIfAbsent(modelName, name ->
ChatClient.builder(chatModel)
.defaultOptions(ChatOptions.builder().model(modelName).build())
.defaultAdvisors(
MyLoggingAdvisor.builder()
.showAvailableTools(true)
.showSystemMessage(true)
.build())
.build()
);
}
}
实现轻量级 ReAct Agent
这是整个项目的核心部分!让我们逐行分析 SimpleReActAgent 的实现。也希望大家可以基于这个简单的实现来理解ReAct的核心理念
核心数据结构
public class SimpleReActAgent {
private static final int MAX_ITERATIONS = 10; // 最大迭代次数
private final ChatClient chatClient; // 聊天客户端
private final List<ToolCallback> tools; // 可用工具列表
public SimpleReActAgent(ChatClient chatClient, List<ToolCallback> tools) {
this.chatClient = chatClient;
this.tools = tools != null ? tools : new ArrayList<>();
}
}
主流程:ReAct 循环
public String run(String question) {
// 1. 初始化对话历史
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage(question));
// 2. ReAct 循环
for (int iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
log.info("🔁 第 {} 轮思考", iteration);
try {
// Thinking: 让大模型思考下一步该做什么
ChatResponse response = think(messages);
AssistantMessage assistantMessage = response.getResult().getOutput();
// Act & Observe: 检查是否需要调用工具
if (hasToolCalls(assistantMessage)) {
// 执行工具调用
String toolResult = executeTools(assistantMessage);
// 将工具结果添加到对话历史
var toolCall = getToolCall(assistantMessage);
messages.add(ToolResponseMessage.builder()
.responses(List.of(new ToolResponseMessage.ToolResponse(
toolCall.id(), toolCall.name(), toolResult)))
.build());
log.info("工具执行结果 {} => {}", toolCall.name(), toolResult);
} else {
// 没有工具调用,说明已给出最终答案
String finalAnswer = assistantMessage.getText();
log.info("✅ 无工具调用,直接返回结果:{}", finalAnswer);
return finalAnswer;
}
} catch (Exception e) {
log.error("ReAct 循环出错:{}", e.getMessage(), e);
break;
}
}
return "达到最大迭代次数 (" + MAX_ITERATIONS + "),无法完成任务。";
}
流程解析:
- 初始化:将用户问题加入对话历史
- 循环开始:设置最大迭代次数防止无限循环
- Thinking:调用大模型,让它思考下一步
- 判断:检查是否有工具调用
- ✅ 有 → 执行工具,记录结果,继续下一轮
- ❌ 无 → 返回最终答案
- 异常处理:捕获错误,安全退出
请注意,上面这里判断是否结束是根据是否有工具调用来的,在真实的业务场景中,这样的方式显然不太友好,理应设计一套更符合实际场景的Observe(比如让大模型审查构建的返回是否满足需要,再多一轮对话让大模型整理输出结果等)
Thinking 阶段:构建系统提示
这是智能体的“内心独白”。它会分析当前情况、分解任务、制定下一步计划,或者反思上一步的结果。
private ChatResponse think(List<Message> messages) {
// 构建系统提示
String systemPrompt = """
你是一个智能助手,使用 ReAct 范式解决问题。
请按以下步骤思考:
1. 分析当前问题和已有信息
2. 判断是否需要使用工具获取更多信息
3. 如果需要工具,调用一个合适的工具
4. 【重要】如果已有足够信息,直接给出最终答案
响应规则:
- 你必须一次只调用一个工具,不允许同时调用多个工具
- 关键:调用工具时,你必须使用工具定义中完整的工具名称
- 【重要】如果之前的工具执行已经提供了足够的信息来回答原始问题,
不要再调用另一个工具。相反,综合结果并直接提供你的最终答案
可用工具列表:\n""" + getToolDescriptions();
Message systemMessage = new SystemMessage(systemPrompt);
List<Message> allMessages = new ArrayList<>();
allMessages.add(systemMessage);
allMessages.addAll(messages);
// 设置工具调用选项(禁用自动执行)
ToolCallingChatOptions options = ToolCallingChatOptions.builder()
.internalToolExecutionEnabled(false) // 关键:手动控制工具执行
.build();
Prompt prompt = new Prompt(allMessages, options);
return chatClient.prompt(prompt)
.toolCallbacks(tools)
.call()
.chatResponse();
}
关键技巧:
- 明确的指令:告诉 AI 如何使用 ReAct 范式
- 限制条件:一次只调用一个工具,避免混乱
- 终止条件:信息足够时直接给出答案
- 禁用自动执行:
internalToolExecutionEnabled(false)让我们手动控制
Act & Observe 阶段:执行工具
这是智能体决定采取的具体动作,通常是调用一个工具,然后观察工具的执行结果,判断是否可以终止循环
private String executeTools(AssistantMessage message) throws Exception {
var toolCall = getToolCall(message); // 获取第一个工具调用
log.debug("🔧 执行工具调用");
log.debug(" 工具名称:{}", toolCall.name());
log.debug(" 工具参数:{}", toolCall.arguments());
// 查找并执行匹配的工具
for (ToolCallback tool : tools) {
if (tool.getToolDefinition().name().equals(toolCall.name())) {
Object result = tool.call(toolCall.arguments());
String resultText = result != null ? result.toString() : "null";
log.debug(" 执行结果:{}", resultText);
return resultText;
}
}
throw new RuntimeException("未找到工具:" + toolCall.name());
}
执行流程:
- 提取工具名称和参数
- 遍历工具列表找到匹配的
- 调用工具并获取结果
- 返回结果用于下一轮思考
流式处理优化
基础的 ReAct Agent 功能已经实现,但用户体验不够好——用户需要等待很长时间才能看到结果。让我们用流式处理优化它!

图 3: 流式处理 vs 非流式处理对比 - 左侧红色调展示传统方式的不足,右侧绿色调展示流式处理的优势,底部表格总结关键差异
StreamReActAgent 核心实现
public class StreamReActAgent {
public String run(String question) {
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage(question));
// ReAct 循环
for (int iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
// Thinking: 流式获取大模型的思考过程
AssistantMessage assistantMessage = thinkStreaming(messages);
if (hasToolCalls(assistantMessage)) {
String toolResult = executeTools(assistantMessage);
var toolCall = getToolCall(assistantMessage);
messages.add(ToolResponseMessage.builder()
.responses(List.of(new ToolResponseMessage.ToolResponse(
toolCall.id(), toolCall.name(), toolResult)))
.build());
} else {
return assistantMessage.getText();
}
}
return "达到最大迭代次数,无法完成任务。";
}
}
流式 Thinking 实现
private AssistantMessage thinkStreaming(List<Message> messages) {
String systemPrompt = /* ... 同上 ... */;
Message systemMessage = new SystemMessage(systemPrompt);
List<Message> allMessages = new ArrayList<>();
allMessages.add(systemMessage);
allMessages.addAll(messages);
ToolCallingChatOptions options = ToolCallingChatOptions.builder()
.internalToolExecutionEnabled(false)
.build();
Prompt prompt = new Prompt(allMessages, options);
// 使用流式响应
AtomicReference<AssistantMessage> lastMessage = new AtomicReference<>();
StringBuilder fullText = new StringBuilder();
Flux<ChatResponse> responseFlux = chatClient.prompt(prompt)
.toolCallbacks(tools)
.stream()
.chatResponse();
// 处理流式响应
responseFlux.doOnNext(response -> {
if (response.getResult() != null && response.getResult().getOutput() != null) {
AssistantMessage output = response.getResult().getOutput();
// 累积文本内容
String text = output.getText();
if (text != null && !text.isEmpty()) {
fullText.append(text);
log.info("【流式文本】{}", text);
}
// 检测工具调用
if (output.getToolCalls() != null && !output.getToolCalls().isEmpty()) {
log.info("【检测到工具调用】");
}
// 保存最后一个消息
lastMessage.set(output);
}
}).blockLast();
// 构建完整消息
if (lastMessage.get() != null) {
AssistantMessage currentMessage = lastMessage.get();
if ((currentMessage.getToolCalls() == null || currentMessage.getToolCalls().isEmpty())
&& fullText.length() > 0) {
return AssistantMessage.builder()
.content(fullText.toString())
.build();
}
}
return lastMessage.get();
}
流式处理的优势:
- ✅ 实时反馈:用户可以看到 AI 的思考过程
- ✅ 更好体验:减少等待焦虑
- ✅ 调试友好:清晰展示每一步执行
实战演示
接下来我们写个demo来看看这个基础的ReAct具体表现如何
@Component
public class SimpleReActRunner implements CommandLineRunner {
private final LlmService llmService;
private final ChatClient chatClient;
public SimpleReActRunner(LlmService llmService) {
this.llmService = llmService;
this.chatClient = llmService.getChatClient(null);
}
@Override
public void run(String... args) throws Exception {
// 1. 准备工具
CalculatorTools calculatorTools = new CalculatorTools();
List<ToolCallback> tools = calculatorTools.getTools();
// 2. 创建 ReAct Agent
SimpleReActAgent agent = new SimpleReActAgent(chatClient, tools);
// 3. 运行示例
System.out.println("\n========== 示例 1: 简单加法 ==========");
String answer1 = agent.run("计算 25 + 37 等于多少?");
System.out.println("最终结果:" + answer1);
System.out.println("\n========== 示例 2: 多步计算 ==========");
String answer2 = agent.run("先计算 100 + 50,然后将结果乘以 2,最后除以 3;\n最终根据上面这个计算返回的结果,是奇数就查询武汉天气、是偶数则查询北京天气");
System.out.println("最终结果:" + answer2);
}
}
示例 1:简单加法
其中示例1,比较简单,两轮对话即可返回最终的结果,执行记录如下
输入:计算 25 + 37 等于多少?
执行日志:
========== 示例 1: 简单加法 ==========
╔════════════════════════════════════════╗
║ 🚀 ReAct Agent 启动 ║
╚════════════════════════════════════════╝
💬 问题:计算 25 + 37 等于多少?
┌────────────────────────────────────────
│ 🔁 第 1 轮思考
└────────────────────────────────────────
[🔨] 执行加法:25.0 + 37.0
工具执行结果 add => 62.0
┌────────────────────────────────────────
│ 🔁 第 2 轮思考
└────────────────────────────────────────
✅ 无工具调用,直接返回结果:25 + 37 等于 62。
╔════════════════════════════════════════╗
║ ✨ ReAct 任务完成 ✨ ║
╚════════════════════════════════════════╝
最终结果:25 + 37 等于 62。
分析:
- AI 识别到需要加法运算
- 调用
add工具 - 返回最终答案

示例 2:多步混合运算
输入:先计算 100 + 50,然后将结果乘以 2,最后除以 3;根据计算结果,是奇数就查询武汉天气、是偶数则查询北京天气
执行日志:
========== 示例 2: 多步计算 ==========
╔════════════════════════════════════════╗
║ 🚀 ReAct Agent 启动 ║
╚════════════════════════════════════════╝
💬 问题:先计算 100 + 50,然后将结果乘以 2,最后除以 3;最终根据上面这个计算返回的结果,是奇数就查询武汉天气、是偶数则查询北京天气
┌────────────────────────────────────────
│ 🔁 第 1 轮思考
└────────────────────────────────────────
[🔨] 执行加法:100.0 + 50.0
工具执行结果 add => 150.0
┌────────────────────────────────────────
│ 🔁 第 2 轮思考
└────────────────────────────────────────
[🔨] 执行乘法:150.0 * 2.0
工具执行结果 multiply => 300.0
┌────────────────────────────────────────
│ 🔁 第 3 轮思考
└────────────────────────────────────────
[🔨] 执行除法:300.0 / 3.0
工具执行结果 divide => 100.0
┌────────────────────────────────────────
│ 🔁 第 4 轮思考
└────────────────────────────────────────
✅ 无工具调用,直接返回结果:根据计算结果,100 + 50 = 150,然后 150 × 2 = 300,最后 300 ÷ 3 = 100。100 是偶数,因此需要查询北京天气。
╔════════════════════════════════════════╗
║ ✨ ReAct 任务完成 ✨ ║
╚════════════════════════════════════════╝
最终结果:根据计算结果,100 + 50 = 150,然后 150 × 2 = 300,最后 300 ÷ 3 = 100。100 是偶数,因此需要查询北京天气。
分析:
- AI 正确分解了多步任务
- 按顺序执行:加 → 乘 → 除 → 判断奇偶 → 查询天气
- 展示了强大的逻辑推理能力
示例2相对来说更复杂一些,除了计算之外,还有天气查询的工具调用,相应的循环次数会更多一些

示例 3:复杂应用题
然后我们再扩展一下,使用一个需要使用计算的算钱的应用场景,看下这套ReAct的表现如何
System.out.println("\n========== 示例 3: 应用场景 ==========");
String answer3 = agent.run("小明有 50 元钱,买了 3 本书,每本书 12 元,花了 2.5 元买了一个包子,还剩多少钱?");
System.out.println("最终结果:" + answer3);
执行日志:
========== 示例 3: 应用场景 ==========
╔════════════════════════════════════════╗
║ 🚀 ReAct Agent 启动 ║
╚════════════════════════════════════════╝
💬 问题:小明有 50 元钱,买了 3 本书,每本书 12 元,花了 2.5 元买了一个包子,还剩多少钱?
┌────────────────────────────────────────
│ 🔁 第 1 轮思考
└────────────────────────────────────────
[🔨] 执行乘法:3.0 * 12.0
工具执行结果 multiply => 36.0
┌────────────────────────────────────────
│ 🔁 第 2 轮思考
└────────────────────────────────────────
[🔨] 执行减法:50.0 - 36.0
工具执行结果 subtract => 14.0
┌────────────────────────────────────────
│ 🔁 第 3 轮思考
└────────────────────────────────────────
✅ 无工具调用,直接返回结果:小明一开始有 50 元钱。他买了 3 本书,每本书 12 元,总共花费了 $3 \times 12 = 36$ 元。他还花了 2.5 元买了一个包子。因此,他总共花费了 $36 + 2.5 = 38.5$ 元。
小明剩下的钱为 $50 - 38.5 = 11.5$ 元。
最终答案:小明还剩 11.5 元。
╔════════════════════════════════════════╗
║ ✨ ReAct 任务完成 ✨ ║
╚════════════════════════════════════════╝
最终结果:小明一开始有 50 元钱。他买了 3 本书,每本书 12 元,总共花费了 $3 \times 12 = 36$ 元。他还花了 2.5 元买了一个包子。因此,他总共花费了 $36 + 2.5 = 38.5$ 元。
小明剩下的钱为 $50 - 38.5 = 11.5$ 元。
最终答案:小明还剩 11.5 元。
分析:
- AI 理解了应用题的场景
- 第一步:计算总花费(3 × 12 = 36)
- 第二步:计算剩余(50 - 36 = 14)
- 第三步:返回结果,不调用工具进行结果返回

虽然上面的截图结果是准确的,但是在实际测试时,发现不同模型的表现差距还是很明显的🤣
关键技术要点总结
1. ReAct 循环三要素
| 阶段 | 作用 | 实现方式 |
|---|---|---|
| Thinking | 分析情况,决定下一步 | 调用大模型,传入对话历史和工具列表 |
| Acting | 执行具体操作 | 查找并调用匹配的工具 |
| Observing | 获取操作结果 | 捕获工具返回值,加入对话历史 |
2. 系统提示设计技巧
✅ 必须做的:
- 明确说明使用 ReAct 范式
- 列出可用工具
- 强调一次只调用一个工具
- 说明何时应该停止(信息足够时直接回答)
❌ 避免的:
- 模糊的指令
- 允许多工具并行调用
- 没有明确的终止条件
3. 工具调用控制
禁用自动执行是关键:
ToolCallingChatOptions options = ToolCallingChatOptions.builder()
.internalToolExecutionEnabled(false) // 👈 手动控制
.build();
这样做的好处:
- ✅ 完全掌控执行流程
- ✅ 可以记录和调试
- ✅ 灵活处理异常情况
4. 对话历史管理
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage(question)); // 用户问题
messages.add(systemMessage); // 系统提示
messages.add(assistantMessage); // AI 思考
messages.add(toolResponseMessage); // 工具结果
// ... 不断追加,形成完整上下文
5. 流式处理核心价值
| 对比项 | 非流式 | 流式 |
|---|---|---|
| 用户等待时间 | 长 | 短(实时可见) |
| 调试友好度 | 一般 | 优秀 |
| 实现复杂度 | 简单 | 稍复杂 |
| 推荐场景 | 快速原型 | 生产环境 |
总结与展望
本文核心收获
本文通过一个实现了一个基础的ReActAgent来演示现在Agent中广为使用的ReAct范式,其核心思想是模仿人类解决问题的方式,将推理和行动结合起来,然后通过代码的方式进行演绎。虽然我们实现的还比较初级,但通过这个项目,至少会有一些几个收获:
- 理解了 ReAct 范式:Thinking → Acting → Observing 循环
- 掌握了实现方法:基于Spring AI 手动控制工具调用
- 学会了流式优化:提升用户体验的关键技巧
- 获得了实战经验:完整的代码示例和调试日志
进一步学习方向
如果我们想跟进一步的打造一个生产可用的ReActAgent,那么下面这些是我们不能绕过的点
- 更复杂的工具集:集成真实 API(天气、地图、支付等)
- 多 Agent 协作:多个 ReAct Agent 协同完成复杂任务
- 记忆系统:实现长期记忆和上下文学习
- 自我反思:让 Agent 能够评估和优化自己的行为
- 视觉能力:结合图像识别等多模态能力
思考题
尝试扩展这个项目:
- 添加一个
CalendarTools,支持日期计算和提醒设置 - 实现一个简单的搜索引擎工具,查询网络信息
- 设计一个数据库查询工具,支持 SQL 查询
- 创建一个 Web 自动化测试工具集
参考资源
项目源码
- 完整代码: https://github.com/liuyueyi/spring-ai-demo/tree/master/advance-projects/A06-ReAct-agent
- 配置管理:使用你自己的模型,替换资源文件
resources/application.yml下的大模型相关配置 - 运行方式:Spring Boot 应用直接启动
相关文档
零基础入门:
- LLM 应用开发是什么:零基础也可以读懂的科普文(极简版)
- 大模型应用开发系列教程:序-为什么你“会用 LLM”,但做不出复杂应用?
- 大模型应用开发系列教程:第一章 LLM到底在做什么?
- 大模型应用开发系列教程:第二章 模型不是重点,参数才是你真正的控制面板
- 大模型应用开发系列教程:第三章 为什么我的Prompt表现很糟?
- 大模型应用开发系列教程:第四章 Prompt 的工程化结构设计
- 大模型应用开发系列教程:第五章 从 Prompt 到 Prompt 模板与工程治理
- 大模型应用开发系列教程:第六章 上下文窗口的真实边界
- 大模型应用开发系列教程:第七章 从 “堆上下文” 到 “管理上下文”
- 大模型应用开发系列教程:第八章 记忆策略的工程化选择
- 大模型应用开发系列教程:第九章 上下文工程在企业知识库助手中的落地
实战
- 实战 | 两百行实现一个自然语言地址提取智能体
- 实战 | 基于SpringAI与大模型的零配置发票智能提取架构
- 实战 | 零基础搭建知识库问答机器人:基于SpringAI+RAG的完整实现
- 告别传统AI开发!SpringAI Agent + Skills重新定义智能应用
- Spring AI中的多轮对话艺术:让大模型主动提问获取明确需求
- 实战 | 我用SpringAI造了个「微信红包封面设计师」
推荐阅读
作者:一灰
日期:2026 年 3 月 4 日
标签:#SpringAI #ReAct #AI-Agent #Java #智能体编程
💬 互动环节:你在实践中遇到了什么问题?欢迎在评论区留言讨论!如果觉得这篇文章有帮助,请点赞 👍 收藏 ⭐ 转发 🔄 支持一下!