JB3-9-SpringAI(二)

Java道经第3卷 - 第9阶 - SpringAI(二)

传送门:JB3-9-SpringAI(一)

传送门:JB3-9-SpringAI(二)

文章目录

  • [S06. 智能体](#S06. 智能体)
    • [E01. Agent基础对话](#E01. Agent基础对话)
      • [1. ReactAgent](#1. ReactAgent)
      • [2. 短期记忆MemorySaver](#2. 短期记忆MemorySaver)
      • [3. 长期记忆RedisSaver](#3. 长期记忆RedisSaver)
    • [E02. Agent工具调用](#E02. Agent工具调用)
      • [1. 混合工具调用](#1. 混合工具调用)
      • [2. 钩子函数Hooks](#2. 钩子函数Hooks)
      • [3. 工具包Skills](#3. 工具包Skills)
  • [S07. 多智能体](#S07. 多智能体)
    • [E01. 多智能体调度](#E01. 多智能体调度)
      • [1. SupervisorAgent](#1. SupervisorAgent)
    • [E02. 多智能体并行](#E02. 多智能体并行)
      • [1. ParallelAgent](#1. ParallelAgent)
    • [E03. 多智能体路由](#E03. 多智能体路由)
      • [1. LlmRoutingAgent](#1. LlmRoutingAgent)
  • [S08. 开发前端项目](#S08. 开发前端项目)
      • [1. 添加Axios依赖](#1. 添加Axios依赖)
      • [2. 添加ElementPlus](#2. 添加ElementPlus)
      • [3. 开发交互页面](#3. 开发交互页面)
  • [S09. 模型观察](#S09. 模型观察)
  • [S10. 模型评估](#S10. 模型评估)

S06. 智能体

心法:SpringAiAlibabaAgent 应用底层使用的式 ReAct 架构,它不创造新的底层技术,而是把已有的能力规范组合、加固优化,降低企业级 Agent 的开发与运维成本。

ReAct:是 Reasoning(推理) 加 Acting(行动) 的结合体,完整流程为:

  • 推理:大模型先对用户指令进行逻辑推理,判断当前信息足以直接作答,还是需要借助外部工具。
  • 行动:若确定需要外部工具,则主动调用对应工具,然后整理全部信息,最终输出回答。

关于 RAG:ReactAgent 原生并未内置检索增强(RAG)能力:

若需让智能体具备知识库问答能力,可将 RAG 检索逻辑封装为标准 Tool,注册至 Agent 体系中,由 Agent 根据推理结果,按需触发 RAG 工具完成知识检索,再结合检索内容完成应答,实现能力灵活扩展。

E01. Agent基础对话

心法:对于一个 "能思考、能调用工具、能执行动作" 的 Agent 系统来说,它既能动手(工具调用),也能动口(聊天问答)。
武技:创建 springai-agent-chat 子项目,并完成初始化工作。。

  1. 添加三方依赖:
xml 复制代码
<dependencies>
    <!--spring-boot-starter-web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--spring-ai-alibaba-starter-dashscope-->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>
    <!--spring-ai-alibaba-agent-framework-->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-agent-framework</artifactId>
    </dependency>
    <!--redisson-spring-boot-starter-->
	<dependency>
		<groupId>org.redisson</groupId>
		<artifactId>redisson-spring-boot-starter</artifactId>
		<version>${redisson-spring-boot-starter.version}</version>
	</dependency>
</dependencies>
  1. 开发主配文件:
yaml 复制代码
server:
  port: 13907 # 端口号
  servlet:
    encoding:
      charset: utf-8 # 字符集(解决 stream 中文乱码)
      enabled: true # 启用字符编码(解决 stream 中文乱码)
      force: true # 强制使用字符编码(解决 stream 中文乱码)

spring:
  data:
    redis:
      host: 192.168.40.77 # Redis 主机
      port: 6379 # Redis 端口
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY} # 阿里云百炼 API_KEY
      read-timeout: 100000 # 读取超时时间(毫秒)
      chat:
        options:
          model: qwen-plus # 基础对话模型
          max-tokens: 1000 # Token 限制
          temperature: 0.5 # 采样温度

logging:
  level:
    org.springframework.ai: DEBUG # AI 基础日志
  1. 开发启动类:
java 复制代码
package com.joezhou;

/** @author 周航宇 */
@SpringBootApplication
public class AgentChatApp {
    public static void main(String[] args) {
        SpringApplication.run(AgentChatApp.class, args);
    }
}

1. ReactAgent

心法:ReactAgent 是 Spring AI Alibaba 提供的高级智能体封装,它不是替代 ChatClient 或 ChatModel,而是基于它们进行深度增强,将对话交互、工具调用、记忆管理、流程编排、异常处理等全部企业级化,让开发者从零散 API 拼接,直接升级到可生产落地的智能体。

ReactAgent 相关建造参数:推荐用建造者模式实例化,链式调用、清晰易读,核心配置如下:

建造者配置 简述 描述 必填
model() 模型 Agent 所有思考,推理和响应都基于该模型 必填
name() 名称 Agent 唯一标识符,多用于多智能体路由,会话隔离等场景 必填
instruction() 指令 Agent 核心角色设定,行为指令,工具使用策略等 建议写清工具优先级、禁止编造数据指令、输出格式要求等 该配置直接影响模型的行为 必填
description() 描述 Agent 对外描述,多用于多智能体场景,不影响模型,默认空串 可选
returnReasoningContents() 思考过程 是否返回模型思考推理过程: true:调试环境推荐,方便看模型推理逻辑 false:生产环境推荐,减少返回体大小,避免泄露,默认值 可选
chatOptions() 模型参数 指定 ChatOptions 对象,用于覆盖模型默认参数 比如 temperature,maxTokens 等 可选
outputType() 实体输出 将结果绑定给指定的实体/记录类,用于结构化输出 可选

武技:使用 ReactAgent 完成与模型的基础对话交互。

  1. 开发记录类或实体类:(用于测试实体类响应效果):
java 复制代码
package com.joezhou.record;

/** @author 周航宇 */
public record UserRecord(
        @JsonPropertyDescription("用户姓名")
        String name,
        @JsonPropertyDescription("用户年龄")
        Integer age,
        @JsonPropertyDescription("用户自我介绍")
        String info
) {}
  1. 开发配置类:

ChatAgentConfig

java 复制代码
package com.joezhou.config;  
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Configuration
public class ChatAgentConfig {

    private final String NAME = "chatAgent";
    private final String INSTRUCTION = """
            你是智能助手,回答用户问题,要求如下:
            1. 直接返回纯中文文本内容,不要返回JSON格式。
            2. 不要用花括号包裹,不要用双引号包裹。

            正确示例:恭喜您...
            错误示例:"恭喜您..."
            错误示例:{"message": "恭喜您..."}
            """;
    private final String DESCRIPTION = "这是一个对话智能体,用于回答用户的问题。";
    private final String MODEL = "qwen-max";
    private final Integer MAX_TOKENS = 20;
    private final Double TEMPERATURE = 0.9;

    @Bean(NAME)
    public ReactAgent chatAgent(ChatModel chatModel) {
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .description(DESCRIPTION)
                .returnReasoningContents(false)
                .chatOptions(ChatOptions.builder().model(MODEL).temperature(TEMPERATURE).maxTokens(MAX_TOKENS).build())
                .build();
    }
}

EntityAgentConfig

java 复制代码
package com.joezhou.config;  
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Configuration
public class EntityAgentConfig {

    private final String NAME = "entityAgent";
    private final String INSTRUCTION = """
            你是智能助手,只负责从用户输入中提取信息,要求如下:
            1. 严格按照给定的实体类结构返回。
            2. 不要输出多余文字,只返回 JSON 格式数据。
            """;
    private final String DESCRIPTION = "这是一个实体提取智能体,用于从用户输入中提取信息。";

    @Bean(NAME)
    public ReactAgent entityAgent(ChatModel chatModel) {
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .description(DESCRIPTION)
                .outputType(UserRecord.class)
                .build();
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.joezhou.record.UserRecord;

/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/chat")
public class ChatController {

    private final ReactAgent chatAgent;
    private final ReactAgent entityAgent;

    public ChatController(@Qualifier("chatAgent") ReactAgent chatAgent, @Qualifier("entityAgent") ReactAgent entityAgent) {
        this.chatAgent = chatAgent;
        this.entityAgent = entityAgent;
    }

    @GetMapping("/call")
    public String call(@RequestParam("msg") String msg) throws GraphRunnerException {
        AssistantMessage assistantMessage = chatAgent.call(msg);
        log.info("assistantMessage: {}", assistantMessage);
        return assistantMessage.getText();
    }

    @GetMapping("/stream")
    public Flux<String> stream(@RequestParam("msg") String msg) throws GraphRunnerException {
        return chatAgent.stream(msg)
                // 只保留 StreamingOutput 类型的 output(流程图开头和结尾是 NodeOutput 类型,需要过滤掉)
                .filter(output -> output instanceof StreamingOutput<?>)
                // output -> StreamingOutput.class
                .cast(StreamingOutput.class)
                // 只保留 Agent 模型的 output
                .filter(streamingOutput -> streamingOutput.getOutputType() == OutputType.AGENT_MODEL_STREAMING)
                // 只保留 AssistantMessage 类型的 output
                .filter(streamingOutput -> streamingOutput.message() instanceof AssistantMessage)
                // 提取 AssistantMessage 文本内容
                .mapNotNull(streamingOutput -> ((AssistantMessage) streamingOutput.message()).getText());
    }

    @GetMapping("/streamMessages")
    public Flux<String> streamMessages(@RequestParam("msg") String msg) throws GraphRunnerException {
	    // 永远不要用 streamMessages 做带 Tool 的业务对话,它只适合纯文本无工具的基础流式问答
        return chatAgent.streamMessages(msg).map(Message::getText);
    }

    @GetMapping("/entity")
    public UserRecord entity(@RequestParam("msg") String msg) throws GraphRunnerException, JsonProcessingException {
        AssistantMessage message = entityAgent.call(msg);
        return new ObjectMapper().readValue(message.getText(), UserRecord.class);
    }
}
  1. 测试控制器:
http 复制代码
### 测试同步对话
GET http://localhost:13907/api/v1/chat/call?
    msg=讲个50字左右的笑话

### 测试流式对话
GET http://localhost:13907/api/v1/chat/stream?
    msg=讲个50字左右的笑话

### 测试流式对话
GET http://localhost:13907/api/v1/chat/streamMessages?
    msg=讲个50字左右的笑话

### 测试实体识别
GET http://localhost:13907/api/v1/chat/entity?
    msg=我叫赵四,我今年58岁,我是一个男孩,我喜欢运动,唱歌还有旅游。

2. 短期记忆MemorySaver

心法:MemorySaver 基于应用本地内存存储会话状态,实现单实例内短期对话记忆,服务重启、实例扩容、服务宕机后记忆数据全部丢失,仅适用于本地功能测试,单实例部署、短期临时度化交互、功能测试等非核心场景。

MemorySaver 特点

  • 会话状态仅存储在应用本地内存,数据生命周期与当前服务实例绑定。
  • 不支持分布式集群,多实例部署时各节点记忆相互独立,无法共享会话上下文。
  • 服务重启、进程宕机、节点扩容后,所有历史对话记忆会全部清空。
  • 依靠 threadId(conversationId) 实现会话隔离,单实例内不同对话互不干扰。
  • 无中间件依赖,开箱即用、调用延迟低、实现简单。
  • 无自动过期清理机制,长期大量会话会持续占用堆内存,存在内存溢出风险。

相关配置

java 复制代码
// 构建 agent 时指定 MemorySaver 记忆存储器
// 不指定 saver 配置,表示不使用记忆,每次调用都是无状态的 "一次性对话"
ReactAgent agent = ReactAgent.builder()
	.model(chatModel)
	.name("xxx")
	.instruction("xxx")
	.saver(new MemorySaver())
	.build();

武技:基于 MemorySaver 实现单实例会话记忆,通过会话 ID 区分不同用户对话上下文。

  1. 开发配置类:
java 复制代码
package com.joezhou.config;  
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Configuration
public class MemorySaverAgentConfig {

    private final String NAME = "memorySaverAgent";
    private final String INSTRUCTION = """
                    你是智能助手,回答用户问题,要求如下:
                    1. 直接返回纯中文文本内容,不要返回JSON格式。
                    2. 不要用花括号包裹,不要用双引号包裹。

                    正确示例:恭喜您...
                    错误示例:"恭喜您..."
                    错误示例:{"message": "恭喜您..."}
                    """;

    @Bean
    public MemorySaver memorySaver() {
        return new MemorySaver();
    }

    @Bean(NAME)
    public ReactAgent memorySaverAgent(ChatModel chatModel, MemorySaver memorySaver) {
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .outputType(String.class)
                .saver(memorySaver)
                .build();
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;

/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/memorySaver")
public class MemorySaverController {

    private final ReactAgent memorySaverAgent;

    public MemorySaverController(@Qualifier("memorySaverAgent") ReactAgent memorySaverAgent) {
        this.memorySaverAgent = memorySaverAgent;
    }

    @GetMapping("/chat")
    public Flux<String> chat(@RequestParam("msg") String msg, @RequestParam("conversationId") String conversationId) throws GraphRunnerException {

        // 每个 conversationId 共享记忆
        RunnableConfig config = RunnableConfig.builder()
                .threadId(conversationId)
                .build();
        // 交互
        return memorySaverAgent.streamMessages(msg, config).map(Message::getText);
    }
}
  1. 测试控制器:
http 复制代码
### 发送个人信息(对话ID:123456)
GET http://localhost:13907/api/v1/memorySaver/chat?
    msg=我今年100岁了&
    conversationId=123456

### 询问年龄(AI 会记住上一条消息,正确回答)
GET http://localhost:13907/api/v1/memorySaver/chat?
    msg=我多大了&
    conversationId=123456

### 更换对话ID(无记忆,AI 无法回答)
GET http://localhost:13907/api/v1/memorySaver/chat?
    msg=我多大了&
    conversationId=654321

3. 长期记忆RedisSaver

心法:RedisSaver 将智能体会话状态、对话上下文序列化后持久化至 Redis,属于分布式检查点存储,支持服务重启、集群多实例部署、流程中断后断点续跑,是生产环境长流程、核心业务的标准方案,适用于线上正式环境、长流程对话、工单办理、智能客服等核心业务。

RedisSaver 特点

  • 会话状态持久化到 Redis,支持分布式集群、多实例部署,所有节点共享会话记忆。
  • 服务重启、节点上下线,历史对话上下文不丢失,天然支持断点续跑。
  • 同样通过 threadId(conversationId) 实现会话隔离,不同用户 / 对话互不干扰。
  • 可结合 Redis 过期策略,自动清理长期不活跃的会话数据,避免内存溢出。

相关配置

java 复制代码
// 构建 redisServer 时需要依赖 redissonClient 客户端
RedisSaver redisSaver = RedisSaver.builder()
	.redisson(redissonClient)
	.build();

// 构建 agent 时指定 RedisSaver 记忆存储器
// 不指定 saver 配置,表示不使用记忆,每次调用都是无状态的 "一次性对话"
ReactAgent agent = ReactAgent.builder()
	.model(chatModel)
	.name("xxx")
	.instruction("xxx")
	.saver(redisSaver)
	.build();

武技:基于 RedisSaver 实现分布式会话记忆,结合 Redisson 操作 Redis,跨实例共享对话上下文。

  1. 开发配置类:
java 复制代码
package com.joezhou.config;  
import org.springframework.ai.chat.model.ChatModel;  
  
/** @author 周航宇 */
@Configuration
public class RedisSaverAgentConfig {

    private final String NAME = "redisSaverAgent";
    private final String INSTRUCTION = """
            你是智能助手,回答用户问题,要求如下:
            1. 直接返回纯中文文本内容,不要返回JSON格式。
            2. 不要用花括号包裹,不要用双引号包裹。

            正确示例:恭喜您...
            错误示例:"恭喜您..."
            错误示例:{"message": "恭喜您..."}
            """;

    @Bean(NAME)
    public ReactAgent redisSaverAgent(ChatModel chatModel, RedissonClient redissonClient) {
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .outputType(String.class)
                .saver(RedisSaver.builder().redisson(redissonClient).build())
                .build();
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;

/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/redisSaver")
public class RedisSaverController {

    private final ReactAgent redisSaverAgent;

    public RedisSaverController(@Qualifier("redisSaverAgent") ReactAgent redisSaverAgent) {
        this.redisSaverAgent = redisSaverAgent;
    }

    @GetMapping("/chat")
    public Flux<String> chat(@RequestParam("msg") String msg, @RequestParam("conversationId") String conversationId) throws GraphRunnerException {
        // 每个conversationId共享记忆
        RunnableConfig config = RunnableConfig.builder()
                .threadId(conversationId)
                .build();
        // 交互
        return redisSaverAgent.streamMessages(msg, config).map(Message::getText);
    }
}
  1. 测试控制器:
http 复制代码
### 发送个人信息(对话ID:123456)
GET http://localhost:13907/api/v1/redisSaver/chat?
    msg=我叫王钢蛋&
    conversationId=123456

### 询问姓名(AI 会记住上一条消息,正确回答)
GET http://localhost:13907/api/v1/redisSaver/chat?
    msg=我叫什么名字&
    conversationId=123456

### 更换对话ID(无记忆,AI 无法回答)
GET http://localhost:13907/api/v1/redisSaver/chat?
    msg=我叫什么名字&
    conversationId=654321

E02. Agent工具调用

武技:创建 springai-agent-skills 子项目,并完成初始化工作。。

  1. 添加三方依赖:
xml 复制代码
<dependencies>
    <!--spring-boot-starter-web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--spring-ai-alibaba-starter-dashscope-->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>
    <!--spring-ai-starter-mcp-client-webflux-->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
    </dependency>
    <!--spring-ai-alibaba-agent-framework-->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-agent-framework</artifactId>
    </dependency>
</dependencies>
  1. 开发配置文件:

mcp-servers-configuration.json

json 复制代码
{
  "mcpServers": {
    "amap-maps": {
      "args": [
        "-y",
        "@amap/amap-maps-mcp-server"
      ],
      "command": "D:\\node\\nodejs\\npx.cmd",
      "env": {
        "AMAP_MAPS_API_KEY": "a21c36b6b2f90920d260ef48b3a9f31a"
      }
    }
  }
}

application.yml

yaml 复制代码
server:
  port: 13908 # 端口号
  servlet:
    encoding:
      charset: utf-8 # 字符集(解决 stream 中文乱码)
      enabled: true # 启用字符编码(解决 stream 中文乱码)
      force: true # 强制使用字符编码(解决 stream 中文乱码)

spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY} # 阿里云百炼 API_KEY
      read-timeout: 100000 # 读取超时时间(毫秒)
      chat:
        options:
          model: qwen-plus # 基础对话模型
          max-tokens: 1000 # Token 限制
          temperature: 0.5 # 采样温度
    mcp:
      client:
        enabled: true # 启用 MCP 客户端
        request-timeout: 200000 # 请求超时时间(毫秒)
        toolcallback:
          enabled: true # 启用工具回调
        name: springai-agent-client # 客户端名称
        stdio:
          servers-configuration: classpath:mcp-servers-configuration.json # MCP 服务器配置文件

logging:
  level:
    org.springframework.ai: DEBUG # AI 基础日志
    io.modelcontextprotocol: DEBUG # MCP 调用日志
    org.springframework.ai.tool: DEBUG # 本地 Tool 工具调用日志
  1. 开发启动类:
java 复制代码
package com.joezhou;

/** @author 周航宇 */
@SpringBootApplication
public class AgentSkillsApp {
    public static void main(String[] args) {
        SpringApplication.run(AgentSkillsApp.class, args);
    }
}

1. 混合工具调用

心法:ReactAgent 内置了企业级工具调用引擎,天然支持本地工具 + MCP 外部工具统一混合调用,并自带并行、异步、防阻塞、限流等生产级能力,完全替代了手写工具调用的繁琐逻辑,让开发者只写业务工具,不用管调度、并发、阻塞问题,真正实现 "推理 + 行动" 一体化智能执行。

ReactAgent 相关建造参数:推荐用建造者模式实例化,链式调用、清晰易读,核心配置如下:

建造者配置 简述 描述
toolExecutionTimeout() 超时时间 限制单个工具调用的最大时长,生产必配,避免阻塞,默认 30 秒 10~30 秒:普通查询任务建议配置(如查数据库,查接口等) 1~2 分钟:第三方查询任务建议配置(如支付宝,大模型等) 3~5 分钟:长流程任务建议配置(如报表生成,文件处理等)
parallelToolExecution() 并行工具执行 控制 ReactAgent 是否并行执行多个工具(本地 + MCP) 默认 true:无相互依赖的工具可同时执行,大幅降低整体响应耗时
maxParallelTools() 最大并行数量 限制 ReactAgent 同一时间可并行调用的最大工具数 调试场景推荐设置为 1:串行更方便排查 生产场景推荐设置为 3:平衡性能与资源
wrapSyncToolsAsAsync() 异步包装器 ReactAgent 是否将同步本地工具、同步 MCP 调用包装为异步执行 默认 true:避免耗时工具阻塞 AI 主线程,提升系统吞吐量与稳定性
tools() 本地工具列表 指定本地工具列表,推荐使用 ToolCallbacks.from(工具实例列表) 包装
toolCallbackProviders() MCP 工具列表 指定 MCP 工具列表,类型为 ToolCallbackProvider(自动发现)

相关配置

java 复制代码
ReactAgent agent = ReactAgent.builder()
	.model(chatModel)
	.name("xxx")
	.instruction("xxx")
	.toolExecutionTimeout(xxx)
	.parallelToolExecution(true)
	.maxParallelTools(1)
	.wrapSyncToolsAsAsync(true)
	.tools(ToolCallbacks.from(weatherTool))
	.toolCallbackProviders(mcpTools)
	.build();

武技:测试 ReactAgent 的工具调用能力。

  1. 开发本地工具类:
java 复制代码
package com.joezhou.tool;

/** @author 周航宇 */
@Component
public class WeatherTool {

    /**
     * 根据天气和温度智能推荐穿衣搭配
     *
     * @param weather     天气描述,例如:晴朗、阴雨、下雪、多云
     * @param temperature 环境温度,单位:摄氏度
     * @return 穿衣建议
     */
    @Tool(name="recommendClothesByWeather", description = "根据天气和温度智能推荐穿衣搭配")
    public String recommendClothesByWeather(
            @ToolParam(description = "天气描述,例如:晴朗、阴雨、下雪、多云") String weather,
            @ToolParam(description = "环境温度,单位:摄氏度") Integer temperature) {

        // 校验参数是否合法
        if (StrUtil.isBlank(weather)) {
            return "天气状况不能为空,请输入:晴朗、阴雨、多云、下雪等";
        }

        if (ObjectUtil.isNull(temperature)) {
            return "温度不能为空,请输入数字类型的温度值";
        }

        if (temperature < -50 || temperature > 80) {
            return "温度数值不合理,请输入 -50℃ ~ 80℃ 之间的有效温度";
        }

        String prefix = "【穿衣建议】当前天气【%s】,温度【%d℃】,".formatted(weather, temperature);

        // 低温:-50℃ ~ 5℃
        if (temperature <= 5) {
            return prefix + "天气严寒,推荐:JoeZhou牌羽绒服、JoeZhou牌加绒裤、JoeZhou牌保暖内衣、JoeZhou牌围巾手套。";
        }

        // 凉爽:5℃ ~ 15℃
        if (temperature <= 15) {
            if (weather.contains("雨") || weather.contains("雪")) {
                return prefix + "湿冷天气,推荐:JoeZhou牌呢子大衣、JoeZhou牌毛衣、JoeZhou牌加绒裤、JoeZhou牌防水鞋。";
            } else {
                return prefix + "气温寒冷,推荐:JoeZhou牌毛呢外套、JoeZhou牌针织衫、JoeZhou牌休闲长裤。";
            }
        }

        // 舒适:15℃ ~ 25℃
        if (temperature <= 25) {
            if (weather.contains("雨")) {
                return prefix + "多雨天气,推荐:JoeZhou牌长袖上衣、JoeZhou牌薄长裤、JoeZhou牌便携雨具。";
            } else {
                return prefix + "温度适宜,推荐:JoeZhou牌短袖、JoeZhou牌薄衬衫、JoeZhou牌休闲裤。";
            }
        }

        // 高温:25℃ ~ 80℃
        return prefix + "天气炎热,推荐:JoeZhou牌纯棉短袖、JoeZhou牌短裤、JoeZhou牌防晒衣。";
    }
}
  1. 开发配置类:
java 复制代码
package com.joezhou.config;
import java.time.Duration;  
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Configuration
public class WeatherAgentConfig {

    /** 名称 */
    private static final String NAME = "weatherAgent";
    /** 指令 */
    private static final String INSTRUCTION = """
            你是智能助手,只能调用以下两类工具:
            
            1. 本地的 "根据天气和温度智能推荐穿衣搭配" 的工具。
            2. 高德地图MCP工具。
                
            工具选择规则:
            
            1. 路线规划/周边搜索/天气查询 → 只能使用高德地图MCP工具。
            2. 饮食建议/穿衣建议/运动建议 → 只能使用本地的 "根据天气和温度智能推荐穿衣搭配" 的工具。
            3. 禁止用模型自带知识回答。
            4. 直接返回纯中文文本内容,不要返回JSON格式,不要用花括号包裹,不要用双引号包裹。

            正确示例:恭喜您...
            错误示例:"恭喜您..."
            错误示例:{"message": "恭喜您..."}
            """;
    /** 超时时间 */
    private static final Duration TOOL_EXECUTE_TIMEOUT = Duration.ofSeconds(40);
    /** 是否并行执行工具调用 */
    private static final boolean PARALLEL_TOOL_EXECUTION = true;
    /** 最大并行工具调用数(调试用 1,生产用 3) */
    private static final int MAX_PARALLEL_TOOLS = 3;
    /** 是否将同步工具调用包装为异步调用(避免阻塞主线程,提升响应效率) */
    private static final boolean WRAP_SYNC_TOOLS_AS_ASYNC = true;

    @Bean("weatherAgent")
    public ReactAgent weatherAgent(ChatModel chatModel, WeatherTool weatherTool, ToolCallbackProvider mcpTools) {

        // 创建智能体
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .toolExecutionTimeout(TOOL_EXECUTE_TIMEOUT)
                .parallelToolExecution(PARALLEL_TOOL_EXECUTION)
                .maxParallelTools(MAX_PARALLEL_TOOLS)
                .wrapSyncToolsAsAsync(WRAP_SYNC_TOOLS_AS_ASYNC)
                // 引入本地工具
                .tools(ToolCallbacks.from(weatherTool))
                // 引入 MCP 工具,用于调用高德 MCP 工具
                .toolCallbackProviders(mcpTools)
                .build();
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;

/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/weatherAgent")
public class WeatherAgentController {

    private final ReactAgent weatherAgent;

    public WeatherAgentController(@Qualifier("weatherAgent") ReactAgent weatherAgent) {
        this.weatherAgent = weatherAgent;
    }

	@GetMapping("/chat")
    public Flux<String> chat(@RequestParam("msg") String msg) throws GraphRunnerException {
        // 执行 Agent,此时 Agent 会自动思考 + 调用对应工具
        return weatherAgent.stream(msg)
                // 只保留 StreamingOutput 类型的 output(流程图开头和结尾是 NodeOutput 类型,需要过滤掉)
                .filter(output -> output instanceof StreamingOutput<?>)
                // output -> StreamingOutput.class
                .cast(StreamingOutput.class)
                // 只保留 Agent 模型的 output
                .filter(streamingOutput -> streamingOutput.getOutputType() == OutputType.AGENT_MODEL_STREAMING)
                // 只保留 AssistantMessage 类型的 output
                .filter(streamingOutput -> streamingOutput.message() instanceof AssistantMessage)
                // 提取 AssistantMessage 文本内容
                .mapNotNull(streamingOutput -> ((AssistantMessage) streamingOutput.message()).getText());
    }
}
  1. 测试控制器:
http 复制代码
### 自动推理:只调用本地工具给出穿衣建议
GET http://localhost:13908/api/v1/weatherAgent/chat?
    msg=今天温度20摄氏度,天气晴朗,给出穿衣建议

对应图示

http 复制代码
### 自动推理:只调用高德地图 MCP 工具查询出行路线
GET http://localhost:13908/api/v1/weatherAgent/chat?
    msg=查询哈尔滨恒隆华府小区的经纬度

对应图示

http 复制代码
### 自动推理:先调用高德地图 MCP 工具查询天气,再调用本地工具给出穿衣建议
GET http://localhost:13908/api/v1/weatherAgent/chat?
    msg=根据哈尔滨今天的天气,给出穿衣建议

对应图示

2. 钩子函数Hooks

心法:Hooks(钩子)是 Agent 的生命周期监听工具,专注监听状态流转与节点执行,且 Hook 自身可以内置拦截器(Interceptors),也能与外部独立拦截器同时生效、组合使用,常用于日志埋点、监控告警、操作审计、全流程调试、异常处理、重试、参数校验、缓存、权限控制等企业级场景。

Hooks (粗粒度・全局监听):监听 整个 Agent 的完整生命周期,关注全局流程推进:

txt 复制代码
Agent 开始 → 思考中 → 决定调用 XXX 工具 → 等待结果 → 生成回答 → Agent 结束

Interceptors (细粒度・单点监听):监听 单次模型 / 工具调用 的执行过程,关注单点行为:

txt 复制代码
准备调用 XXX 工具 → 开始调用 XXX 工具 → 结束调用 XXX 工具 → 工具返回结果

Hooks 和 Interceptors 最大的区别在于监听粒度与生命周期不同,而实际开发中,二者必须组合使用,才能实现全流程追踪与控制:

相关配置

java 复制代码
ReactAgent agent = ReactAgent.builder()
	.model(chatModel)
	.name("xxx")
	.instruction("xxx")
	.hooks(new MyLogHook())
	.build();

武技:测试使用 Hook(内置 Interceptors)监控 Agent 全流程。

  1. 开发拦截器类:

MyToolInterceptor:拦截 Agent 调用,比如修改 prompt,限流,日志埋点等:

java 复制代码
package com.joezhou.component;

/** @author 周航宇 */
@Slf4j
public class MyToolInterceptor extends ToolInterceptor {

    @Override
    public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) {
        // 工具调用前日志
        log.info("【{}】决定调用工具:[{}],参数:{}", getName(), request.getToolName(), request.getArguments());
        log.info("【{}】等待工具返回结果...", getName());
        // 执行工具调用
        ToolCallResponse response = handler.call(request);
        // 工具调用后日志
        log.info("【{}】工具执行完成:[{}],结果:{}", getName(), response.getToolName(), response.getResult());
        return response;
    }

    @Override
    public String getName() {
        return "MyToolInterceptor";
    }
}

MyModelInterceptor:拦截 Tool 调用,比如修改参数,重试,日志埋点等:

java 复制代码
package com.joezhou.component;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse;

/** @author 周航宇 */
@Slf4j
public class MyModelInterceptor extends ModelInterceptor {
    
    @Override
    public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
        // 模型调用前日志(思考阶段)
        log.info("【{}】思考中(正在分析用户意图/生成回答)", getName());
        return handler.call(request);
    }

    @Override
    public String getName() {
        return "MyModelInterceptor";
    }
}
  1. 开发 Hook 类:
java 复制代码
package com.joezhou.component;
import org.springframework.ai.chat.messages.Message;

/** @author 周航宇 */
@Slf4j
@Component("myLogHook")
public class MyLogHook extends AgentHook {

    @Resource
    private MyToolInterceptor myToolInterceptor;

    @Resource
    private MyModelInterceptor myModelInterceptor;

    /**
     * 在 Agent 开始前调用,记录开始时间并初始化状态变量
     *
     * @param state  全局状态,包含用户输入、模型输出等信息
     * @param config 运行配置,包含模型选择、参数设置等
     * @return 包含自定义数据的 CompletableFuture,用于后续处理
     */
    @Override
    public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {
        // 获取用户问题(默认就是 input 字段)
        String input = state.value("input", "");
        log.info("【{}】=== Agent 开始执行 ===", getName());
        log.info("【{}】用户输入:{}", getName(), input);
        return CompletableFuture.completedFuture(Map.of("agentStartTime", System.currentTimeMillis()));
    }

    /**
     * 在 Agent 结束后调用
     *
     * @param state  全局状态,包含用户输入、模型输出等信息
     * @param config 运行配置,包含模型选择、参数设置等
     * @return 包含自定义数据的 CompletableFuture,用于后续处理
     */
    @Override
    public CompletableFuture<Map<String, Object>> afterAgent(OverAllState state, RunnableConfig config) {
        // 获取请求开始时间
        long startTime = state.value("agentStartTime", 0L);

        // 获取所有消息
        List<Message> messages = state.value("messages", List.of());

        // 获取最后一条 ASSISTANT 消息的文本内容
        String finalAnswer = messages.stream()
                // 只保留 ASSISTANT 消息
                .filter(msg -> msg instanceof AssistantMessage)
                // 将 Message 转换为 AssistantMessage
                .map(msg -> (AssistantMessage) msg)
                // 获取最后一条消息:"给我两个参数,我永远返回第二个参数"
                .reduce((first, second) -> second)
                // 获取文本内容
                .map(AssistantMessage::getText)
                // 如果没有消息,返回空字符串
                .orElse("");

        log.info("【{}】开始生成最终回答...", getName());
        log.info("【{}】最终回复:{}", getName(), finalAnswer);
        log.info("【{}】=== Agent 执行结束,总耗时:{}ms ===", getName(), System.currentTimeMillis() - startTime);

        // 返回空 CompletableFuture,表示没有后续处理需求
        return CompletableFuture.completedFuture(Map.of());
    }

    /**
     * 重写 Hook 接口方法:添加工具调用拦截器,实现工具调用日志
     */
    @Override
    public List<ToolInterceptor> getToolInterceptors() {
        return List.of(new MyToolInterceptor());
    }

    /**
     * 重写 Hook 接口方法:添加模型调用拦截器,实现"思考中"日志
     */
    @Override
    public List<ModelInterceptor> getModelInterceptors() {
        return List.of(new MyModelInterceptor());
    }

    @Override
    public String getName() {
        return "MyLogHook";
    }
}
  1. 开发配置类:
java 复制代码
package com.joezhou.config;  
import java.time.Duration;  
import org.springframework.ai.chat.model.ChatModel;  
  
/** @author 周航宇 */
@Configuration
public class HookAgentConfig {

    /** 名称 */
    private static final String NAME = "hookAgent";
    /** 指令 */
    private static final String INSTRUCTION = """
            你是智能助手,只能调用以下两类工具:
            
            1. 本地的 "根据天气和温度智能推荐穿衣搭配" 的工具。
            2. 高德地图MCP工具。
                
            工具选择规则:
            
            1. 路线规划/周边搜索/天气查询 → 只能使用高德地图MCP工具。
            2. 饮食建议/穿衣建议/运动建议 → 只能使用本地的 "根据天气和温度智能推荐穿衣搭配" 的工具。
            3. 禁止用模型自带知识回答。
            4. 直接返回纯中文文本内容,不要返回JSON格式,不要用花括号包裹,不要用双引号包裹。

            正确示例:恭喜您...
            错误示例:"恭喜您..."
            错误示例:{"message": "恭喜您..."}
            """;
    /** 超时时间 */
    private static final Duration TOOL_EXECUTE_TIMEOUT = Duration.ofSeconds(40);
    /** 是否并行执行工具调用 */
    private static final boolean PARALLEL_TOOL_EXECUTION = true;
    /** 最大并行工具调用数(调试用 1,生产用 3) */
    private static final int MAX_PARALLEL_TOOLS = 3;
    /** 是否将同步工具调用包装为异步调用(避免阻塞主线程,提升响应效率) */
    private static final boolean WRAP_SYNC_TOOLS_AS_ASYNC = true;

    @Bean("hookAgent")  
    public ReactAgent hookAgent(ChatModel chatModel, WeatherTool weatherTool, ToolCallbackProvider mcpTools, @Qualifier("myLogHook") MyLogHook myLogHook  
) {

        // 创建智能体
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .toolExecutionTimeout(TOOL_EXECUTE_TIMEOUT)
                .parallelToolExecution(PARALLEL_TOOL_EXECUTION)
                .maxParallelTools(MAX_PARALLEL_TOOLS)
                .wrapSyncToolsAsAsync(WRAP_SYNC_TOOLS_AS_ASYNC)
                // 引入本地工具
                .tools(ToolCallbacks.from(weatherTool))
                // 引入 MCP 工具,用于调用高德 MCP 工具
                .toolCallbackProviders(mcpTools)
                .hooks(myLogHook)
                .build();
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;

/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/hookAgent")
public class HookAgentController {

    private final ReactAgent hookAgent;

    public HookAgentController(@Qualifier("hookAgent") ReactAgent hookAgent) {
        this.hookAgent = hookAgent;
    }

    @GetMapping("/chat")
    public Flux<String> chat(@RequestParam("msg") String msg) throws GraphRunnerException {
        // 执行 Agent,此时 Agent 会自动思考 + 调用对应工具
        return hookAgent.stream(msg)
                // 只保留 StreamingOutput 类型的 output(流程图开头和结尾是 NodeOutput 类型,需要过滤掉)
                .filter(output -> output instanceof StreamingOutput<?>)
                // output -> StreamingOutput.class
                .cast(StreamingOutput.class)
                // 只保留 Agent 模型的 output
                .filter(streamingOutput -> streamingOutput.getOutputType() == OutputType.AGENT_MODEL_STREAMING)
                // 只保留 AssistantMessage 类型的 output
                .filter(streamingOutput -> streamingOutput.message() instanceof AssistantMessage)
                // 提取 AssistantMessage 文本内容
                .mapNotNull(streamingOutput -> ((AssistantMessage) streamingOutput.message()).getText());
    }
}
  1. 测试控制器:
http 复制代码
### 先调用高德地图 MCP 工具查询天气,再调用本地工具给出穿衣建议
GET http://localhost:13908/api/v1/hookAgent/chat?
    msg=根据哈尔滨今天的天气,给出穿衣建议

控制台结果

3. 工具包Skills

心法:在 Spring AI Alibaba 生态中,Tool(这里包括本地工具和 MCP 工具)和 Skill(业务技能包)是互补关系:"Tool 是 Skill 的基础,没有 Tool,Skill 只是无执行能力的配置文件,而 Skill 是 Tool 的上层封装,没有 Skill,Tool 只是零散的底层函数"。

Tool :通过 @Tool 定义的底层原子函数,可理解为螺丝刀、眼镜布、扳手等单独工具:

  • 核心目标:解决 "怎么做" 的问题。
  • 内存浪费:Agent 启动时会全局加载所有 Tool,Tool 越多内存占用越高。
  • 资金浪费:每次请求都必须把所有 Tool 的完整信息(名称、描述、参数)塞进 Prompt,Token 消耗高。
  • 算力浪费:模型必须从全量 Tool 中筛选判断,决策成本高、易误调用。
  • 低内聚:Tool 之间无业务关联、无协作规则,模型无法感知工具之间的关系。
  • 高耦合:新增或修改 Tool 必须修改 Agent 注册代码,耦合度高、维护麻烦。
  • 低扩展:Tool 数量越多,Agent 调度越混乱,无法支撑企业级大规模工具管理。

Skill :以 SKILL.md 为配置中心,按业务场景聚合的 Tool 套装,可理解为电脑维修包、眼镜清洁包等工具包:

  • 核心目标:解决 "什么时候用、用哪些、按什么规则用" 的问题。
  • 节省内存:Agent 启动时只加载 Skill 名称 + 简介,不加载内部 Tool,内存占用极低。
  • 节省资金:初始 Prompt 仅携带技能元数据,Tool 信息按需加载,Token 消耗大幅降低。
  • 节省算力:模型先匹配业务场景,再加载对应 Skill 的 Tool,决策成本极低。
  • 高内聚:Skill 通过 Markdown 文件定义触发条件、业务规则、工具关系和执行流程,语义高度内聚。
  • 低耦合:新增或修改 Tool 只需修改 SKILL.md 文件即可,无需改动 Agent 代码,维护极简单。
  • 高扩展:技能模块化、可插拔、可独立迭代,完美支持企业级大规模场景。

SKILL.md 文件开发规范

  • 所有 MD 文件统一放在 src/main/resources/skills/ 目录下,每个技能单独一个目录。
  • 配置文件名称固定为 SKILL.md 文件,全大写。
  • 配置文件内容分为 YAML Frontmatter(三横线分割块,大模型启动时就要提供的元信息)和 Markdown 正文(大模型加载技能之后才会看到的详细指引)两部分,如下:
markdown 复制代码
---
name: XX(技能唯一标识,仅支持小写字母,数字和连字符,最长 64 字符,且必须和所在目录名一致)
description: XX 技能,包括 X、XX 能力。(一句话描述用途和触发场景,太长会被截断)
version: 1.0(版本,用于迭代)
author: 周航宇(作者/团队名)
tags: A,B,C,D(标签,便于管理)
tools:(对应 Tool 工具的 bean 的名称)
  - toolBeanName1
  - toolBeanName2
  - toolBeanName3
---

# XXX 处理技能(技能的中文简述)

## 技能定位(定义技能边界,职责,服务范围)

## 启用范围(区分适用场景,不适用场景)

## 工具详情(精细化描述工具,参数,数据类型,业务限制)

## 行为规范(强约束,风控要求,数据输出标准,错误处理)

## 交互示例(区分正常调用,异常调用,边界用例)

武技:测试 Skills 开发。

  1. 开发 3 个 Tool 工具:

SelectOrderTool

java 复制代码
package com.joezhou.tool;

/** @author 周航宇 */
@Component
public class SelectOrderTool {

    /**
     * 根据订单号查询订单记录
     *
     * @param orderNo 订单号
     * @return 订单记录
     */
    @Tool(name="selectOrder", description = "根据订单号查询订单记录")
    public String selectOrder(@ToolParam(description = "订单号,如:ORD12345") String orderNo) {
        // 模拟数据库查询
        return """
                {
                    "orderNo": "%s",
                    "status": "已支付",
                    "goods": "iPhone 15",
                    "num": 1,
                    "userId": "U001",
                    "amount": "5999.00"
                }
                """.formatted(orderNo);
    }
}

InsertOrderTool

java 复制代码
package com.joezhou.tool;

/** @author 周航宇 */
@Component
public class InsertOrderTool {

    /**
     * 插入新订单
     *
     * @param goods  商品名称
     * @param num    数量
     * @param userId 用户ID
     * @return 订单号
     */
    @Tool(name="insertOrder", description = "插入新订单")
    public String insertOrder(@ToolParam(description = "商品名称") String goods, @ToolParam(description = "数量") Integer num, @ToolParam(description = "用户ID") String userId) {
        // 模拟生成订单号+入库
        String orderNo = "ORD" + System.currentTimeMillis();
        return """
                {
                    "success": true,
                    "orderNo": "%s",
                    "goods": "%s",
                    "num": %d,
                    "userId": "%s",
                    "status": "待支付"
                }
                """.formatted(orderNo, goods, num, userId);
    }
}

CancelOrderTool

java 复制代码
package com.joezhou.tool;

/** @author 周航宇 */
@Component
public class CancelOrderTool {

    /**
     * 取消订单
     *
     * @param orderNo 订单号
     * @return 取消结果
     */
    @Tool(name="cancelOrder", description = "根据订单号取消订单")
    public String cancelOrder(@ToolParam(description = "订单号") String orderNo) {
        // 模拟取消
        return """
                {
                    "orderNo": "%s",
                    "status": "已取消",
                    "msg": "取消成功"
                }
                """.formatted(orderNo);
    }
}
  1. 在 classpath 下开发 skills/order-skills/SKILL.md 文件:
markdown 复制代码
---
name: order-skills
description: 订单处理业务技能,提供订单查询、新建、取消全流程操作能力,适用于用户订单相关诉求处理。
version: 1.0
author: 周航宇
tags: 订单业务,订单CRUD
tools:
  - selectOrderTool
  - insertOrderTool
  - cancelOrderTool
---

# 订单处理技能

## 技能定位
作为企业订单系统专属助手,统一处理订单查询、订单创建、订单取消三类核心业务操作,严格按照业务规则调用对应工具完成诉求。

## 启用范围
当用户提问包含以下关键词及相关语义时,启用本技能:
创建订单、下单、新增订单、查询订单、查订单、取消订单、撤销订单、作废订单等。

## 工具详情
### 1. selectOrderTool(订单查询)
- 功能:根据订单编号查询订单完整详情
- 入参:orderNo(订单号,必填)

### 2. insertOrderTool(订单创建)
- 功能:生成并创建新订单
- 入参:goods(商品名称,必填)、num(购买数量,必填)、userId(用户ID,必填)

### 3. cancelOrderTool(订单取消)
- 功能:对有效订单执行取消操作
- 入参:orderNo(订单号,必填)

## 行为规范
1. 工具隔离:严格区分三类工具职责,禁止跨场景混用工具。
2. 参数校验:调用工具前逐项校验必填参数,参数缺失、格式非法时,明确告知用户并引导补全/修正。
3. 工具范围:仅使用本技能声明的3个工具,不调用技能外其他能力。
4. 返回规范:所有正常执行结果、异常提示,均使用标准 JSON 格式对外输出。
5. 数据安全:不编造订单数据、不篡改订单状态,如实返回工具执行结果。
  
## 交互示例
### 正常示例01:查询订单
用户请求:帮我查询订单 ORD20260601001
工具调用:selectOrderTool(orderNo="ORD20260601001")
返回结果:{"orderNo": "ORD20260601001","status": "已支付","goods": "iPhone 15","num": 1,"userId": "U001","amount": "5999.00"}

### 正常示例02:创建订单
用户请求:帮我创建订单,商品为蓝牙耳机,数量 2,用户 ID U003
工具调用:insertOrderTool (goods="蓝牙耳机", num=2, userId="U003")
返回结果:{"orderNo": "ORD20260601001","status": "已支付","goods": "华为Mate60","num": 1,"userId": "U002","amount": "4999.00"}

### 正常示例03:取消订单
用户请求:取消订单 ORD20260601001
工具调用:cancelOrderTool (orderNo="ORD20260601001")
返回结果:{"orderNo": "ORD20260601001","status": "已取消","msg": "取消成功"}

### 异常示例03:参数缺失
用户请求:帮我创建订单
返回结果:{"code": "PARAM_MISS", "msg": "创建订单缺少必填参数,请补充商品名称、购买数量、用户ID"}
  1. 开发配置类:
java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Configuration
public class SkillAgentConfig {

    /** 名称 */
    private static final String NAME = "skillAgent";
    /** 指令 */
    private static final String INSTRUCTION = """
            你是订单助手,只负责处理用户订单相关问题,其他问题请拒绝回答,要求如下:
            工具调用强制规则:
            1. 如果用户想要查询订单,必须调用 selectOrderTool 工具。
            2. 如果用户想要取消订单,必须调用 cancelOrderTool 工具。
            3. 如果用户想要创建订单,必须调用 insertOrderTool 工具。
            回答输出规范:
            4. 禁止用模型自带知识回答。
            5. 直接返回纯中文文本内容,不要返回JSON格式,不要用花括号包裹,不要用双引号包裹。
            6. 不要输出思考过程、不要写工具调用日志,只给用户可读自然答案。

            正确示例:恭喜您...
            错误示例:"恭喜您..."
            错误示例:{"message": "恭喜您..."}
            """;

    /**
     * 配置 Skill 注册器,该注册器用于扫描,加载和管理指定目录下的 SKILL.md
     * 让 Agent 知道他有哪些技能包可以用
     *
     * @return Skill 注册器,全局可用,推荐单独配 bean,加入 Spring 管理,而非局部创建
     */
    @Bean
    public SkillRegistry skillRegistry() {
        // 注册 SKILL.md 文件
        return ClasspathSkillRegistry.builder()
                // 指定扫描路径,默认 resources/skills/ 中的全部 SKILL.md 文件
                .classpathPath("skills")
                // 指定缓存目录,默认生成 D:/tmp 目录并使用
                .basePath("D:/workspace/java/v3-9-ssm-ai/springai-agent-skills/cache")
                .build();
    }

    /**
     * 挂载 Tool 到 Skill(关键:让 Skill 内部能调用这3个Tool)
     *
     * @param skillRegistry   Skill 注册器
     * @param selectOrderTool 查询订单工具
     * @param insertOrderTool 创建订单工具
     * @param cancelOrderTool 取消订单工具
     * @return Skill 挂载器
     */
    @Bean
    public SkillsAgentHook skillsAgentHook(SkillRegistry skillRegistry, SelectOrderTool selectOrderTool, InsertOrderTool insertOrderTool, CancelOrderTool cancelOrderTool) {

        // 把工具对象转换成 ToolCallback
        ToolCallback[] toolCallbacks = MethodToolCallbackProvider.builder().toolObjects(selectOrderTool, insertOrderTool, cancelOrderTool).build().getToolCallbacks();

        // 按技能分组
        Map<String, List<ToolCallback>> groupedTools = Map.of("order-skills", List.of(toolCallbacks));

        return SkillsAgentHook.builder().skillRegistry(skillRegistry).groupedTools(groupedTools) // 核心:Skill 内可见这3个Tool
                .build();
    }

    @Bean(NAME)
    public ReactAgent skillAgent(ChatModel chatModel, SkillsAgentHook skillsAgentHook) {
        return ReactAgent.builder()
                .name(NAME)
                .instruction(INSTRUCTION)
                .model(chatModel)
                .hooks(skillsAgentHook)
                .build();
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;

/** @author 周航宇 */
@RestController
@RequestMapping("/api/v1/skillAgent")
public class SkillAgentController {

    private final ReactAgent skillAgent;

    public SkillAgentController(@Qualifier("skillAgent") ReactAgent skillAgent) {
        this.skillAgent = skillAgent;
    }
    
    @GetMapping("/chat")
    public Flux<String> chat(@RequestParam("msg") String msg) throws GraphRunnerException {
        // 执行 Agent,此时 Agent 会自动思考 + 调用对应工具
        return skillAgent.stream(msg)
                // 只保留 StreamingOutput 类型的 output(流程图开头和结尾是 NodeOutput 类型,需要过滤掉)
                .filter(output -> output instanceof StreamingOutput<?>)
                // output -> StreamingOutput.class
                .cast(StreamingOutput.class)
                // 只保留 Agent 模型的 output
                .filter(streamingOutput -> streamingOutput.getOutputType() == OutputType.AGENT_MODEL_STREAMING)
                // 只保留 AssistantMessage 类型的 output
                .filter(streamingOutput -> streamingOutput.message() instanceof AssistantMessage)
                // 提取 AssistantMessage 文本内容
                .mapNotNull(streamingOutput -> ((AssistantMessage) streamingOutput.message()).getText());
    }
}
  1. 测试控制器:
http 复制代码
### 创建订单
GET http://localhost:13908/api/v1/skillAgent/chat?
    msg=创建订单,商品是华为Mate60,数量2,用户U002

### 查询订单
GET http://localhost:13908/api/v1/skillAgent/chat?
	msg=查询订单ORD1780231349571

### 取消订单
GET http://localhost:13908/api/v1/skillAgent/chat?
	msg=取消订单ORD1780231349571

S07. 多智能体

E01. 多智能体调度

武技:创建 springai-agent-supervisor 子项目,并完成初始化工作。。

  1. 添加三方依赖:
xml 复制代码
<dependencies>
	<!--spring-boot-starter-web-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<!--spring-ai-alibaba-starter-dashscope-->
	<dependency>
		<groupId>com.alibaba.cloud.ai</groupId>
		<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
	</dependency>
	<!--spring-ai-alibaba-agent-framework-->
	<dependency>
		<groupId>com.alibaba.cloud.ai</groupId>
		<artifactId>spring-ai-alibaba-agent-framework</artifactId>
	</dependency>
</dependencies>
  1. 开发配置文件:
yaml 复制代码
server:
  port: 13909 # 端口号
  servlet:
    encoding:
      charset: utf-8 # 字符集(解决 stream 中文乱码)
      enabled: true # 启用字符编码(解决 stream 中文乱码)
      force: true # 强制使用字符编码(解决 stream 中文乱码)

spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY} # 阿里云百炼 API_KEY
      read-timeout: 100000 # 读取超时时间(毫秒)
      chat:
        options:
          model: qwen-plus # 基础对话模型
          max-tokens: 1000 # Token 限制
          temperature: 0.5 # 采样温度

logging:
  level:
    org.springframework.ai: DEBUG # AI 基础日志
  1. 开发启动类:
java 复制代码
package com.joezhou;

/** @author 周航宇 */
@SpringBootApplication
public class AgentSupervisorApp {
    public static void main(String[] args) {
        SpringApplication.run(AgentSupervisorApp.class, args);
    }
}

1. SupervisorAgent

  1. 开发实体类:

Order

java 复制代码
package com.joezhou.entity;

/** @author 周航宇 */
@Data
public class Order implements Serializable {
    @JsonPropertyDescription("用户姓名")
    private String username;
    @JsonPropertyDescription("用户联系电话")
    private String telephone;
    @JsonPropertyDescription("收货地址")
    private String address;
    @JsonPropertyDescription("订单号")
    private String orderNo;
    @JsonPropertyDescription("下单时间")
    private LocalDateTime orderTime;
    @JsonPropertyDescription("商品名称")
    private String productName;
    @JsonPropertyDescription("用户预算")
    private String budget;
}

Shop

java 复制代码
package com.joezhou.entity;

/** @author 周航宇 */
@Data
public class Shop implements Serializable {
    @JsonPropertyDescription("商家名称")
    private String name;
    @JsonPropertyDescription("商家地址")
    private String address;
    @JsonPropertyDescription("商家联系电话")
    private String telephone;
    @JsonPropertyDescription("商品名称")
    private String productName;
    @JsonPropertyDescription("商品价格")
    private double productPrice;
    @JsonPropertyDescription("餐盒费")
    private double boxFee;
    @JsonPropertyDescription("配送费")
    private double deliveryFee;
    @JsonPropertyDescription("打包费")
    private double packingFee;
}
  1. 开发工具类:

OrderTool

java 复制代码
package com.joezhou.tool;
import com.joezhou.entity.Order;

/** @author 周航宇 */
@Component
@Slf4j
public class OrderTool {

    private final ChatClient chatClient;

    public OrderTool(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    /**
     * 从用户输入的订单描述中提取商品名称和预算信息
     *
     * @param msg 用户输入的订单描述
     * @return 解析后的订单信息
     */
    @Tool(name = "parseOrder", description = "从用户输入的订单描述中提取商品名称和用户预算信息")
    public Order parseOrder(@ToolParam(description = "用户输入的订单描述") String msg) {
        log.info("【parseOrder】获取用户输入:{}", msg);

        // 调用大模型,提取评论的关键词
        Order order = chatClient.prompt()
                .user("根据订单描述 %s,提取商品名称和用户预算信息" .formatted(msg))
                .call()
                .entity(Order.class);

        // 补充订单信息
        if (ObjUtil.isNotNull(order)) {
            order.setUsername("赵四");
            order.setTelephone("17766541438");
            order.setAddress("香坊区幸福小区9号楼");
            order.setOrderNo(RandomUtil.randomNumbers(12));
            order.setOrderTime(LocalDateTime.now());
        }

        log.info("【parseOrder】解析用户输入:{}", order);
        return order;
    }
}

ShopTool

java 复制代码
package com.joezhou.tool;
import com.joezhou.entity.Order;

/** @author 周航宇 */
@Component
@Slf4j
public class ShopTool {

    private final ChatClient chatClient;

    public ShopTool(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    /**
     * 根据用户的订单描述,匹配附近的商家信息
     *
     * @param order 用户输入的订单描述
     * @return 匹配到的商家信息
     */
    @Tool(name = "matchShop", description = "根据用户的订单描述,匹配附近的商家信息")
    public Shop matchShop(@ToolParam(description = "用户输入的订单描述") Order order) {
        log.info("【matchShop】获取订单描述:{}", order);

        // 调用大模型,提取评论的关键词
        Shop shop = chatClient.prompt()
                .user(("""
                         根据用户的订单描述 %s,随机生成一个商家信息,要求:
                         1. 必须生成商家名称,该商家的业务范围必须涵盖用户订单的商品
                         2. 必须生成商家地址,该地址必须在用户订单的地址附近,且需要包含省,市,区,街道信息
                         3. 必须生成商家联系电话,该联系电话必须是11位数字
                         4. 必须生成餐盒费,该餐盒费必须在10-50元之间随机生成
                         5. 必须生成配送费,该配送费必须在10-50元之间随机生成
                         6. 必须生成打包费,该打包费必须在10-50元之间随机生成
                         7. 必须生成一个商品价格,该商品价格必须在用户能接受的范围以内随机生成
                        """).formatted(order))
                .call()
                .entity(Shop.class);

        // 设置商品名称
        assert shop != null;
        shop.setProductName(order.getProductName());

        log.info("【matchShop】匹配商家信息:{}", shop);
        return shop;
    }
}

NotifyTool

java 复制代码
package com.joezhou.tool;
import com.joezhou.entity.Order;

/** @author 周航宇 */
@Component
@Slf4j
public class NotifyTool {
    /**
     * 根据订单信息和匹配到的商家信息,生成通知消息
     *
     * @param order 订单信息
     * @param shop  匹配到的商家信息
     * @return 通知消息
     */
    @Tool(name = "buildNotify", description = "根据订单信息和匹配到的商家信息,生成一张通知单")
    public String buildNotify(Shop shop, Order order) {
        log.info("【buildNotify】获取商家信息:{}", shop);
        log.info("【buildNotify】获取订单信息:{}", order);

        double productPrice = shop.getProductPrice();
        double boxFee = shop.getBoxFee();
        double deliveryFee = shop.getDeliveryFee();
        double packingFee = shop.getPackingFee();
        double totalFee = productPrice + boxFee + deliveryFee + packingFee;

        // 处理订单时间格式
        LocalDateTime orderTime = order.getOrderTime();
        String orderTimeStr = orderTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));

        String notify = """
                ********** #177 京东秒送 **********
                商家【%s】已接单
                **********************************
                顾客地址:%s
                %s
                %s
                ------------- 商品列表 ------------
                %s  【x1】   %s
                ------------- 其它费用 ------------
                餐盒费                【x1】   %s
                配送费                【x1】   %s
                打包费                【x1】   %s
                ------------- 总计费用 ------------
                总金额                         %s
                -----------------------------------
                订单编号:%s
                下单时间:%s
                ********** #177 京东秒送 **********
                 """.formatted(shop.getName(),
                order.getAddress(),
                DesensitizedUtil.chineseName(order.getUsername()),
                DesensitizedUtil.mobilePhone(order.getTelephone()),
                shop.getProductName(),
                productPrice,
                boxFee,
                deliveryFee,
                packingFee,
                totalFee,
                order.getOrderNo(),
                orderTimeStr
        );

        log.info("【buildNotify】生成通知单:\n{}", notify);
        return notify;
    }
}
  1. 开发配置类:

OrderAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/**  @author 周航宇 */
@Slf4j
@Configuration
public class OrderAgentConfig {

    private final String NAME = "orderAgent";
    private final String INSTRUCTION = """
             你是订单解析智能体,专门负责解析用户提供的订单信息。

             执行规则:
             1. 你必须调用 parseOrder 工具来解析用户订单信息,禁止自行编造或猜测订单内容。
             2. 如果用户消息中缺少必要信息,请明确告知用户需要补充什么信息。
             3. 解析完成后,直接返回工具返回的原始结果,不要额外解释或添加格式。
            """;

    @Bean(NAME)
    public ReactAgent orderAgent(ChatModel chatModel, OrderTool orderTool) {
        log.info("【初始化】子智能体 orderAgent 已创建");
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .tools(MethodToolCallbackProvider.builder().toolObjects(orderTool).build().getToolCallbacks())
                .build();
    }
}

ShopAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class ShopAgentConfig {

    private final String NAME = "shopAgent";
    private final String INSTRUCTION = """
             你是商家推荐智能体,专门负责根据用户提供的订单信息匹配合适商家。

             执行规则:
             1. 你必须调用 matchShop 工具来匹配合适的商家,禁止自行编造或猜测订单内容。
             2. 如果用户消息中缺少必要信息,请明确告知用户需要补充什么信息。
             3. 解析完成后,直接返回工具返回的原始结果,不要额外解释或添加格式。
            """;

    @Bean(NAME)
    public ReactAgent shopAgent(ChatModel chatModel, ShopTool shopTool) {
        log.info("【初始化】子智能体 shopAgent 已创建");
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .tools(MethodToolCallbackProvider.builder().toolObjects(shopTool).build().getToolCallbacks())
                .build();
    }
}

NotifyAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class NotifyAgentConfig {

    private final String NAME = "notifyAgent";
    private final String INSTRUCTION = """
             你是结果通知智能体,专门负责根据用户提供的订单信息和匹配到的商家,生成一张通知单。

             执行规则:
             1. 你必须调用 buildNotify 工具来生成通知单,禁止自行编造或猜测订单内容。
             2. 如果用户消息中缺少必要信息,请明确告知用户需要补充什么信息。
             3. 解析完成后,直接返回工具返回的原始结果,不要额外解释或添加格式。
            """;

    @Bean(NAME)
    public ReactAgent notifyAgent(ChatModel chatModel, NotifyTool notifyTool) {
        log.info("【初始化】子智能体 notifyAgent 已创建");
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .tools(MethodToolCallbackProvider.builder().toolObjects(notifyTool).build().getToolCallbacks())
                .build();
    }
}

SupervisorAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class SupervisorAgentConfig {

    private final String NAME = "supervisorAgent";
    private final String INSTRUCTION = """
            你是用户订单调度主管,必须严格按顺序执行:
            1. 第一步调用 orderAgent 子智能体,解析用户订单信息,返回 ["orderAgent"]
            2. 第二步调用 shopAgent 子智能体,根据用户订单匹配合适商家,返回 ["shopAgent"]
            3. 第三步调用 notifyAgent 子智能体,生成用户订单状态通知,返回 ["notifyAgent"]
            4. 最后返回 ["FINISH"]
            重要规则:
            5. 只返回 JSON 数组,无任何多余文字,不要解释,不要回答,只输出格式
            6. 上一步执行结果自动传递给下一步,无需手动传递
            """;

    @Bean(NAME)
    public SupervisorAgent supervisorAgent(ChatModel chatModel, @Qualifier("orderAgent") ReactAgent orderAgent, @Qualifier("shopAgent") ReactAgent shopAgent, @Qualifier("notifyAgent") ReactAgent notifyAgent) {

        // 创建主智能体
        ReactAgent mainAgent = ReactAgent.builder()
                .model(chatModel)
                .name("mainAgent")
                .instruction(INSTRUCTION)
                .saver(new MemorySaver())
                .build();
        log.info("【初始化】主智能体 mainAgent 已创建");

        // 创建调度智能体
        SupervisorAgent supervisor = SupervisorAgent.builder()
                .name(NAME)
                .mainAgent(mainAgent)
                // 添加子智能体
                .subAgents(List.of(orderAgent, shopAgent, notifyAgent))
                .build();

        log.info("【初始化】调度智能体 {} 已创建:成功加载 3 个子 Agent", NAME);
        return supervisor;
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;
import org.springframework.ai.chat.messages.Message;

/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/supervisorAgent")
public class SupervisorAgentController {

    private final SupervisorAgent supervisorAgent;

    public SupervisorAgentController(@Qualifier("supervisorAgent") SupervisorAgent supervisorAgent) {
        this.supervisorAgent = supervisorAgent;
    }

	@GetMapping("/chat")
    public Flux<String> chat(@RequestParam("msg") String msg) throws GraphRunnerException {
        // 执行 Agent,此时 Agent 会自动思考 + 调用对应工具
        return supervisorAgent.stream(msg)
                // 只保留 StreamingOutput 类型的 output(流程图开头和结尾是 NodeOutput 类型,需要过滤掉)
                .filter(output -> output instanceof StreamingOutput<?>)
                // output -> StreamingOutput.class
                .cast(StreamingOutput.class)
                // 只保留 Agent 模型的 output
                .filter(streamingOutput -> streamingOutput.getOutputType() == OutputType.AGENT_MODEL_STREAMING)
                // 只保留 AssistantMessage 类型的 output
                .filter(streamingOutput -> streamingOutput.message() instanceof AssistantMessage)
                // 提取 AssistantMessage 文本内容
                .mapNotNull(streamingOutput -> ((AssistantMessage) streamingOutput.message()).getText());
    }
}
  1. 测试控制器:
http 复制代码
### chat
GET http://localhost:13909/api/v1/supervisorAgent/chat?
    msg=我想买一个35元以内的香辣鸡腿堡

控制台日志解析

shell 复制代码
# ======================== 【项目启动阶段】========================
# 项目启动,Spring 容器初始化 3 个子智能体 Bean
SupervisorAgentConfig   : 【初始化】子智能体 orderAgent 已创建          # 订单解析Agent初始化完成
SupervisorAgentConfig   : 【初始化】子智能体 shopAgent 已创建           # 商家匹配Agent初始化完成
SupervisorAgentConfig   : 【初始化】子智能体 notifyAgent 已创建         # 通知生成Agent初始化完成

# 初始化 Supervisor 的大脑(mainAgent),负责调度决策
SupervisorAgentConfig   : 【初始化】主智能体 mainAgent 已创建           # 调度大脑创建完成

# 总调度器 Supervisor 装配完成,管理 3 个子Agent
SupervisorAgentConfig   : 【初始化】调度智能体 supervisorAgent 已创建:成功加载 3 个子 Agent

# ======================== 【接口请求进入】========================
# 框架线程池初始化(用于并行执行Agent)
ParallelNode     : Calculated core pool size: 40 (CPU cores: 20)       # 核心线程池:40
ParallelNode     : Calculated maximum pool size: 80 (CPU cores: 20)    # 最大线程池:80
ParallelNode     : Calculated queue capacity: 1000                     # 队列容量:1000

# ======================== 【第一步:调度 orderAgent】========================
# 进入 Supervisor 主调度大脑,开始第一轮决策
Invoking mainAgent 'mainAgent' compiled graph with threadId: Optional[subgraph_mainAgent]

# 调度大脑输出结果:下一步调用 orderAgent
MainAgentNodeAction: supervisor_next from last AssistantMessage = [orderAgent]

# 框架解析调度指令:确认调用 orderAgent
SupervisorNodeFromState: routingKey='supervisor_next', value='[orderAgent]', parsed agentNames=[orderAgent], validAgentNames=[orderAgent]

# 开始执行 orderAgent 内部工具:parseOrder(解析订单)
Starting execution of tool: parseOrder

# 订单工具接收用户输入
【parseOrder】获取用户输入:我想买一个35元以内的香辣鸡腿堡

# 订单工具解析完成,输出结构化 Order 对象
【parseOrder】解析用户输入:Order(username=赵四, telephone=17766541438, address=香坊区幸福小区9号楼, orderNo=410920627575, orderTime=2026-06-03T21:24:11.157492300, productName=香辣鸡腿堡, budget=35元以内)

# parseOrder 工具执行成功
Successful execution of tool: parseOrder

# 工具执行结果转为 JSON,存入状态机
Converting tool result to JSON.

# ======================== 【第二步:调度 shopAgent】========================
# 调度大脑进行第二轮决策
Invoking mainAgent 'mainAgent' compiled graph with threadId: Optional[subgraph_mainAgent]

# 调度大脑输出:下一步调用 shopAgent
MainAgentNodeAction: supervisor_next from last AssistantMessage = [shopAgent]

# 框架确认路由到 shopAgent
SupervisorNodeFromState: routingKey='supervisor_next', value='[shopAgent]', parsed agentNames=[shopAgent], validAgentNames=[shopAgent]

# 开始执行 shopAgent 内部工具:matchShop(匹配商家)
Starting execution of tool: matchShop

# 商家工具接收订单信息
【matchShop】获取订单描述:Order(username=赵四, telephone=17766541438, address=香坊区幸福小区9号楼, orderNo=410920627575, orderTime=2026-06-03T21:24:11, productName=香辣鸡腿堡, budget=35元以内)

# 商家工具生成并返回 Shop 对象
【matchShop】匹配商家信息:Shop(name=香辣鸡腿堡快餐店, address=黑龙江省哈尔滨市香坊区幸福路123号, telephone=13987654321, productName=香辣鸡腿堡, productPrice=29.9, boxFee=28.5, deliveryFee=15.0, packingFee=12.8)

# matchShop 工具执行成功
Successful execution of tool: matchShop

# 工具执行结果转为 JSON,存入状态机
Converting tool result to JSON.

# ======================== 【第三步:调度 notifyAgent】========================
# 调度大脑第三轮决策
Invoking mainAgent 'mainAgent' compiled graph with threadId: Optional[subgraph_mainAgent]

# 调度大脑输出:下一步调用 notifyAgent
MainAgentNodeAction: supervisor_next from last AssistantMessage = [notifyAgent]

# 框架确认路由到 notifyAgent
SupervisorNodeFromState: routingKey='supervisor_next', value='[notifyAgent]', parsed agentNames=[notifyAgent], validAgentNames=[notifyAgent]

# 开始执行 notifyAgent 内部工具:buildNotify(生成通知)
Starting execution of tool: buildNotify

# 通知工具获取 Shop 信息
【buildNotify】获取商家信息:Shop(name=香辣鸡腿堡快餐店, address=黑龙江省哈尔滨市香坊区幸福路123号, telephone=13987654321, productName=香辣鸡腿堡, productPrice=29.9, boxFee=28.5, deliveryFee=15.0, packingFee=12.8)

# 通知工具获取原始 Order 信息
【buildNotify】获取订单信息:Order(username=赵四, telephone=17766541438, address=香坊区幸福小区9号楼, orderNo=410920627575, orderTime=2026-06-03T21:24:11, productName=香辣鸡腿堡, budget=35元以内)

# 通知工具生成订单小票
【buildNotify】生成通知消息:XXXX

# buildNotify 工具执行成功
Successful execution of tool: buildNotify

# 工具执行结果转为 JSON,存入状态机
Converting tool result to JSON.

# ======================== 【第四步:结束流程】========================
# 调度大脑最后一次决策
Invoking mainAgent 'mainAgent' compiled graph with threadId: Optional[subgraph_mainAgent]

# 调度大脑输出:任务完成,返回 FINISH
MainAgentNodeAction   : MainAgentNodeAction: supervisor_next = FINISH from last AssistantMessage

# 框架识别到 FINISH,结束整个流程
MainAgentToSupervisorEdgeAction : MainAgentToSupervisorEdgeAction: routing to END as value for key 'supervisor_next' is finish or empty: [FINISH]

E02. 多智能体并行

武技:创建 springai-agent-parallel 子项目,并完成初始化工作。。

  1. 添加三方依赖:
xml 复制代码
<dependencies>
	<!--spring-boot-starter-web-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<!--spring-ai-alibaba-starter-dashscope-->
	<dependency>
		<groupId>com.alibaba.cloud.ai</groupId>
		<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
	</dependency>
	<!--spring-ai-alibaba-agent-framework-->
	<dependency>
		<groupId>com.alibaba.cloud.ai</groupId>
		<artifactId>spring-ai-alibaba-agent-framework</artifactId>
	</dependency>
</dependencies>
  1. 开发配置文件:
yaml 复制代码
server:
  port: 13910 # 端口号
  servlet:
    encoding:
      charset: utf-8 # 字符集(解决 stream 中文乱码)
      enabled: true # 启用字符编码(解决 stream 中文乱码)
      force: true # 强制使用字符编码(解决 stream 中文乱码)

spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY} # 阿里云百炼 API_KEY
      read-timeout: 100000 # 读取超时时间(毫秒)
      chat:
        options:
          model: qwen-plus # 基础对话模型
          max-tokens: 1000 # Token 限制
          temperature: 0.5 # 采样温度

logging:
  level:
    org.springframework.ai.chat.client.advisor: DEBUG # 开启 SimpleLoggerAdvisor 日志功能
  1. 开发启动类:
java 复制代码
package com.joezhou;

/** @author 周航宇 */
@SpringBootApplication
public class AgentParallelApp {
    public static void main(String[] args) {
        SpringApplication.run(AgentParallelApp.class, args);
    }
}

1. ParallelAgent

  1. 开发钩子类:
java 复制代码
package com.joezhou.hook;

/** @author 周航宇 */
@Scope("prototype") // 让每个 Agent 都有一个独立的钩子实例,否则会共享状态
@Slf4j
@Component
public class MyLogHook extends AgentHook {

    /**
     * 在 Agent 开始前调用,记录开始时间并初始化状态变量
     *
     * @param state  全局状态,包含用户输入、模型输出等信息
     * @param config 运行配置,包含模型选择、参数设置等
     * @return 包含自定义数据的 CompletableFuture,用于后续处理
     */
    @Override
    public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {
        long startTime = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(2L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return CompletableFuture.completedFuture(Map.of("agentStartTime", startTime));
    }

    /**
     * 在 Agent 结束后调用
     *
     * @param state  全局状态,包含用户输入、模型输出等信息
     * @param config 运行配置,包含模型选择、参数设置等
     * @return 包含自定义数据的 CompletableFuture,用于后续处理
     */
    @Override
    public CompletableFuture<Map<String, Object>> afterAgent(OverAllState state, RunnableConfig config) {
        // 获取请求开始时间
        long startTime = state.value("agentStartTime", 0L);
        log.info("【{}】执行结束,总耗时:{}ms", getAgentName(), System.currentTimeMillis() - startTime);
        // 返回空 CompletableFuture,表示没有后续处理需求
        return CompletableFuture.completedFuture(Map.of());
    }

    @Override
    public String getName() {
        return "MyLogHook";
    }
}
  1. 开发配置类:

ClothesAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class ClothesAgentConfig {

    private final String NAME = "clothesAgent";
    private final String INSTRUCTION = """
		    身份:穿搭专属助手小衣
            核心职责:从用户需求信息中提取目的地,然后调用天气工具查询实况气温与天气状况,结合天气给出精简穿搭方案。
                
            强制规则:
            1. 如果无法提取目的地,必须直接回答"无法提取目的地"并结束;
            2. 最终回答严格控制在20字以内,直接返回纯中文文本内容,不要返回JSON格式;
            3. 若工具查询失败/无城市数据,可合理模拟天气与穿搭方案回复;
            4. 只输出穿搭建议正文,不展示工具调用过程、原始JSON、思考步骤。
 
            正确示例1:你好
            正确示例2:无法提取目的地
            错误示例1:带双引号的内容
            错误示例2:JSON格式内容
            """;
    private final String OUTPUT_KEY = "CLOTHES_AGENT_RESULT";

    @Bean(NAME)
    public ReactAgent clothesAgent(ChatModel chatModel, MyLogHook myLogHook) {
        return ReactAgent.builder()
                .name(NAME)
                .model(chatModel)
                .instruction(INSTRUCTION)
                .outputKey(OUTPUT_KEY)
                .outputType(String.class)
                .hooks(List.of(myLogHook))
                .build();
    }
}

FoodAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class FoodAgentConfig {

    private final String NAME = "foodAgent";
    private final String INSTRUCTION = """
            身份:美食专属助手小食
            核心职责:从用户需求信息中提取目的地,然后调用美食工具查询实况美食,结合美食给出精简美食方案。
                
            强制规则:
            1. 如果无法提取目的地,必须直接回答"无法提取目的地"并结束;
            2. 最终回答严格控制在20字以内,直接返回纯中文文本内容,不要返回JSON格式;
            3. 若工具查询失败/无城市数据,可合理模拟美食方案回复;
            4. 只输出美食建议正文,不展示工具调用过程、原始JSON、思考步骤。
            
            正确示例1:你好
            正确示例2:无法提取目的地
            错误示例1:带双引号的内容
            错误示例2:JSON格式内容
            """;
    private final String OUTPUT_KEY = "FOOD_AGENT_RESULT";

    @Bean(NAME)
    public ReactAgent foodAgent(ChatModel chatModel, MyLogHook myLogHook) {
        return ReactAgent.builder()
                .name(NAME)
                .model(chatModel)
                .instruction(INSTRUCTION)
                .outputKey(OUTPUT_KEY)
                .outputType(String.class)
                .hooks(List.of(myLogHook))
                .build();
    }
}

HotelAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class HotelAgentConfig {

    private final String NAME = "hotelAgent";
    private final String INSTRUCTION = """
            身份:酒店专属助手小住
            核心职责:从用户需求信息中提取目的地,然后调用酒店工具查询实况酒店,结合酒店给出精简酒店方案。
                
            强制规则:
            1. 如果无法提取目的地,必须直接回答"无法提取目的地"并结束;
            2. 最终回答严格控制在20字以内,直接返回纯中文文本内容,不要返回JSON格式;
            3. 若工具查询失败/无城市数据,可合理模拟酒店方案回复;
            4. 只输出酒店建议正文,不展示工具调用过程、原始JSON、思考步骤。
            
            正确示例1:你好
            正确示例2:无法提取目的地
            错误示例1:带双引号的内容
            错误示例2:JSON格式内容
            """;
    private final String OUTPUT_KEY = "HOTEL_AGENT_RESULT";

    @Bean(NAME)
    public ReactAgent hotelAgent(ChatModel chatModel, MyLogHook myLogHook) {
        return ReactAgent.builder()
                .name(NAME)
                .model(chatModel)
                .instruction(INSTRUCTION)
                .outputKey(OUTPUT_KEY)
                .outputType(String.class)
                .hooks(List.of(myLogHook))
                .build();
    }
}

TravelAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class TravelAgentConfig {

    private final String NAME = "travelAgent";
    private final String INSTRUCTION = """
            身份:旅行专属助手小行
            核心职责:从用户需求信息中提取目的地,然后调用旅行工具查询实况旅行,结合旅行给出精简旅行方案。
                
            强制规则:
            1. 如果无法提取目的地,必须直接回答"无法提取目的地"并结束;
            2. 最终回答严格控制在20字以内,直接返回纯中文文本内容,不要返回JSON格式;
            3. 若工具查询失败/无城市数据,可合理模拟旅行方案回复;
            4. 只输出旅行建议正文,不展示工具调用过程、原始JSON、思考步骤。
            
            正确示例1:你好
            正确示例2:无法提取目的地
            错误示例1:带双引号的内容
            错误示例2:JSON格式内容
            """;
    private final String OUTPUT_KEY = "TRAVEL_AGENT_RESULT";

    @Bean(NAME)
    public ReactAgent travelAgent(ChatModel chatModel, MyLogHook myLogHook) {
        return ReactAgent.builder()
                .name(NAME)
                .model(chatModel)
                .instruction(INSTRUCTION)
                .outputKey(OUTPUT_KEY)
                .outputType(String.class)
                .hooks(List.of(myLogHook))
                .build();
    }
}

ParallelAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class ParallelAgentConfig {

    private final String NAME = "parallelAgent";

    @Bean(NAME)
    public ParallelAgent parallelAgent(
            @Qualifier("clothesAgent") ReactAgent clothesAgent,
            @Qualifier("foodAgent") ReactAgent foodAgent,
            @Qualifier("hotelAgent") ReactAgent hotelAgent,
            @Qualifier("travelAgent") ReactAgent travelAgent
    ) {
        log.info("【ParallelAgent】并行智能体初始化完成 → 4个任务同时跑");

        return ParallelAgent.builder()
                .name("并行处理器")
                .subAgents(List.of(clothesAgent, foodAgent, hotelAgent, travelAgent))
                .saver(new MemorySaver())
                .build();
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;

/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/parallelAgent")
public class ParallelAgentController {

    private final ParallelAgent parallelAgent;

    public ParallelAgentController(@Qualifier("parallelAgent") ParallelAgent parallelAgent) {
        this.parallelAgent = parallelAgent;
    }

    @GetMapping("chat")
    public Object chat(@RequestParam("msg") String msg) throws GraphRunnerException {
        log.info("【并行请求】用户需求:{}", msg);

        long start = System.currentTimeMillis();
        OverAllState state = parallelAgent.invoke(msg).orElseThrow();
        long end = System.currentTimeMillis();

        return Map.of(
                "助手小衣🥼建议", this.getText(state, "CLOTHES_AGENT_RESULT"),
                "助手小食🍔建议", this.getText(state, "FOOD_AGENT_RESULT"),
                "助手小住🏨建议", this.getText(state, "HOTEL_AGENT_RESULT"),
                "助手小行🚙建议", this.getText(state, "TRAVEL_AGENT_RESULT"),
                "总耗时(ms)", end - start
        );
    }

    public String getText(OverAllState state, String key) {
        return state.value(key, AssistantMessage.class)
                .orElseThrow()
                .getText();
    }
}
  1. 测试控制器:
http 复制代码
### chat
GET http://localhost:13910/api/v1/parallelAgent/chat?
    msg=2026年1月1号,想去哈尔滨冰雪大世界玩

E03. 多智能体路由

武技:创建 springai-agent-parallel 子项目,并完成初始化工作。。

  1. 添加三方依赖:
xml 复制代码
<dependencies>
	<!--spring-boot-starter-web-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<!--spring-ai-alibaba-starter-dashscope-->
	<dependency>
		<groupId>com.alibaba.cloud.ai</groupId>
		<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
	</dependency>
	<!--spring-ai-alibaba-agent-framework-->
	<dependency>
		<groupId>com.alibaba.cloud.ai</groupId>
		<artifactId>spring-ai-alibaba-agent-framework</artifactId>
	</dependency>
</dependencies>
  1. 开发配置文件:
yaml 复制代码
server:
  port: 13911 # 端口号
  servlet:
    encoding:
      charset: utf-8 # 字符集(解决 stream 中文乱码)
      enabled: true # 启用字符编码(解决 stream 中文乱码)
      force: true # 强制使用字符编码(解决 stream 中文乱码)

spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY} # 阿里云百炼 API_KEY
      read-timeout: 100000 # 读取超时时间(毫秒)
      chat:
        options:
          model: qwen-plus # 基础对话模型
          max-tokens: 1000 # Token 限制
          temperature: 0.5 # 采样温度

logging:
  level:
    org.springframework.ai.chat.client.advisor: DEBUG # 开启 SimpleLoggerAdvisor 日志功能
  1. 开发启动类:
java 复制代码
package com.joezhou;

/** @author 周航宇 */
@SpringBootApplication
public class AgentRoutingApp {
    public static void main(String[] args) {
        SpringApplication.run(AgentRoutingApp.class, args);
    }
}

1. LlmRoutingAgent

  1. 开发钩子类:
java 复制代码
package com.joezhou.hook;

/** @author 周航宇 */
/** @author 周航宇 */
@Scope("prototype") // 让每个 Agent 都有一个独立的钩子实例,否则会共享状态
@Slf4j
@Component
public class MyLogHook extends AgentHook {

    /**
     * 在 Agent 开始前调用,记录开始时间并初始化状态变量
     *
     * @param state  全局状态,包含用户输入、模型输出等信息
     * @param config 运行配置,包含模型选择、参数设置等
     * @return 包含自定义数据的 CompletableFuture,用于后续处理
     */
    @Override
    public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {
        String agentName = super.getAgentName();
        return CompletableFuture.completedFuture(Map.of("agentName", agentName));
    }

    /**
     * 在 Agent 结束后调用
     *
     * @param state  全局状态,包含用户输入、模型输出等信息
     * @param config 运行配置,包含模型选择、参数设置等
     * @return 包含自定义数据的 CompletableFuture,用于后续处理
     */
    @Override
    public CompletableFuture<Map<String, Object>> afterAgent(OverAllState state, RunnableConfig config) {
        log.info("【{}】执行结束", getAgentName());
        // 返回空 CompletableFuture,表示没有后续处理需求
        return CompletableFuture.completedFuture(Map.of());
    }

    @Override
    public String getName() {
        return "MyLogHook";
    }
}
  1. 开发配置类:

MemorySaverConfig

java 复制代码
package com.joezhou.config;

/** @author 周航宇 */
@Slf4j
@Configuration
public class MemorySaverConfig {

    @Bean("memorySaver")
    public MemorySaver memorySaver() {
        return new MemorySaver();
    }
}

CustomerServiceAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class CustomerServiceAgentConfig {

    private final String NAME = "customerServiceAgent";
    private final String INSTRUCTION = """
            身份:电商客服专员
            核心职责:回答用户对商品信息的提问。
                
            强制规则:
            1. 最终回答严格控制在100字以内,直接返回纯中文文本内容,不要返回JSON格式;
            2. 若无准确信息则合理模拟数据回复;
            3. 不展示工具调用过程、原始JSON、思考步骤。
             
            正确示例1:你好
            正确示例2:无法提取目的地
            错误示例1:带双引号的内容
            错误示例2:JSON格式内容
            """;
    private final String OUTPUT_KEY = "CUSTOMER_SERVICE_AGENT_RESULT";

    @Bean(NAME)
    public ReactAgent customerServiceAgent(ChatModel chatModel, MyLogHook myLogHook, MemorySaver memorySaver) {
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .outputKey(OUTPUT_KEY)
                .outputType(String.class)
                .hooks(List.of(myLogHook))
                .saver(memorySaver)
                .build();
    }
}

OrderSupportAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class OrderSupportAgentConfig {

    private final String NAME = "orderSupportAgent";
    private final String INSTRUCTION = """
            身份:电商订单专员
            核心职责:负责回答用户对订单的提问,包括修改发货地址和查询发货地址等。
                
            强制规则:
            1. 最终回答严格控制在100字以内,直接返回纯中文文本内容,不要返回JSON格式;
            2. 若无准确信息则合理模拟数据回复;
            3. 不展示工具调用过程、原始JSON、思考步骤。
             
            正确示例1:你好
            正确示例2:无法提取目的地
            错误示例1:带双引号的内容
            错误示例2:JSON格式内容
            """;
    private final String OUTPUT_KEY = "ORDER_SUPPORT_AGENT_RESULT";

    @Bean(NAME)
    public ReactAgent orderSupportAgent(ChatModel chatModel, MyLogHook myLogHook, MemorySaver memorySaver) {
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .outputKey(OUTPUT_KEY)
                .outputType(String.class)
                .hooks(List.of(myLogHook))
                .saver(memorySaver)
                .build();
    }
}

AfterSalesAgentConfig

java 复制代码
package com.joezhou.config;
import org.springframework.ai.chat.model.ChatModel;

/** @author 周航宇 */
@Slf4j
@Configuration
public class AfterSalesAgentConfig {

    private final String NAME = "afterSalesAgent";
    private final String INSTRUCTION = """
            身份:电商售后专员
            核心职责:回答用户对退货的提问。
                
            强制规则:
            1. 最终回答严格控制在100字以内,直接返回纯中文文本内容,不要返回JSON格式;
            2. 若无准确信息则合理模拟数据回复;
            3. 不展示工具调用过程、原始JSON、思考步骤。
             
            正确示例1:你好
            正确示例2:无法提取目的地
            错误示例1:带双引号的内容
            错误示例2:JSON格式内容
            """;
    private final String OUTPUT_KEY = "AFTER_SALES_AGENT_RESULT";

    @Bean(NAME)
    public ReactAgent afterSalesAgent(ChatModel chatModel, MyLogHook myLogHook, MemorySaver memorySaver) {
        return ReactAgent.builder()
                .model(chatModel)
                .name(NAME)
                .instruction(INSTRUCTION)
                .outputKey(OUTPUT_KEY)
                .outputType(String.class)
                .hooks(List.of(myLogHook))
                .saver(memorySaver)
                .build();
    }
}

RoutingAgentConfig

java 复制代码
package com.joezhou.config;

/** @author 周航宇 */
@Slf4j
@Configuration
public class RoutingAgentConfig {

    private final String NAME = "routingAgent";
    private final String INSTRUCTION = """
            身份:电商客服调度员
            核心职责:严格匹配问题分配对应专员,仅输出专员名称。
            调度规则:
            1. 尺码、面料、怎么用、搭配 → customerServiceAgent
            2. 查订单、修改收货地址、查询收货地址、取消订单 → orderSupportAgent
            3. 退货、退款、补差价 → afterSalesAgent
            强制规则:
            4. 禁止多余解释文字,只输出匹配的专员名字;
            5. 不展示工具调用过程、原始JSON、思考步骤。
            """;

    @Bean(NAME)
    public LlmRoutingAgent routingAgent(ChatModel chatModel,
                                        @Qualifier("customerServiceAgent") ReactAgent customerServiceAgent,
                                        @Qualifier("orderSupportAgent") ReactAgent orderSupportAgent,
                                        @Qualifier("afterSalesAgent") ReactAgent afterSalesAgent,
                                        MemorySaver memorySaver
    ) {
        log.info("【LlmRoutingAgent】电商售后路由智能体初始化完成");

        return LlmRoutingAgent.builder()
                .name(NAME)
                .model(chatModel)
                .subAgents(List.of(customerServiceAgent, orderSupportAgent, afterSalesAgent))
                // 路由判定规则,清晰约束大模型分发逻辑
                .description(INSTRUCTION)
                .saver(memorySaver)
                .build();
    }
}
  1. 开发控制器:
java 复制代码
package com.joezhou.controller;
import org.springframework.ai.chat.messages.Message;

/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/routingAgent")
public class RoutingAgentController {

    private final LlmRoutingAgent routingAgent;

    public RoutingAgentController(@Qualifier("routingAgent") LlmRoutingAgent routingAgent) {
        this.routingAgent = routingAgent;
    }
    
    @GetMapping(value = "/chat")
    public Flux<String> chat(@RequestParam("msg") String msg) throws GraphRunnerException {
        return routingAgent.streamMessages(msg).map(Message::getText);
    }
}
  1. 测试控制器:
http 复制代码
### 测试客服路由
GET http://localhost:13911/api/v1/routingAgent/chat?
    msg=这件纯棉外套怎么清洗不缩水

### 测试订单路由
GET http://localhost:13911/api/v1/routingAgent/chat?
    msg=把收货地址改成哈尔滨香坊区立汇小区

### 测试售后路由
GET http://localhost:13911/api/v1/routingAgent/chat?
    msg=衣服尺码不合适,我要退货退款

S08. 开发前端项目

武技:创建 v3-9-ssm-ai/springai-chat-web 项目。

  1. 使用 vite 创建 vue 项目:
shell 复制代码
# 切换到工作空间目录,注意路径中不要有中文
cd D:\workspace\java\v3-9-ssm-ai

# 创建Vue项目(第一遍安装需要输入y安装vite)
npm create vite@5.5.1 springai-chat-web -- --template vue

cd springai-chat-web

npm install

npm run dev
  1. 在 vite.config.js 文件中修改项目端口:
javascript 复制代码
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [vue()],

    server: {
        host: 'localhost',//ip地址
        port: 13999, // 设置服务启动端口号
    }
})

1. 添加Axios依赖

shell 复制代码
# 安装Axios组件组件
npm install axios@1.6.7 --save

2. 添加ElementPlus

  1. 安装 ElementPlus 依赖:
shell 复制代码
# 局部安装ElementPlus组件库
npm install element-plus@2.5.3 --save
  1. 在 main.js 文件中引入 ElementPlus 依赖:
javascript 复制代码
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';

// ElementPlus组件库: 核心对象,核心CSS,显隐CSS,国际化对象
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import 'element-plus/theme-chalk/display.css';
import {zhCn} from "element-plus/es/locale/index";

// 使用ElementPlus组件库
let app = createApp(App);
app.use(ElementPlus, {locale: zhCn});
app.mount('#app');

3. 开发交互页面

style.css

css 复制代码
body {
    margin: 0; /* 外边距 */
    padding: 20px 10%; /* 内边距 */
}

.el-timeline {
    background-color: #F5F5F5; /* 背景颜色 */
    padding-top: 20px; /* 内边距 */
    border-radius: 10px; /* 圆角 */
    box-sizing: border-box; /* 忽略边框和内边距影响 */
    height: 75vh; /* 75% 视窗高度 */
    overflow-y: auto; /* 超出部分显示滚动条 */
    padding-right: 40px !important; /* 内边距 */
}

.image {
    width: 200px; /* 宽度 */
    height: 200px; /* 高度 */
}

.send-ipt {
    height: 60px; /* 高度 */
    margin: 10px auto; /* 外边距 */
}

.userContent {
    background-color: #f1d2d2; /* 背景颜色 */
    display: inline-block; /* 内联块元素 */
    padding: 10px 20px; /* 内边距 */
    border-radius: 10px; /* 圆角 */
}

.btn-group {
    text-align: center; /* 居中对齐 */
    margin: 10px auto; /* 外边距 */
}

App.vue

html 复制代码
<template>
  <el-timeline class="timeline" id="timeline">
    <el-timeline-item v-for="e in activities" :key="e" :color="e.role === 'user' ? 'blue': 'green'"
                      :style="{textAlign: e.role === 'user' ? 'right': 'left'}" :timestamp="e.timestamp">
      <template #default>
        <div v-if="e.type === 'image'">
          <img class="image" :src="e.src" alt="示例图片"/>
          <br/>
          <span :class="e.role === 'user' ? 'userContent' : 'aiContent'">{{ e.content }}</span>
        </div>
        <div v-else>
          <span :class="e.role === 'user' ? 'userContent' : 'aiContent'">{{ e.content }}</span>
        </div>
      </template>
    </el-timeline-item>
  </el-timeline>

  <el-input class="send-ipt" v-model="msg" placeholder="请输入您的问题"/>

  <div class="btn-group">
    <el-button type="primary" @click="call()">基础对话</el-button>
    <el-button type="success" @click="stream()">流式对话</el-button>
    <el-button type="warning" @click="system()">预设角色</el-button>
    <el-button type="primary" @click="memoryAdvisor()">记忆助手</el-button>
    <el-button type="info" @click="myAdvisor()">我的助手</el-button>
    <el-button type="success" @click="toolCalling()">工具调用</el-button>
    <el-button type="warning" @click="rag()">检索增强</el-button>
    <el-button type="info" @click="ImageModel()">生成图片</el-button>
  </div>

</template>

<script setup>
import {ref} from "vue";
import axios from "axios";

// AI接口地址
const API_URL_PREFIX = 'http://localhost:15101/api/v1/';
const API_CALL_URL = API_URL_PREFIX + 'chatClient/call';
const API_STREAM_URL = API_URL_PREFIX + 'chatClient/stream';
const API_SYSTEM_URL = API_URL_PREFIX + 'systemRole/call';
const API_MEMORY_ADVISOR_URL = API_URL_PREFIX + 'memoryAdvisor/call';
const API_MY_ADVISOR_URL = API_URL_PREFIX + 'myAdvisor/call';
const API_TOOL_CALLING_URL = API_URL_PREFIX + 'toolCalling/call';
const API_RAG_URL = API_URL_PREFIX + 'rag/call';
const API_IMAGE_URL = API_URL_PREFIX + 'imageModel/generate';

// 设置超时时间为 300 秒,因为文生图比较慢,需要等待较长时间
const AJAX = axios.create({timeout: 300000});
// AI欢迎词配置
let activities = ref([{content: '我是AI,有何指教?', timestamp: now(), role: 'ai'}]);
// 用户输入的内容
let msg = ref('');
// AI是否正在回复中
let isReplying = false;
// 时间线对象
let timeline;

/*========== 对话生命周期函数 ==========*/

function beforeChat() {
  // 如果用户输入的内容为空,则不处理
  if (msg.value === '') return;
  // 如果AI正在回复中,则不处理
  if (isReplying) return;
  // 将AI回复中标记为true
  isReplying = true;
  // 加入用户输入的信息和AI回复的信息
  activities.value.push({content: msg.value, timestamp: now(), role: 'user'});
  activities.value.push({content: 'waiting...', timestamp: now(), role: 'ai'});
}

function afterChat(res) {
  // 始终滚动到底部
  timeline.scrollTop = timeline.scrollHeight - timeline.clientHeight;
  // 将AI回复中标记为false
  isReplying = false;
  if (res) {
    // 如果请求成功,则加入AI回复的信息
    if (res.status === 200) {
      activities.value[activities.value.length - 1].content = res.data;
    }
    // 如果请求失败,则加入错误信息提示
    else {
      activities.value[activities.value.length - 1].content = '请求失败,请稍后重试!';
    }
  }
}

/*========== 基础对话 ==========*/

async function call() {
  beforeChat();
  // 发送请求
  let res = await AJAX.get(`${API_CALL_URL}?msg=${msg.value}`);
  afterChat(res);
}

/*========== 流式对话 ==========*/

// SSE客户端对象:用于接收服务端推送的消息
let sse;

async function stream() {
  beforeChat();
  // 关闭上一个SSE连接
  if (sse) sse.close();
  // SSE服务端推送时
  sse = new EventSource(`${API_STREAM_URL}?msg=${msg.value}`);
  // SSE客户端接收到消息时
  sse.onmessage = (ev) => {
    // 如果读取到 [over] 结束标记,则关闭 SSE 连接,否则1秒执行一次
    if (ev.data === '[over]') {
      sse.close();
      isReplying = false;
      return;
    }
    // 拼接AI回复的信息
    activities.value[activities.value.length - 1].content += ev.data;
    // 始终滚动到底部
    timeline.scrollTop = timeline.scrollHeight - timeline.clientHeight;
  };
  // SSE连接成功时
  sse.onopen = () => activities.value[activities.value.length - 1].content = '';
}

/*========== 预设角色 ==========*/

async function system() {
  beforeChat();
  // 发送请求
  let res = await AJAX.get(`${API_SYSTEM_URL}?msg=${msg.value}`);
  afterChat(res);
}

/*========== 我的助手 ==========*/

async function myAdvisor() {
  beforeChat();
  // 发送请求
  let res = await AJAX.get(`${API_MY_ADVISOR_URL}?msg=${msg.value}`);
  afterChat(res);
}

/*========== 记忆助手 ==========*/

async function memoryAdvisor() {
  beforeChat();
  // 发送请求
  let res = await AJAX.get(`${API_MEMORY_ADVISOR_URL}?msg=${msg.value}&conversationId=chat001`);
  afterChat(res);
}

/*========== 工具调用 ==========*/

async function toolCalling() {
  beforeChat();
  // 发送请求
  let res = await AJAX.get(`${API_TOOL_CALLING_URL}?msg=${msg.value}`);
  afterChat(res);
}

/*========== 检索增强 ==========*/

async function rag() {
  beforeChat();
  // 发送请求
  let res = await AJAX.get(`${API_RAG_URL}?msg=${msg.value}`);
  afterChat(res);
}

/*========== 生成图片 ==========*/

async function ImageModel() {
  beforeChat();
  // 发送请求
  let res = await AJAX.get(`${API_IMAGE_URL}?msg=${msg.value}`, {responseType: 'blob'});
  // 如果请求成功,则加入AI回复的图片
  if (res.status === 200) {
    activities.value[activities.value.length - 1].type = 'image';
    activities.value[activities.value.length - 1].content = "";
    activities.value[activities.value.length - 1].content += "图片已生成!";
    activities.value[activities.value.length - 1].src = URL.createObjectURL(res.data);
  }
  afterChat();
}

// 获取当前时间
function now() {
  let now = new Date();
  return now.toLocaleDateString() + " " + now.toLocaleTimeString();
}

onload = () => timeline = document.querySelector("#timeline");

</script>

S09. 模型观察

心法:模型观察功能 Observability可以收集和分析 AI 运行时产生的数据,像模型处理请求的耗时、资源使用量、生成结果的质量等,通过这些数据,开发人员能知道 AI 系统是否正常运行、有没有性能问题,还能找到出错原因,方便优化和改进 AI 应用。

待续。

S10. 模型评估

心法:模型评估功能 AI Model Evaluation该功能就像一个质检员,专门负责检查 AI 生成的文本、图像等内容是否符合要求,有没有出现 AI 幻觉响应(输出看似逻辑通顺、实则完全错误的虚假内容),通过 AI 模型评估,我们可以及时发现这些问题,采取措施来改进 AI 模型,让它生成更准确、更有用的内容。

待续。

Java道经第3卷 - 第9阶 - SpringAI(二)

传送门:JB3-9-SpringAI(一)

传送门:JB3-9-SpringAI(二)

相关推荐
好家伙VCC1 小时前
Web Components主题热切换方案揭秘
java·前端
慕木沐2 小时前
Google ADK Java 1.0版本 核心机制与实战 Demo
java·开发语言·python
Tbisnic2 小时前
AI大模型学习第十一天:技术选型、安全防护与金融实战
python·学习·ai·大模型·提示词工程
AI工具挖掘机2 小时前
Codex 桌面版上手:从安装到自己开发首个小游戏,0 基础快速入门,手把手教学
经验分享·ai·ai编程
凉菜凉凉2 小时前
AI时代,被抛弃的前端
前端·ai
焦虑的说说3 小时前
秒杀系统设计方案
java
许彰午3 小时前
30_Java Stream流操作全解
java·windows·python
qq_2518364573 小时前
基于java Web网络订餐系统设计与实现 源码文档
java·开发语言·前端
凡人叶枫3 小时前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法