手写 Spring AI Agent:让大模型自主规划任务,ReAct 模式全流程拆解
Spring AI 实战指南 · 进阶篇 | 一文搞懂 ReAct 核心循环、多工具编排与 Spring AI Alibaba 能力对比
先问一个问题
你在用 Spring AI 写代码的时候,有没有想过这些问题:
- 想让 AI 同时调用多个工具(比如查航班、查酒店、查天气一起执行),Tool Calling 怎么做到?
- AI 每次调用工具后,结果怎么传回去,让它基于上一步的结果决定下一步?
- 怎么防止 AI 死循环调用工具 ,或者调用次数失控?
- 原生 Spring AI 和 Spring AI Alibaba ,在 Agent 这件事上差距有多大?
如果你用过 Spring AI 的 Tool Calling ,那你其实已经懂了 60%------ReAct 就是在工具调用的基础上加了一个「思考-行动-观察的循环」。
但有个关键区别,很多人没搞明白,导致写出来的 Agent 要么死循环,要么调用乱序,要么最后根本不知道怎么对比 Spring AI Alibaba。
今天这篇文章,我把 ReAct 的原理、手写实现、多工具编排、Spring AI Alibaba 对比,一次性讲透。
📖 本文是「Spring AI 实战指南」系列进阶篇第 4 篇。建议按顺序阅读效果更佳,当然直接看也完全没问题。 🔗 系列目录 | 完整代码关注同名公众号获取
📖 目录
- [什么是 ReAct 模式](#什么是 ReAct 模式 "#%E4%B8%80-%E4%BB%80%E4%B9%88%E6%98%AF-react-%E6%A8%A1%E5%BC%8F")
- [Spring AI vs Spring AI Alibaba:Agent 能力对比](#Spring AI vs Spring AI Alibaba:Agent 能力对比 "#%E4%BA%8C-spring-ai-vs-spring-ai-alibabaagent-%E8%83%BD%E5%8A%9B%E5%AF%B9%E6%AF%94")
- 业务场景:智能出行规划助手
- 系统架构设计
- [ReAct 核心循环实现](#ReAct 核心循环实现 "#%E4%BA%94-react-%E6%A0%B8%E5%BF%83%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0")
- 多工具编排实战
- [Agent 执行追踪与调试](#Agent 执行追踪与调试 "#%E4%B8%83-agent-%E6%89%A7%E8%A1%8C%E8%BF%BD%E8%B8%AA%E4%B8%8E%E8%B0%83%E8%AF%95")
一、什么是 ReAct 模式
1.1 从"单步工具调用"到"自主规划"
传统的 Tool Calling 是单步执行:用户问 → AI 调工具 → 返回结果。面对复杂任务时,这个模式力不从心:
arduino
用户:"帮我规划下周三去上海的出行,要预订机票、酒店,顺便查一下上海的天气。"
❌ 单步工具调用做不到:需要同时调用机票查询、酒店查询、天气查询,还要综合结果给出规划建议
✅ ReAct Agent 可以:自主拆解任务 → 依次调用工具 → 汇总结果 → 给出完整规划
1.2 ReAct 是什么?
ReAct = Reasoning(推理)+ Acting(行动),是一种让 AI 自主规划和执行多步任务的框架:
┌─────────────────────────────────────────────────────────┐
│ ReAct 执行循环 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Thought │ → │ Action │ → │Observation│ │
│ │ (推理) │ │ (行动) │ │ (观察) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ↑ ↓ │
│ └────────────────────────────────┘ │
│ 循环直到完成任务 │
└─────────────────────────────────────────────────────────┘
| 阶段 | 说明 | 示例 |
|---|---|---|
| Thought(思考) | AI 分析当前状态,规划下一步 | "用户需要订机票,先查询可用航班" |
| Action(行动) | 调用具体工具执行操作 | searchFlights("北京", "上海", "2025-03-12") |
| Observation(观察) | 接收工具执行结果 | "查到 3 个航班:CA1234, MU5678..." |
1.3 ReAct vs 传统 Tool Calling
| 对比项 | 传统 Tool Calling | ReAct Agent |
|---|---|---|
| 任务复杂度 | 单步,一问一答 | 多步,自主规划 |
| 工具数量 | 通常 1-2 个 | 可以编排 N 个工具 |
| 执行策略 | 用户指定 | AI 自主决策 |
| 容错能力 | 工具失败则结束 | 失败可重试或换策略 |
| 适用场景 | 简单查询 | 复杂任务、跨系统操作 |
一句话总结:Tool Calling 是「遥控器」,按一下动一下;ReAct 是「自动驾驶」,告诉目的地,自己规划路线。
二、Spring AI vs Spring AI Alibaba:Agent 能力对比
在动手写代码之前,有必要先聊一个绕不开的问题------同样是 Spring AI 生态,原生 Spring AI 和阿里的 Spring AI Alibaba 在 Agent 这件事上差距有多大?
这对选型很重要,选错了可能要走不少弯路。
2.1 定位差异
| 框架 | 定位 | Agent 支持 |
|---|---|---|
| Spring AI | 通用 AI 集成抽象层 | ❌ 无内置 Agent,需手动实现 ReAct 循环 |
| Spring AI Alibaba | 生产级 Agentic 应用框架 | ✅ 开箱即用的 ReactAgent + 多 Agent 编排 |
原生 Spring AI 的定位更偏向基础设施 ------它把 LLM、工具调用、记忆存储等能力标准化封装好,但没有替你把"Agent 循环"这层建起来。所以才有了本文第五章那一大段手写的 ReactAgentExecutor。
Spring AI Alibaba 则在此之上多建了一层:
yaml
原生 Spring AI:
┌──────────────────────────────┐
│ ChatClient / ChatModel │ ← 基础抽象
└──────────────────────────────┘
Spring AI Alibaba:
┌──────────────────────────────────────┐
│ ReactAgent / SequentialAgent ... │ ← 高级 Agent 抽象(新增)
├──────────────────────────────────────┤
│ Graph Core 工作流运行时 │ ← 工作流引擎(新增)
├──────────────────────────────────────┤
│ Spring AI │ ← 基础抽象
└──────────────────────────────────────┘
2.2 核心能力对比
| 能力维度 | 原生 Spring AI | Spring AI Alibaba |
|---|---|---|
| ReAct 循环 | ❌ 需手动实现(本文就是在做这件事) | ✅ 内置 ReactAgent,3 行完成 |
| 多 Agent 编排 | ❌ 不支持 | ✅ Sequential / Parallel / Routing / Loop |
| 工具重试 | ❌ 需自己写 try-catch | ✅ ToolRetryInterceptor |
| 工具调用限制 | ❌ 需手动计数 | ✅ ToolCallLimitInterceptor |
| 上下文摘要压缩 | ❌ 不支持 | ✅ SummarizationHook,超长自动压缩 |
| 人在回路 | ❌ 不支持 | ✅ HumanInTheLoopHook |
| 流式 Agent 输出 | ✅ 支持(ChatClient stream) | ✅ agent.stream() + 更细粒度的事件类型 |
| 结构化输出 | ✅ .entity(Clazz) |
✅ .outputType(Clazz) |
| 状态持久化 | ✅ ChatMemory(需手动集成) | ✅ .saver(new MemorySaver()) 一行搞定 |
| 可视化调试 | ❌ 无 | ✅ Spring AI Alibaba Studio |
| MCP 协议工具 | ⚠️ 支持但生态少 | ✅ 原生集成,支持 Python 工具 |
| A2A Agent 通信 | ❌ 不支持 | ✅ Agent-to-Agent + Nacos 服务发现 |
📊 差距一目了然:Spring AI Alibaba 在 Agent 能力上是碾压级的。但这不代表原生 Spring AI 没有价值------理解底层原理,才能在上层框架出问题时快速定位。
2.3 用 Spring AI Alibaba 重写出行规划 Agent
来直观感受一下差距------同样实现本文的智能出行规划 Agent,用 Spring AI Alibaba 的写法是这样的:
依赖引入:
xml
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-agent</artifactId>
<version>1.0.0.2</version>
</dependency>
Agent 构建(对比本文第五章手写的 200+ 行):
java
@Service
public class TravelPlanAgentService {
private final ChatModel chatModel;
public TravelPlanAgentService(ChatModel chatModel) {
this.chatModel = chatModel;
}
// 🔥 3 行关键代码 vs 手写 200+ 行
public ReactAgent buildAgent() {
return ReactAgent.builder()
.name("travel-planner")
.model(chatModel)
.instruction("""
你是一个智能出行规划助手。
根据用户需求,依次查询航班、酒店和天气,给出完整出行规划。
""")
// 注册多个工具
.tools(
flightSearchTool(),
hotelSearchTool(),
weatherQueryTool()
)
// 工具失败自动重试(最多 3 次)
.interceptors(ToolRetryInterceptor.builder().maxRetries(3).build())
// 工具总调用次数上限(防止死循环)
.interceptors(ToolCallLimitInterceptor.builder().maxToolCalls(10).build())
// 对话状态持久化
.saver(new MemorySaver())
// 启用执行日志
.enableLogging(true)
.build();
}
}
调用方式:
java
// 同步调用
AssistantMessage response = agent.call("帮我规划3月12日从北京去上海的出行,住两晚,机票预算1000以内");
// 流式调用(逐步返回每一步思考和工具调用过程)
Flux<NodeOutput> stream = agent.stream("帮我规划出行");
stream.subscribe(output -> {
if (output instanceof StreamingOutput s) {
if (s.getOutputType() == OutputType.AGENT_MODEL_STREAMING) {
System.out.print(s.message().getText()); // 实时打印推理过程
}
}
});
// 带记忆的多轮对话(相同 threadId 自动共享上下文)
RunnableConfig config = RunnableConfig.builder().threadId("user_001").build();
agent.call("帮我规划去上海的出行", config);
agent.call("把机票预算改到1500", config); // 自动记住上文
2.4 那本文为什么还要手写?
既然 Spring AI Alibaba 封装得这么好,为什么本文还要从头手写 ReAct 循环?
原因很简单:理解底层,才能用好上层。
| 场景 | 推荐方案 |
|---|---|
| 学习 ReAct 原理、理解 Agent 执行机制 | ✅ 手写(本文方式) |
| 快速搭建生产级 Agent | ✅ Spring AI Alibaba ReactAgent |
| 需要定制执行逻辑(如特殊 Prompt 格式、私有协议) | ✅ 手写或基于 Spring AI Alibaba Graph Core 扩展 |
| 团队已重度使用阿里云生态(DashScope、Nacos) | ✅ Spring AI Alibaba,生态协同更顺畅 |
| 需要多 Agent 协作、工作流编排 | ✅ Spring AI Alibaba(差距最大的地方) |
💡 建议:先啃透本文的手写实现,再用 Spring AI Alibaba 的封装去做项目------你会对它每个配置项的背后逻辑都心知肚明,遇到问题不慌。
三、业务场景:智能出行规划助手
3.1 场景描述
构建一个智能出行规划助手,用户只需一句话,Agent 自动完成:
| 功能 | 说明 |
|---|---|
| 航班搜索 | 根据出发地、目的地、日期查询可用航班 |
| 价格比较 | 对比不同航班的价格和时长 |
| 酒店推荐 | 根据目的地和入住日期推荐酒店 |
| 天气查询 | 查询目的地未来天气 |
| 行程汇总 | 综合所有信息给出完整出行规划 |
3.2 用户对话示例
sql
用户输入:
"帮我规划下周三(3月12日)去上海的出行,北京出发,住两晚,预算机票不超过1000元。"
Agent 自主执行过程:
Thought 1:需要查询北京→上海的航班信息
Action 1:searchFlights("北京", "上海", "2025-03-12")
Observation 1:找到5个航班,价格680-1200元
Thought 2:需要筛选1000元以内的航班,并查询酒店
Action 2:searchHotels("上海", "2025-03-12", "2025-03-14")
Observation 2:找到10家酒店,价格200-800元/晚
Thought 3:需要查询上海天气
Action 3:getWeather("上海", "2025-03-12")
Observation 3:上海3月12-14日,晴,15-22℃
Thought 4:已有足够信息,整理出行规划
Final Answer:为您整理出行规划如下...
3.3 核心挑战
| 挑战 | 解决方案 |
|---|---|
| 任务分解 | ReAct Prompt 引导 AI 逐步规划 |
| 工具选择 | Spring AI Tool 注册机制 |
| 上下文传递 | 将每步 Observation 追加到对话历史 |
| 执行超时 | 最大步数限制 + 超时兜底 |
| 结果汇总 | Final Answer 触发条件判断 |
四、系统架构设计
4.1 整体架构
scss
┌─────────────────────────────────────────────────────────────────┐
│ 用户输入 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ ReactAgentExecutor │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ReAct 执行循环 │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Think │→ │ Act │→ │ Observe │ ← 循环 │ │
│ │ │(LLM推理) │ │(工具调用) │ │(结果收集) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ ExecutionContext(执行上下文) │ │ │
│ │ │ - 历史 Thought/Action/Observation │ │ │
│ │ │ - 当前步数 / 最大步数 │ │ │
│ │ │ - 工具调用记录 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 工具层(Tools) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 航班搜索 │ │ 酒店查询 │ │ 天气查询 │ │ 行程生成 │ │
│ │FlightTool│ │HotelTool │ │WeatherTool│ │ItineraryT│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓
┌────────────────┐
│ 最终出行规划 │
└────────────────┘
4.2 项目依赖与配置
xml
<dependencies>
<!-- Spring AI Ollama -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
yaml
# application.yml
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
model: qwen2.5:7b
options:
temperature: 0.1 # 降低随机性,让 Agent 规划更稳定
num-ctx: 8192 # 扩大上下文,支持多轮 ReAct 循环
开发环境备注:本文基于本地模型(Ollama + qwen2.5:7b)讲解,零成本可离线运行。生产环境建议替换为大参数量云端模型以获得更好的规划能力。后续示例源码使用智谱大模型验证通过。
五、ReAct 核心循环实现
这是全文最核心的部分------我们将手写一个完整的 ReAct 执行引擎。理解了这段代码,你就真正懂了 Agent 的底层运作机制。
5.1 ReAct System Prompt ------ Agent 的"大脑指令"
ReAct 的核心在于给 AI 一个清晰的"思考-行动-观察"提示词格式:
java
@Component
public class ReactPromptTemplate {
/**
* ReAct 核心 System Prompt
*
* 设计要点:
* - 明确定义 Thought/Action/Observation 的输出格式
* - 设置 Final Answer 作为终止条件
* - 限制最大步数防止死循环
*/
public static final String REACT_SYSTEM_PROMPT = """
你是一个智能出行规划 Agent,能够自主规划和执行多步任务。
你必须按照以下格式进行思考和行动:
Thought: [分析当前状态,决定下一步做什么]
Action: [调用的工具名称和参数]
Observation: [工具返回的结果,由系统填入]
重复上述步骤,直到你有足够的信息给出最终答案。
当任务完成时,用以下格式输出:
Final Answer: [完整的出行规划]
注意事项:
1. 每次只执行一个 Action
2. 基于 Observation 调整下一步策略
3. 避免重复调用相同参数的工具
4. 最多执行 10 步,超出后直接给出现有信息的总结
""";
}
5.2 执行上下文 ------ Agent 的"短期记忆"
java
/**
* ReAct 执行上下文,记录 Agent 每一步的思考、行动和观察结果
* 类似于人类的"工作记忆",在单次任务执行过程中持续累积
*/
@Data
@Builder
public class ReactExecutionContext {
private String sessionId;
private String originalTask; // 用户原始任务
private List<ReactStep> steps; // 执行历史
private int maxSteps; // 最大步数限制
private boolean completed; // 是否完成
private String finalAnswer; // 最终答案
public boolean isExceededMaxSteps() {
return steps.size() >= maxSteps;
}
/**
* 将执行历史转为对话格式,追加到下一次 LLM 调用
* 这是 ReAct 能"记住"之前做了什么的关键机制
*/
public String buildHistoryText() {
StringBuilder sb = new StringBuilder();
for (ReactStep step : steps) {
sb.append("Thought: ").append(step.getThought()).append("\n");
if (step.getAction() != null) {
sb.append("Action: ").append(step.getAction()).append("\n");
sb.append("Observation: ").append(step.getObservation()).append("\n");
}
sb.append("\n");
}
return sb.toString();
}
}
/**
* 单步执行记录
*/
@Data
@Builder
public class ReactStep {
private String thought; // AI 的推理过程
private String action; // 执行的工具调用
private String observation; // 工具返回的观察结果
private long durationMs; // 本步执行耗时
}
5.3 ReAct 执行器核心逻辑 ------ Agent 的"发动机"
java
@Service
@Slf4j
public class ReactAgentExecutor {
private final ChatClient chatClient;
private final ToolRegistry toolRegistry;
public ReactAgentExecutor(ChatClient.Builder builder, ToolRegistry toolRegistry) {
this.toolRegistry = toolRegistry;
this.chatClient = builder
.defaultSystem(ReactPromptTemplate.REACT_SYSTEM_PROMPT)
.build();
}
/**
* ========== 主入口:执行 ReAct 循环 ==========
*
* 核心流程:
* 1. 初始化执行上下文
* 2. 循环执行 Think → Act → Observe
* 3. 检测 Final Answer 终止条件
* 4. 超出最大步数则强制汇总
*/
public ReactExecutionContext execute(String task) {
// ① 初始化上下文
ReactExecutionContext context = ReactExecutionContext.builder()
.sessionId(UUID.randomUUID().toString())
.originalTask(task)
.steps(new ArrayList<>())
.maxSteps(10) // 安全阀:最多执行 10 步
.completed(false)
.build();
log.info("[ReAct] 开始执行任务:{}", task);
// ② ReAct 主循环
while (!context.isCompleted() && !context.isExceededMaxSteps()) {
ReactStep step = executeOneStep(context);
context.getSteps().add(step);
// ③ 检测终止条件:LLM 输出了 Final Answer
if (step.getThought() != null && step.getThought().contains("Final Answer:")) {
context.setCompleted(true);
context.setFinalAnswer(extractFinalAnswer(step.getThought()));
log.info("[ReAct] 任务完成,共执行 {} 步", context.getSteps().size());
}
}
// ④ 超出最大步数的兜底处理
if (!context.isCompleted()) {
context.setFinalAnswer(forceSummarize(context));
log.warn("[ReAct] 已达最大步数 {},强制汇总", context.getMaxSteps());
}
return context;
}
/**
* ========== 单步执行:Think → Act → Observe ==========
*/
private ReactStep executeOneStep(ReactExecutionContext context) {
long startTime = System.currentTimeMillis();
// --------- Phase 1: Think ------ 让 LLM 进行推理 ---------
String prompt = buildStepPrompt(context);
String llmResponse = chatClient.prompt()
.user(prompt)
.tools(toolRegistry.getAllTools()) // 注册可用工具列表
.call()
.content();
// 解析 LLM 输出中的 Thought 和 Action
String thought = parseThought(llmResponse);
String actionText = parseAction(llmResponse);
// --------- Phase 2 & 3: Act + Observe ------ 执行工具调用并收集结果 ---------
String observation = "";
if (actionText != null && !actionText.isBlank()) {
observation = executeAction(actionText);
}
return ReactStep.builder()
.thought(thought)
.action(actionText)
.observation(observation)
.durationMs(System.currentTimeMillis() - startTime)
.build();
}
/**
* 构建包含历史的 Prompt ------ 让 LLM "看到"之前做了什么
*/
private String buildStepPrompt(ReactExecutionContext context) {
String history = context.buildHistoryText();
if (history.isBlank()) {
// 第一次调用:只传原始任务
return "任务:" + context.getOriginalTask() + "\n\n请开始规划,先输出你的 Thought:";
}
// 后续调用:带上完整历史,让 LLM 基于之前的决策继续思考
return String.format("""
任务:%s
已执行的步骤:
%s
请继续,输出下一步的 Thought 和 Action,或输出 Final Answer:
""",
context.getOriginalTask(),
history
);
}
/**
* 执行 Action(工具调用),带异常保护
*/
private String executeAction(String actionText) {
try {
ToolCall toolCall = ToolCallParser.parse(actionText);
log.info("[ReAct] 调用工具:{} 参数:{}", toolCall.getToolName(), toolCall.getParams());
Tool tool = toolRegistry.getTool(toolCall.getToolName());
if (tool == null) {
return "错误:未找到工具 " + toolCall.getToolName();
}
return tool.execute(toolCall.getParams());
} catch (Exception e) {
log.error("[ReAct] 工具调用失败:{}", e.getMessage());
// 返回错误信息给 LLM,让它自行决定下一步策略(重试 or 换思路)
return "工具调用失败:" + e.getMessage() + ",请调整参数重试。";
}
}
// ---------- 文本解析工具方法 ----------
private String parseThought(String llmResponse) {
int thoughtStart = llmResponse.indexOf("Thought:");
int actionStart = llmResponse.indexOf("Action:");
if (thoughtStart == -1) return llmResponse;
if (actionStart == -1) return llmResponse.substring(thoughtStart + 8).trim();
return llmResponse.substring(thoughtStart + 8, actionStart).trim();
}
private String parseAction(String llmResponse) {
int actionStart = llmResponse.indexOf("Action:");
int observationStart = llmResponse.indexOf("Observation:");
if (actionStart == -1) return null;
if (observationStart == -1) return llmResponse.substring(actionStart + 7).trim();
return llmResponse.substring(actionStart + 7, observationStart).trim();
}
private String extractFinalAnswer(String thought) {
int idx = thought.indexOf("Final Answer:");
if (idx == -1) return thought;
return thought.substring(idx + 13).trim();
}
/**
* 兜底方案:达到最大步数后,让 LLM 基于已收集信息强制汇总
*/
private String forceSummarize(ReactExecutionContext context) {
String summary = chatClient.prompt()
.user("已收集信息如下:\n" + context.buildHistoryText() +
"\n请基于以上信息,给出出行规划建议。")
.call()
.content();
return summary;
}
}
🔑 核心设计要点:
- 历史累积机制:每次循环将 Thought/Action/Observation 拼接进 Prompt,让 LLM 拥有完整上下文
- 终止条件检测 :通过识别
Final Answer:关键字判断任务完成- 安全兜底 :
maxSteps上限 +forceSummarize强制汇总,避免无限循环- 容错设计:工具调用异常时返回错误信息给 LLM,而非直接抛异常中断
六、多工具编排实战
6.1 工具注册中心 ------ 统一管理所有可用工具
java
@Component
public class ToolRegistry {
/**
* 使用 LinkedHashMap 保证注册顺序(影响 LLM 的工具选择)
*/
private final Map<String, Tool> toolMap = new LinkedHashMap<>();
/**
* 通过构造器自动注入所有 Tool Bean,无需手动注册
* 新增工具只需加 @Component 即可,符合开闭原则
*/
public ToolRegistry(FlightSearchTool flightTool,
HotelSearchTool hotelTool,
WeatherQueryTool weatherTool,
ItineraryGeneratorTool itineraryTool) {
register(flightTool);
register(hotelTool);
register(weatherTool);
register(itineraryTool);
}
private void register(Tool tool) {
toolMap.put(tool.getName(), tool);
}
public Tool getTool(String name) {
return toolMap.get(name);
}
public List<Object> getAllTools() {
return new ArrayList<>(toolMap.values());
}
}
6.2 航班搜索工具
java
@Component
public class FlightSearchTool {
@Tool(description = "搜索指定日期的可用航班,返回航班号、价格、时长等信息")
public String searchFlights(
@ToolParam(description = "出发城市,如:北京") String from,
@ToolParam(description = "到达城市,如:上海") String to,
@ToolParam(description = "出发日期,格式:YYYY-MM-DD") String date) {
log.info("[Tool] 搜索航班:{} → {},日期:{}", from, to, date);
// 实际项目中对接真实机票 API(如去哪儿、携程开放平台)
List<FlightInfo> flights = flightApiClient.search(from, to, date);
if (flights.isEmpty()) {
return String.format("未找到 %s 到 %s 在 %s 的航班", from, to, date);
}
// 格式化返回结果,便于 LLM 理解和引用
StringBuilder result = new StringBuilder();
result.append(String.format("找到 %d 个航班:\n", flights.size()));
for (FlightInfo flight : flights) {
result.append(String.format(
"- %s | %s→%s | 价格:¥%d | 时长:%s\n",
flight.getFlightNo(),
flight.getDepartTime(),
flight.getArriveTime(),
flight.getPrice(),
flight.getDuration()
));
}
return result.toString();
}
@Tool(description = "获取指定航班的详细信息和余票情况")
public String getFlightDetail(
@ToolParam(description = "航班号,如:CA1234") String flightNo) {
FlightDetail detail = flightApiClient.getDetail(flightNo);
return String.format(
"航班 %s:%s → %s\n机型:%s\n余票:%d 张\n餐食:%s",
detail.getFlightNo(),
detail.getDepartAirport(),
detail.getArriveAirport(),
detail.getAircraftType(),
detail.getAvailableSeats(),
detail.getMealService()
);
}
}
6.3 酒店搜索工具
java
@Component
public class HotelSearchTool {
@Tool(description = "搜索指定城市、日期的酒店,支持按星级和价格过滤")
public String searchHotels(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "入住日期,格式:YYYY-MM-DD") String checkIn,
@ToolParam(description = "退房日期,格式:YYYY-MM-DD") String checkOut,
@ToolParam(description = "最高价格(元/晚),不限填 0") int maxPrice) {
log.info("[Tool] 搜索酒店:{},{} ~ {}", city, checkIn, checkOut);
List<HotelInfo> hotels = hotelApiClient.search(city, checkIn, checkOut, maxPrice);
if (hotels.isEmpty()) {
return String.format("未找到 %s 符合条件的酒店", city);
}
StringBuilder result = new StringBuilder();
result.append(String.format("找到 %d 家酒店:\n", hotels.size()));
for (HotelInfo hotel : hotels) {
result.append(String.format(
"- %s | %s星 | ¥%d/晚 | 评分:%.1f | 位置:%s\n",
hotel.getName(),
hotel.getStarLevel(),
hotel.getPricePerNight(),
hotel.getRating(),
hotel.getLocation()
));
}
return result.toString();
}
}
6.4 天气查询工具
java
@Component
public class WeatherQueryTool {
@Tool(description = "查询指定城市未来 7 天的天气预报")
public String getWeatherForecast(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "查询日期,格式:YYYY-MM-DD") String date) {
log.info("[Tool] 查询天气:{},日期:{}", city, date);
WeatherInfo weather = weatherApiClient.getForecast(city, date);
return String.format(
"%s %s 天气:%s,气温 %d~%d℃,风力:%s,%s",
city,
date,
weather.getCondition(),
weather.getTempMin(),
weather.getTempMax(),
weather.getWindLevel(),
weather.getTravelTip()
);
}
}
6.5 并行工具调用优化 ------ 让无依赖的工具同时执行
有些工具之间没有依赖关系,可以并行执行来减少总耗时:
java
@Service
public class ParallelToolExecutor {
/**
* 使用虚拟线程(Java 21+),轻量级并发
* 相比 ThreadPoolExecutor,虚拟线程创建开销几乎为零
*/
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
/**
* 并行执行多个工具,减少等待时间
* 适用场景:酒店搜索和天气查询可以同时发起,无需等待对方结果
*/
public Map<String, String> executeParallel(List<ToolCallRequest> requests) {
Map<String, CompletableFuture<String>> futures = new LinkedHashMap<>();
for (ToolCallRequest request : requests) {
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> executeOne(request), executor)
.orTimeout(5, TimeUnit.SECONDS) // 单工具超时 5 秒
.exceptionally(e -> "工具调用超时或失败:" + e.getMessage());
futures.put(request.getToolName(), future);
}
// 等待所有工具完成
Map<String, String> results = new LinkedHashMap<>();
futures.forEach((name, future) -> {
try {
results.put(name, future.get());
} catch (Exception e) {
results.put(name, "执行异常:" + e.getMessage());
}
});
return results;
}
private String executeOne(ToolCallRequest request) {
Tool tool = toolRegistry.getTool(request.getToolName());
return tool.execute(request.getParams());
}
}
6.6 ReAct + 并行工具的混合策略
java
/**
* 混合执行器:继承基础 ReAct 执行器,增强并行调用能力
*
* 设计思路:
* - 如果 LLM 返回单个 Action → 走原有串行逻辑
* - 如果 LLM 返回多个 Action(用 || 分隔)→ 并行执行
*/
@Service
@Slf4j
public class HybridReactExecutor extends ReactAgentExecutor {
private final ParallelToolExecutor parallelExecutor;
/**
* 重写 executeAction,增加并行分支判断
*/
@Override
protected String executeAction(String actionText) {
// 如果 AI 返回了多个并行 Action(用 || 分隔)
if (actionText.contains("||")) {
return executeParallelActions(actionText);
}
// 否则走默认串行逻辑
return super.executeAction(actionText);
}
private String executeParallelActions(String actionText) {
String[] actions = actionText.split("\\|\\|");
List<ToolCallRequest> requests = Arrays.stream(actions)
.map(String::trim)
.map(ToolCallParser::parse)
.map(tc -> new ToolCallRequest(tc.getToolName(), tc.getParams()))
.collect(Collectors.toList());
log.info("[ReAct] 并行执行 {} 个工具", requests.size());
Map<String, String> results = parallelExecutor.executeParallel(requests);
// 合并所有工具结果,统一返回给 LLM
StringBuilder combined = new StringBuilder();
results.forEach((toolName, result) -> {
combined.append("【").append(toolName).append("结果】\n");
combined.append(result).append("\n\n");
});
return combined.toString();
}
}
七、Agent 执行追踪与调试
生产环境中,能清晰看到 Agent 的每一步决策至关重要------否则出了问题根本不知道 LLM 在想什么。
7.1 执行链追踪
java
@Component
@Slf4j
public class ReactExecutionTracer {
/**
* 打印完整的执行链路日志
* 包含:任务信息、每步 Thought/Action/Observation、最终答案
* 格式化输出,便于日志分析和问题排查
*/
public void traceExecution(ReactExecutionContext context) {
log.info("═══════════════════════════════════════════");
log.info("任务:{}", context.getOriginalTask());
log.info("会话 ID:{}", context.getSessionId());
log.info("总步数:{}/{}", context.getSteps().size(), context.getMaxSteps());
log.info("───────────────────────────────────────────");
for (int i = 0; i < context.getSteps().size(); i++) {
ReactStep step = context.getSteps().get(i);
log.info("[Step {}] ({}ms)", i + 1, step.getDurationMs());
log.info(" Thought : {}", abbreviate(step.getThought(), 100));
log.info(" Action : {}", step.getAction());
log.info(" Observation: {}", abbreviate(step.getObservation(), 100));
}
log.info("───────────────────────────────────────────");
log.info("Final Answer: {}", abbreviate(context.getFinalAnswer(), 200));
log.info("═══════════════════════════════════════════");
}
/** 截断过长文本,避免日志刷屏 */
private String abbreviate(String text, int maxLen) {
if (text == null) return "null";
return text.length() <= maxLen ? text : text.substring(0, maxLen) + "...";
}
}
7.2 对外暴露 API
java
@RestController
@RequestMapping("/api/travel-agent")
public class TravelAgentController {
private final HybridReactExecutor agentExecutor;
private final ReactExecutionTracer tracer;
/**
* 同步接口:等待 Agent 全部执行完毕后返回结果
* 适用场景:前端 loading 等待,对实时性要求不高
*/
@PostMapping("/plan")
public TravelPlanResponse planTravel(@RequestBody TravelPlanRequest request) {
ReactExecutionContext context = agentExecutor.execute(request.getTask());
tracer.traceExecution(context);
return TravelPlanResponse.builder()
.sessionId(context.getSessionId())
.finalAnswer(context.getFinalAnswer())
.steps(context.getSteps().size())
.completed(context.isCompleted())
.build();
}
/**
* SSE 流式接口:逐步返回每一步执行过程
* 适用场景:前端实时展示 Agent 的思考和行动过程,用户体验更好
*
* 事件格式:step:N|thought:xxx|action:xxx|observation:xxx
* 终止事件:final:xxx
*/
@GetMapping(value = "/plan/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> planTravelStream(@RequestParam String task) {
return Flux.create(sink -> {
agentExecutor.executeWithCallback(task, new ReactStepCallback() {
@Override
public void onStep(ReactStep step, int stepIndex) {
String event = String.format(
"step:%d|thought:%s|action:%s|observation:%s",
stepIndex,
step.getThought(),
step.getAction(),
step.getObservation()
);
sink.next(event);
}
@Override
public void onComplete(String finalAnswer) {
sink.next("final:" + finalAnswer);
sink.complete();
}
@Override
public void onError(Throwable error) {
sink.error(error);
}
});
});
}
}
7.3 效果演示
调用 /api/travel-agent/plan,传入:
json
{
"task": "帮我规划3月12日从北京去上海的出行,住两晚,机票预算1000以内"
}
返回结果:
json
{
"sessionId": "a3f2c1d4-...",
"finalAnswer": "为您规划如下出行方案:\n\n✈️ 推荐航班:CA1532,08:00出发,10:05到达,¥680\n🏨 推荐酒店:全季酒店·上海外滩,¥388/晚,评分4.8\n🌤 天气:3月12-14日,晴,16-22℃,适合出行,建议带件薄外套\n\n💡 总费用预估:机票¥680 + 酒店¥776 = ¥1456\n 出发前记得提前2小时到达机场!",
"steps": 4,
"completed": true
}
总结
本文通过"智能出行规划助手"场景,完整演示了 ReAct 模式 + 多工具编排的实现。下面一张表帮你快速回顾全文要点:
| 核心概念 | 手写实现(Spring AI) | Spring AI Alibaba 等价方案 |
|---|---|---|
| ReAct 循环 | 手写 ReactAgentExecutor (~200行) |
ReactAgent.builder().build() (3行) |
| 多工具注册 | ToolRegistry 统一管理 |
.tools(tool1, tool2, ...) |
| 上下文传递 | ExecutionContext 累积历史 |
内置,MemorySaver 自动持久化 |
| 并行工具 | CompletableFuture + Virtual Threads |
ParallelAgent 原生支持 |
| 工具重试 | 手写 try-catch | ToolRetryInterceptor.builder().maxRetries(3) |
| 执行步数限制 | maxSteps 字段手动判断 |
ToolCallLimitInterceptor |
| 执行追踪 | ReactExecutionTracer 自定义日志 |
.enableLogging(true) + Studio 可视化 |
| 流式输出 | Flux + 自定义 SSE |
agent.stream() 内置,事件类型更丰富 |
选型速查表
| 你的场景 | 推荐 |
|---|---|
| 学习 ReAct 底层原理 | 👉 先看本文手写版 |
| 快速做生产级项目 | 👉 直接上 Spring AI Alibaba |
| 需要自定义执行逻辑 | 👉 手写或基于 Graph Core 扩展 |
| 多 Agent 协作/工作流 | 👉 Spring AI Alibaba(优势最大) |
📚 系列导航
本篇文章属于「Spring AI 系列」进阶篇,建议按顺序阅读:
| 篇 | 主题 | 关键字 |
|---|---|---|
| 入门:环境搭建与第一个 AI 对话 | Quick Start | |
| Tool Calling 工具调用基础 | Function Callback | |
| ★ 6 | 进阶:ReAct 模式与多工具编排 | ReAct Agent(本文) |
| Advisor 拦截器链 | 责任链 / 自定义扩展 | |
| 源码解析:ChatClient 内部机制 | 源码解读 | |
| Tool Calling 工具调用 | Function Callback | |
| VectorStore与RAG Pipeline | 检索增强生成 |
👋 我是亦暖筑序,专注 Java 后端开发者的 AI 应用落地指南。
如果这篇 ReAct Agent 解析对你有帮助,欢迎 点赞 + 收藏 ,评论区说说你在项目中是如何处理多工具协作的? 我们下期见!🚀