Java道经第3卷 - 第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 子项目,并完成初始化工作。。
- 添加三方依赖:
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>
- 开发主配文件:
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 基础日志
- 开发启动类:
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 完成与模型的基础对话交互。
- 开发记录类或实体类:(用于测试实体类响应效果):
java
package com.joezhou.record;
/** @author 周航宇 */
public record UserRecord(
@JsonPropertyDescription("用户姓名")
String name,
@JsonPropertyDescription("用户年龄")
Integer age,
@JsonPropertyDescription("用户自我介绍")
String info
) {}
- 开发配置类:
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();
}
}
- 开发控制器:
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);
}
}
- 测试控制器:
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 区分不同用户对话上下文。
- 开发配置类:
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();
}
}
- 开发控制器:
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);
}
}
- 测试控制器:
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,跨实例共享对话上下文。
- 开发配置类:
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();
}
}
- 开发控制器:
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);
}
}
- 测试控制器:
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 子项目,并完成初始化工作。。
- 添加三方依赖:
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>
- 开发配置文件:
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 工具调用日志
- 开发启动类:
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 的工具调用能力。
- 开发本地工具类:
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牌防晒衣。";
}
}
- 开发配置类:
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();
}
}
- 开发控制器:
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());
}
}
- 测试控制器:
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 全流程。
- 开发拦截器类:
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";
}
}
- 开发 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";
}
}
- 开发配置类:
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();
}
}
- 开发控制器:
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());
}
}
- 测试控制器:
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 开发。
- 开发 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);
}
}
- 在 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"}
- 开发配置类:
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();
}
}
- 开发控制器:
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());
}
}
- 测试控制器:
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 子项目,并完成初始化工作。。
- 添加三方依赖:
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>
- 开发配置文件:
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 基础日志
- 开发启动类:
java
package com.joezhou;
/** @author 周航宇 */
@SpringBootApplication
public class AgentSupervisorApp {
public static void main(String[] args) {
SpringApplication.run(AgentSupervisorApp.class, args);
}
}
1. SupervisorAgent
- 开发实体类:
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;
}
- 开发工具类:
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;
}
}
- 开发配置类:
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;
}
}
- 开发控制器:
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());
}
}
- 测试控制器:
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 子项目,并完成初始化工作。。
- 添加三方依赖:
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>
- 开发配置文件:
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 日志功能
- 开发启动类:
java
package com.joezhou;
/** @author 周航宇 */
@SpringBootApplication
public class AgentParallelApp {
public static void main(String[] args) {
SpringApplication.run(AgentParallelApp.class, args);
}
}
1. ParallelAgent
- 开发钩子类:
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";
}
}
- 开发配置类:
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();
}
}
- 开发控制器:
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();
}
}
- 测试控制器:
http
### chat
GET http://localhost:13910/api/v1/parallelAgent/chat?
msg=2026年1月1号,想去哈尔滨冰雪大世界玩
E03. 多智能体路由
武技:创建 springai-agent-parallel 子项目,并完成初始化工作。。
- 添加三方依赖:
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>
- 开发配置文件:
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 日志功能
- 开发启动类:
java
package com.joezhou;
/** @author 周航宇 */
@SpringBootApplication
public class AgentRoutingApp {
public static void main(String[] args) {
SpringApplication.run(AgentRoutingApp.class, args);
}
}
1. LlmRoutingAgent
- 开发钩子类:
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";
}
}
- 开发配置类:
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();
}
}
- 开发控制器:
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);
}
}
- 测试控制器:
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 项目。
- 使用 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
- 在 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
- 安装 ElementPlus 依赖:
shell
# 局部安装ElementPlus组件库
npm install element-plus@2.5.3 --save
- 在 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(二)