2026年4月最新版 :基于 Spring AI 2.0.0-M4 + Spring Boot 4.0.0,代码可直接运行。
1. Java程序员为什么得关注Multi-Agent?
说实话,去年我还在用单轮调用的方式接大模型,写出来的东西跟玩具似的。用户多问两句就露馅,上下文根本接不上。
今年年初接了一个智能客服的项目,需求方要求能处理复杂投诉------比如用户一句"手机充不进电,App还总闪退,你们什么质量",系统得同时处理硬件问题和软件问题,最后给出一份像人写出来的回复。单Agent根本搞不定,这时候我才真正意识到Multi-Agent的价值。
Spring AI 2.0的几个关键特性:
- MCP(Model Context Protocol) :Agent接外部工具跟插U盘一样简单
- Advisor机制:对话记忆、RAG检索、安全审核,配置一下就能用
- Orchestrator-Workers并行编排:复杂任务自动拆解,多个Agent同时跑
- ChatClient流式API:声明式写法,代码量比我想象的少很多
这篇文章我把整个项目完整摊开来讲,从依赖到源码,从运行到上线注意事项。代码都是实际跑通过的,版本号别抄错。
2. 实战案例:一个能并行处理投诉的智能客服团队
先看效果。用户输入:
"我买的手机充不进电,而且你们的App总是闪退,你们这是什么质量?"
系统内部的处理流程:
css
[调度Agent] 识别到两个子任务:
├─ 子任务1:硬件故障排查 → 分配给 [技术支持Agent]
└─ 子任务2:软件缺陷反馈 → 分配给 [产品Agent]
[技术支持Agent] 返回:建议检查充电口、更换充电线、申请售后检测
[产品Agent] 返回:记录App闪退问题,建议升级到v3.2.1版本
[总结Agent] 整合输出:
"非常抱歉给您带来不好的体验。关于充电问题,建议您先尝试...
关于App闪退,我们已记录该问题,建议您升级到最新版本v3.2.1..."
这是典型的 Orchestrator-Workers 模式------调度Agent负责拆解任务,多个专业Agent并行处理,总结Agent最后汇总输出。整个流程在1-2秒内完成,用户感知不到背后有这么多Agent在协作。
3. 项目依赖
版本号是这个项目最关键的东西,用旧版API会踩一堆坑。
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
<relativePath/>
</parent>
<groupId>com.mszlu.ai</groupId>
<artifactId>spring-ai-multi-agent</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-ai.version>2.0.0-M4</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 显式引入 Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- OpenAI 模型支持(可替换为DeepSeek/Qwen/Claude) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- MCP Client 支持 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
版本说明:
Spring Boot 4.0.0:全面支持虚拟线程Spring AI 2.0.0-M4:2026年最新里程碑版本,MCP和Advisor已稳定Java 21:虚拟线程在这个场景下比传统线程池省太多资源
注意 :Spring AI 2.0的BOM管理方式变了,如果你还在用1.x的spring-ai-core直接引依赖,升级到2.0会报一堆类找不到。建议直接用BOM管理。
4. 配置文件
yaml
spring:
application:
name: multi-agent-service
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://mytoken.top # 国内可替换为中转地址
chat:
options:
model: gpt-5.5 # 2026年建议直接用GPT-5
temperature: 0.3 # Agent需要稳定输出,temperature调低
max-tokens: 4096
# MCP 服务器配置(可选,示例接入一个本地工具服务器)
mcp:
client:
stdio:
servers:
filesystem:
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-filesystem"
- "/tmp/mcp-data"
server:
port: 8080
temperature: 0.3 这个参数是我调了很久才定下来的。Agent不是写诗,不需要太多随机性,稳定输出最重要。调太高的话,同一个问题每次回复都不一样,测试结果没法复现。
5. 核心代码实现
5.1 领域模型
先定义Agent之间的数据交换格式,用Java 17的record非常方便:
arduino
package com.example.multiagent.model;
import java.util.List;
/**
* 调度Agent的输出:任务拆解结果
*/
public record TaskPlan(
String originalRequest,
List<SubTask> subTasks
) {
public record SubTask(
int id,
String description,
String assignedAgent, // "TECH_SUPPORT" / "PRODUCT" / "SALES"
List<String> keywords
) {}
}
/**
* Worker Agent的执行结果
*/
public record WorkerResult(
int subTaskId,
String agentType,
String content,
boolean requiresEscalation
) {}
/**
* 最终返回给用户的响应
*/
public record CustomerResponse(
String reply,
List<String> actions, // 建议用户执行的操作
boolean humanHandoff // 是否需要转人工
) {}
5.2 工具层:给Agent配"外挂"
less
package com.example.multiagent.tools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Map;
@Component
public class CustomerServiceTools {
/**
* 查询订单状态
*/
@Tool(name = "queryOrderStatus",
description = "根据订单号查询订单当前状态和物流信息")
public OrderStatus queryOrderStatus(
@ToolParam(description = "订单号,通常是12位数字") String orderId) {
// 实际项目中接入订单服务
return new OrderStatus(
orderId,
"已发货",
"顺丰速运",
"SF1234567890",
LocalDateTime.now().minusDays(1)
);
}
/**
* 创建售后工单
*/
@Tool(name = "createTicket",
description = "为用户创建售后工单,返回工单编号")
public String createTicket(
@ToolParam(description = "用户ID") String userId,
@ToolParam(description = "问题类型:HARDWARE/SOFTWARE/RETURN/OTHER") String type,
@ToolParam(description = "问题描述") String description) {
String ticketId = "TKT-" + System.currentTimeMillis();
System.out.printf("[工单系统] 已创建工单: %s | 用户: %s | 类型: %s%n",
ticketId, userId, type);
return ticketId;
}
/**
* 查询知识库FAQ
*/
@Tool(name = "searchFAQ",
description = "搜索知识库中的常见问题解答")
public String searchFAQ(
@ToolParam(description = "搜索关键词") String keyword) {
Map<String, String> faqDb = Map.of(
"充电", "请使用原装充电器,检查充电口是否有异物。如果问题持续,请申请售后检测。",
"闪退", "请升级到最新版本App(v3.2.1),并清理缓存。如仍闪退,请提供崩溃日志。",
"退货", "支持7天无理由退货,请确保商品未拆封。已拆封商品需质检后处理。"
);
return faqDb.getOrDefault(keyword,
"未找到相关问题,建议联系人工客服。");
}
public record OrderStatus(
String orderId,
String status,
String carrier,
String trackingNo,
LocalDateTime shippedAt
) {}
}
5.3 基础Agent抽象层
把所有Agent的公共逻辑抽出来,省得每个Agent都写一遍ChatClient配置:
arduino
package com.example.multiagent.agent;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
/**
* 所有Agent的基类,封装ChatClient和Memory
*/
public abstract class BaseAgent {
protected final ChatClient chatClient;
protected final ChatMemory chatMemory;
protected final String agentName;
protected BaseAgent(ChatClient.Builder chatClientBuilder,
String agentName,
String systemPrompt) {
this.agentName = agentName;
this.chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20) // 保留最近20轮对话
.build();
this.chatClient = chatClientBuilder
.defaultSystem(systemPrompt)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
public String getAgentName() {
return agentName;
}
}
MessageWindowChatMemory默认是内存存储,测试用没问题。上生产要换成Redis。
5.4 三大专业Agent实现
技术支持Agent:
scala
package com.example.multiagent.agent;
import com.example.multiagent.tools.CustomerServiceTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
@Component
public class TechSupportAgent extends BaseAgent {
private static final String SYSTEM_PROMPT = """
你是一名资深技术支持工程师,专门处理硬件故障、设备使用问题。
工作原则:
1. 先询问具体现象,再给出排查步骤
2. 优先引导用户自助解决,降低售后成本
3. 如果确认硬件故障,主动创建售后工单
4. 回答要专业但易懂,不要使用过于技术化的术语
你可以使用以下工具:
- queryOrderStatus:查询订单状态
- searchFAQ:搜索知识库
- createTicket:创建售后工单
""";
private final CustomerServiceTools tools;
public TechSupportAgent(ChatClient.Builder chatClientBuilder,
CustomerServiceTools tools) {
super(chatClientBuilder, "TECH_SUPPORT", SYSTEM_PROMPT);
this.tools = tools;
}
public String handle(String userMessage, String conversationId) {
return chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param("conversationId", conversationId))
.tools(tools) // Spring AI 2.0 自动注册所有 @Tool 方法
.call()
.content();
}
}
产品Agent:
scala
package com.example.multiagent.agent;
import com.example.multiagent.tools.CustomerServiceTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
@Component
public class ProductAgent extends BaseAgent {
private static final String SYSTEM_PROMPT = """
你是一名产品经理,专门处理软件缺陷反馈、功能建议和产品咨询。
工作原则:
1. 对用户反馈的软件问题表示重视和感谢
2. 如果已知该问题,提供解决方案或预计修复时间
3. 记录未知缺陷,生成工单转交研发团队
4. 主动推荐相关新功能或升级方案
你可以使用以下工具:
- searchFAQ:搜索已知问题
- createTicket:创建产品反馈工单
""";
private final CustomerServiceTools tools;
public ProductAgent(ChatClient.Builder chatClientBuilder,
CustomerServiceTools tools) {
super(chatClientBuilder, "PRODUCT", SYSTEM_PROMPT);
this.tools = tools;
}
public String handle(String userMessage, String conversationId) {
return chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param("conversationId", conversationId))
.tools(tools)
.call()
.content();
}
}
销售Agent:
scala
package com.example.multiagent.agent;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
@Component
public class SalesAgent extends BaseAgent {
private static final String SYSTEM_PROMPT = """
你是一名专业的销售顾问,负责处理价格咨询、促销活动和购买建议。
工作原则:
1. 主动了解用户需求,推荐最适合的产品配置
2. 清晰说明当前促销活动和优惠政策
3. 处理价格异议时,强调产品价值和售后服务
4. 引导用户完成下单,但不要过度推销
""";
public SalesAgent(ChatClient.Builder chatClientBuilder) {
super(chatClientBuilder, "SALES", SYSTEM_PROMPT);
}
public String handle(String userMessage, String conversationId) {
return chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param("conversationId", conversationId))
.call()
.content();
}
}
5.5 调度Agent(Orchestrator)
这是整个系统最核心的部分。我一开始考虑过用流程引擎或者规则引擎来做任务分发,但后来发现用LLM自己做调度反而更灵活------用户的输入千变万化,写死规则根本覆盖不过来。
arduino
package com.example.multiagent.agent;
import com.example.multiagent.model.TaskPlan;
import com.example.multiagent.model.WorkerResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
@Component
public class OrchestratorAgent {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
private final ExecutorService executor;
// 子Agent注入
private final TechSupportAgent techSupportAgent;
private final ProductAgent productAgent;
private final SalesAgent salesAgent;
public OrchestratorAgent(ChatClient.Builder chatClientBuilder,
TechSupportAgent techSupportAgent,
ProductAgent productAgent,
SalesAgent salesAgent) {
this.techSupportAgent = techSupportAgent;
this.productAgent = productAgent;
this.salesAgent = salesAgent;
this.objectMapper = new ObjectMapper();
// 虚拟线程线程池,Agent并行执行
this.executor = Executors.newVirtualThreadPerTaskExecutor();
this.chatClient = chatClientBuilder
.defaultSystem("""
你是一个智能调度中心,负责分析用户请求并拆解为子任务。
你必须严格按JSON格式输出任务计划,不要有任何额外解释:
{
"originalRequest": "用户原始请求",
"subTasks": [
{
"id": 1,
"description": "子任务描述",
"assignedAgent": "TECH_SUPPORT|PRODUCT|SALES",
"keywords": ["关键词1", "关键词2"]
}
]
}
调度规则:
- 硬件故障、使用问题 → TECH_SUPPORT
- 软件Bug、功能建议 → PRODUCT
- 价格咨询、购买意向 → SALES
- 复杂问题可拆分为多个子任务并行处理
""")
.build();
}
/**
* 核心编排逻辑:拆解 → 并行分发 → 汇总
*/
public List<WorkerResult> process(String userMessage, String conversationId) {
try {
// Step 1: 调度Agent分析并拆解任务
String planJson = chatClient.prompt()
.user("请分析以下用户请求并拆解为子任务:\n" + userMessage)
.call()
.content();
// 清理可能的Markdown代码块标记
planJson = planJson.replaceAll("```json\s*", "")
.replaceAll("```\s*", "");
TaskPlan plan = objectMapper.readValue(planJson, TaskPlan.class);
System.out.printf("[调度中心] 任务拆解完成,共 %d 个子任务%n", plan.subTasks().size());
// Step 2: 并行分发子任务给各个Worker Agent
List<Future<WorkerResult>> futures = new ArrayList<>();
for (TaskPlan.SubTask subTask : plan.subTasks()) {
Future<WorkerResult> future = executor.submit(() -> {
System.out.printf("[Worker] %s 开始处理子任务 #%d: %s%n",
subTask.assignedAgent(), subTask.id(), subTask.description());
long start = System.currentTimeMillis();
String result = dispatchToAgent(subTask, userMessage, conversationId);
long cost = System.currentTimeMillis() - start;
System.out.printf("[Worker] %s 子任务 #%d 完成,耗时 %dms%n",
subTask.assignedAgent(), subTask.id(), cost);
return new WorkerResult(
subTask.id(),
subTask.assignedAgent(),
result,
result.contains("人工") || result.contains("工单")
);
});
futures.add(future);
}
// Step 3: 收集所有Worker结果
List<WorkerResult> results = new ArrayList<>();
for (Future<WorkerResult> future : futures) {
results.add(future.get(30, TimeUnit.SECONDS));
}
return results;
} catch (Exception e) {
throw new RuntimeException("任务编排失败: " + e.getMessage(), e);
}
}
private String dispatchToAgent(TaskPlan.SubTask subTask,
String originalMessage,
String conversationId) {
String context = String.format(
"【子任务】%s\n【原始用户请求】%s\n请基于以上信息给出专业回复。",
subTask.description(), originalMessage
);
return switch (subTask.assignedAgent()) {
case "TECH_SUPPORT" -> techSupportAgent.handle(context, conversationId + "-tech");
case "PRODUCT" -> productAgent.handle(context, conversationId + "-prod");
case "SALES" -> salesAgent.handle(context, conversationId + "-sales");
default -> "未知Agent类型: " + subTask.assignedAgent();
};
}
}
这里有个细节:每个Worker Agent的conversationId都加了后缀(-tech、-prod),保证各个Agent的记忆空间隔离。如果都用同一个conversationId,不同Agent的对话会串在一起,结果很混乱。
5.6 总结Agent
arduino
package com.example.multiagent.agent;
import com.example.multiagent.model.CustomerResponse;
import com.example.multiagent.model.WorkerResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class SummaryAgent {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
public SummaryAgent(ChatClient.Builder chatClientBuilder) {
this.objectMapper = new ObjectMapper();
this.chatClient = chatClientBuilder
.defaultSystem("""
你是一个专业的客服总结专家,负责将多个Agent的处理结果整合为一份
给用户的标准回复。
输出要求:
1. 语气亲切、专业,先致歉(如有问题)再解决
2. 合并重复信息,去除内部术语(如"工单"改为"我们已为您登记")
3. 如果多个Agent都建议转人工,必须设置 humanHandoff=true
4. 输出必须为JSON格式:
{
"reply": "给用户的最终回复文本",
"actions": ["建议用户执行的操作1", "操作2"],
"humanHandoff": false
}
""")
.build();
}
public CustomerResponse summarize(String originalRequest,
List<WorkerResult> workerResults) {
try {
StringBuilder context = new StringBuilder();
context.append("【用户原始请求】\n").append(originalRequest).append("\n\n");
context.append("【各Agent处理结果】\n");
for (WorkerResult result : workerResults) {
context.append(String.format("[%s] %s\n\n",
result.agentType(), result.content()));
}
String responseJson = chatClient.prompt()
.user(context.toString())
.call()
.content();
// 清理可能的Markdown标记
responseJson = responseJson.replaceAll("```json\s*", "")
.replaceAll("```\s*", "");
return objectMapper.readValue(responseJson, CustomerResponse.class);
} catch (Exception e) {
// 降级处理:直接拼接
String fallback = workerResults.stream()
.map(WorkerResult::content)
.reduce((a, b) -> a + "\n" + b)
.orElse("感谢您的反馈,我们会尽快处理。");
return new CustomerResponse(fallback, List.of(), true);
}
}
}
降级处理很重要。LLM输出JSON偶尔会格式不对,如果直接抛异常,用户体验很差。降级方案至少能给用户一个可读的结果,同时标记需要转人工。
5.7 Service层:串联整个流程
kotlin
package com.example.multiagent.service;
import com.example.multiagent.agent.OrchestratorAgent;
import com.example.multiagent.agent.SummaryAgent;
import com.example.multiagent.model.CustomerResponse;
import com.example.multiagent.model.WorkerResult;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
@Service
public class CustomerService {
private final OrchestratorAgent orchestrator;
private final SummaryAgent summaryAgent;
public CustomerService(OrchestratorAgent orchestrator,
SummaryAgent summaryAgent) {
this.orchestrator = orchestrator;
this.summaryAgent = summaryAgent;
}
public CustomerResponse handleRequest(String userMessage) {
String conversationId = UUID.randomUUID().toString();
System.out.println("\n========== 新请求 ==========");
System.out.println("对话ID: " + conversationId);
System.out.println("用户: " + userMessage);
// Step 1: Orchestrator 拆解 + 并行分发
List<WorkerResult> workerResults = orchestrator.process(userMessage, conversationId);
// Step 2: Summary Agent 汇总输出
CustomerResponse response = summaryAgent.summarize(userMessage, workerResults);
System.out.println("\n【最终回复】");
System.out.println(response.reply());
System.out.println("是否转人工: " + response.humanHandoff());
System.out.println("==========================\n");
return response;
}
}
5.8 REST接口层
kotlin
package com.example.multiagent.controller;
import com.example.multiagent.model.CustomerResponse;
import com.example.multiagent.service.CustomerService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/customer-service")
@CrossOrigin
public class CustomerServiceController {
private final CustomerService customerService;
public CustomerServiceController(CustomerService customerService) {
this.customerService = customerService;
}
@PostMapping("/chat")
public CustomerResponse chat(@RequestBody ChatRequest request) {
return customerService.handleRequest(request.message());
}
public record ChatRequest(String message) {}
}
5.9 启动类
typescript
package com.example.multiagent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MultiAgentApplication {
public static void main(String[] args) {
SpringApplication.run(MultiAgentApplication.class, args);
}
}
6. 如何运行?
6.1 准备环境
bash
# 1. 确保 JDK 21+
java -version
# 2. 设置OpenAI API Key(或DeepSeek/Qwen等兼容OpenAI格式的Key)
export OPENAI_API_KEY="sk-xxxxxxxx"
# 3. 编译运行
./mvnw spring-boot:run
Windows下用PowerShell的话,export改成$env:OPENAI_API_KEY="sk-xxx"。
6.2 测试接口
json
curl -X POST http://localhost:8080/api/customer-service/chat \
-H "Content-Type: application/json" \
-d '{"message":"我买的手机充不进电,而且你们的App总是闪退,你们这是什么质量?"}'
预期输出:
swift
{
"reply": "非常抱歉给您带来这么差的使用体验,手机无法充电和 App 闪退确实会严重影响使用,我们会尽快协助您排查并跟进处理。\n\n关于手机充不进电,建议您先按以下方式快速确认:请使用原装充电器和数据线连接墙插充电 15--30 分钟;同时可以更换确认正常的充电器、数据线和插座测试。如果插上后完全没有充电图标、震动或亮屏提示,或更换充电器/数据线后仍无法充电,可能涉及充电口、电池或主板供电等硬件问题,需要进一步检测。\n\n关于 App 闪退,您可以先尝试更新 App 到最新版本、重启手机、清理缓存或重新安装 App,并确认手机系统为官方稳定版本、存储空间充足。如果仍然闪退,请提供手机型号、系统版本、App 版本号,以及闪退发生的具体场景,例如打开即闪退、登录后闪退或进入某个页面后闪退;如方便,也可以提供截图或录屏。\n\n为了尽快帮您处理,请您补充:订单号或购买凭证、手机型号、购买时间、插电后的具体反应、是否更换过充电器/数据线测试、是否有摔落/进水/受潮情况,以及 App 闪退的相关信息。我们会根据您提供的信息为您登记并安排进一步售后协助。",
"actions": [
"使用原装充电器和数据线连接墙插充电 15--30 分钟,观察是否有充电图标、震动或亮屏提示",
"更换确认正常的充电器、数据线和插座进行测试",
"检查充电口是否有灰尘、异物或松动,避免使用金属物硬捅清理",
"尝试更新 App、重启手机、清理缓存或重新安装 App",
"提供订单号或购买凭证、手机型号、购买时间、充电现象、是否摔落/进水,以及 App 版本和闪退场景"
],
"humanHandoff": true
}
6.3 控制台日志
运行时会看到类似这样的并行执行日志:
ini
========== 新请求 ==========
对话ID: 4b6248bc-a117-40db-8eb0-35973a63dad8
用户: 我买的手机充不进电,而且你们的App总是闪退,你们这是什么质量?
[调度中心] 任务拆解完成,共 2 个子任务
[Worker] TECH_SUPPORT 开始处理子任务 #1: 排查手机充不进电的问题,确认是否为充电器、数据线、电池或手机硬件故障
[Worker] PRODUCT 开始处理子任务 #2: 分析App总是闪退的问题,收集设备型号、系统版本、App版本及崩溃日志
[Worker] PRODUCT 子任务 #2 完成,耗时 10580ms
[Worker] TECH_SUPPORT 子任务 #1 完成,耗时 14468ms
【最终回复】
非常抱歉给您带来这么差的使用体验,手机无法充电和 App 闪退确实会严重影响使用,我们会尽快协助您排查并跟进处理。
关于手机充不进电,建议您先按以下方式快速确认:请使用原装充电器和数据线连接墙插充电 15--30 分钟;同时可以更换确认正常的充电器、数据线和插座测试。如果插上后完全没有充电图标、震动或亮屏提示,或更换充电器/数据线后仍无法充电,可能涉及充电口、电池或主板供电等硬件问题,需要进一步检测。
关于 App 闪退,您可以先尝试更新 App 到最新版本、重启手机、清理缓存或重新安装 App,并确认手机系统为官方稳定版本、存储空间充足。如果仍然闪退,请提供手机型号、系统版本、App 版本号,以及闪退发生的具体场景,例如打开即闪退、登录后闪退或进入某个页面后闪退;如方便,也可以提供截图或录屏。
为了尽快帮您处理,请您补充:订单号或购买凭证、手机型号、购买时间、插电后的具体反应、是否更换过充电器/数据线测试、是否有摔落/进水/受潮情况,以及 App 闪退的相关信息。我们会根据您提供的信息为您登记并安排进一步售后协助。
是否转人工: true
==========================
7. Spring AI 2.0 核心新特性解读
7.1 MCP(Model Context Protocol)
MCP让大模型能像插U盘一样使用外部工具:
scss
// Spring AI 2.0 一行代码接入MCP工具
chatClient.prompt()
.toolCallbacks(mcpToolCallbackProvider) // 自动发现远程MCP服务器的工具
.call();
你甚至不需要写@Tool方法,只要启动一个MCP Server(文件系统、Git、数据库、Slack等),Spring AI自动就能调用。我在另一个项目里接了一个MCP文件系统工具,Agent直接能读写项目文档,省了很多胶水代码。
7.2 Advisor 机制
Advisor是Spring AI 2.0最值得吹的设计。它把AOP思想搬到了AI领域,加功能跟贴膏药一样简单:
| Advisor | 作用 |
|---|---|
| MessageChatMemoryAdvisor | 自动管理多轮对话记忆 |
| PromptChatMemoryAdvisor | 将记忆注入System Prompt |
| QuestionAnswerAdvisor | 自动RAG检索增强 |
| SafeGuardAdvisor | 内容安全审核 |
用法:
scss
ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore).build()
)
.build();
7.3 虚拟线程 + 并行编排
Java 21的虚拟线程在这个场景下是降维打击。几百个Agent同时跑,内存占用只有几MB。用传统线程池的话,每个请求都要占一个OS线程,并发一高就崩。
ini
// 一个线程池搞定所有Agent并发
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
8. 进阶:从Demo到生产
上面的代码能跑通,但上生产还要补几件事。
8.1 持久化记忆
MessageWindowChatMemory存在内存里,服务重启就丢了。用户聊到一半掉线,回来发现上下文没了,体验很差。
生产环境建议接入Redis:
typescript
// 接入Redis持久化
@Bean
public ChatMemory redisChatMemory(RedisTemplate<String, String> redisTemplate) {
return new RedisChatMemory(redisTemplate); // 自定义实现
}
实现思路:把MessageWindowChatMemory的底层存储从ConcurrentHashMap换成Redis,key格式用chat:memory:{conversationId},设置7天过期时间。
8.2 流式输出(SSE)
客服场景下用户不愿等整段回复。Spring AI 2.0支持流式输出,一行代码搞定:
less
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
前端用EventSource接收,用户体验跟ChatGPT差不多。
8.3 Agent间通信协议
项目做大了,Agent之间需要传递结构化数据。建议搞一个Agent Message Bus:
arduino
public interface AgentBus {
void publish(String topic, AgentMessage message);
Flux<AgentMessage> subscribe(String topic);
}
用Redis Pub/Sub或者RabbitMQ实现,支持跨服务的Agent协作。我们现在的架构是把Orchestrator和Summary Agent放在主服务,Worker Agent拆到独立微服务里,通过Message Bus通信。
9. 总结
- Orchestrator-Workers 模式:调度Agent自动拆解任务,多Agent并行执行
- Tool Calling :
@Tool注解让Agent具备真实业务能力 - Advisor记忆机制:内置对话记忆,Agent能记住上下文
- 虚拟线程并发:Java 21轻量级线程,Agent并发性能拉满
核心依赖版本(2026年4月有效) :
| 组件 | 版本 |
|---|---|
| Spring Boot | 4.0.0 |
| Spring AI | 2.0.0-M4 |
| Java | 21 |
说实话,半年以前我还觉得Java在AI时代是配角。但Spring AI 2.0 + Java 21这套组合用下来,构建企业级Multi-Agent系统的体验并不比Python差,甚至在并发性能和类型安全上还更有优势。如果你也是Java背景,不用急着换语言,这套栈完全能扛住生产需求。
源码已经完整贴出来了,复制粘贴改改API Key就能跑。实际落地过程中如果遇到问题,比如Agent循环调用、Tool返回值格式不对、内存泄漏之类的,都是我之前踩过的坑,后面可以单独写一篇文章讲排错。