Spring AI 实战:用Java搭一个Multi-Agent多智能体系统,附完整源码

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返回值格式不对、内存泄漏之类的,都是我之前踩过的坑,后面可以单独写一篇文章讲排错。

相关推荐
TE-茶叶蛋2 小时前
Spring最核心扩展点:BeanPostProcessor
java·后端·spring
阿丰资源3 小时前
基于SpringBoot智能化体育馆管理系统(附源码+文档+数据库,一键运行)
数据库·spring boot·后端
千云3 小时前
问题排查报告:一次因元空间溢出导致的CPU飙升与接口超时
java·后端
breeze微风3 小时前
HashMap设计思想深度分析
后端
kree3 小时前
Kubernetes (k8s) 完全入门教程
后端
Jutick3 小时前
Python 行情数据清洗实战:Z-Score、MAD 与分位数过滤的异常值检测
后端·架构
NineData4 小时前
玖章算术NineData成功入选杭州市“新雏鹰”企业
运维·数据库·后端
SamDeepThinking4 小时前
用工厂模式和模板方法统一封装所有第三方的Access Token
java·后端·架构
CodeSheep5 小时前
DeepSeek的最新招人标准,太讽刺了。
前端·后端·程序员