Java道经 - 项目 - MyItem - 智能业务
文章目录
- [S01. ChatClient对话](#S01. ChatClient对话)
-
- [E01. 基础对话](#E01. 基础对话)
-
- [1. 补充主配项](#1. 补充主配项)
- [2. 开发顾问器](#2. 开发顾问器)
- [3. 开发控制器](#3. 开发控制器)
- [E02. 增强对话(RAG)](#E02. 增强对话(RAG))
-
- [1. 补充主配项](#1. 补充主配项)
- [2. 开发配置类](#2. 开发配置类)
- [3. 开发RAG文档](#3. 开发RAG文档)
- [4. 加载RAG文档](#4. 加载RAG文档)
- [5. 开发控制器](#5. 开发控制器)
- [S02. Agent工具调用](#S02. Agent工具调用)
-
- [E01. 数据管理Agent](#E01. 数据管理Agent)
- [E02. 今日推荐Agent](#E02. 今日推荐Agent)
-
- [1. MCP配置](#1. MCP配置)
- [2. WeatherAgent](#2. WeatherAgent)
- [3. OutfitTool](#3. OutfitTool)
- [4. OutfitAgentConfig](#4. OutfitAgentConfig)
- [5. SupervisorAgentConfig](#5. SupervisorAgentConfig)
- [6. RecommendController](#6. RecommendController)
心法:MyItem 智能业务均在 com.joezhou.agent 包下开发,物理隔离。
S01. ChatClient对话
心法:本章节基于 ChatClient 对话客户端进行开发。
武技:完成智能对话的准备工作。
- 开发提示词模板文件:
classpath:prompt/ChatPrompt.st:
txt
# 角色定位
$role$
# 输入规则:
$input:{ rule |$i$、$rule$;
}$
# 输出规则:
$output:{ rule |$i$、$rule$;
}$
# 回答示例
- 正确示例:恭喜您...
- 错误示例:"恭喜您..."
- 错误示例:{"message": "恭喜您..."}
- 开发提示词模板工具类:用于加载提示词模板文件并创建 ST 对象:
java
package com.joezhou.agent.util;
/** @author 周航宇 */
@Slf4j
public class StUtil {
/** 模板分隔符开始字符 */
private static final char DELIMITER_START_CHAR = '$';
/** 模板分隔符结束字符 */
private static final char DELIMITER_STOP_CHAR = '$';
/** 资源加载器 */
private static final DefaultResourceLoader RESOURCE_LOADER = new DefaultResourceLoader();
/**
* 通过加载 .st 文件模板(使用 $ 分隔符)来构建 ST 实例
*
* @param location 资源路径 prompt/xxx.st
* @return ST 实例
*/
public static ST load(String location) {
try {
// 加载资源,支持 classpath 下的文件
Resource resource = RESOURCE_LOADER.getResource(location);
// 获取文件路径
Path path = resource.getFile().toPath();
// 读取文件内容
String content = Files.readString(path, StandardCharsets.UTF_8);
// 构建 ST 对象,使用 $ 和 $ 作为分隔符
return new ST(content, DELIMITER_START_CHAR, DELIMITER_STOP_CHAR);
} catch (Exception e) {
throw new RuntimeException("加载 classpath 下 .st 文件模板失败", e);
}
}
/**
* 构建内联字符串模板(使用 < 和 > 分隔符)
*
* @param templateText 模板文本
* @return ST 实例
*/
public static ST build(String templateText) {
// 构建 ST 对象,使用 < 和 > 作为分隔符(默认值)
return new ST(templateText);
}
}
E01. 基础对话
心法:基于 ChatClient 开发基础对话功能。
业务功能:
- 使用 ST 模板来构建的系统提示词。
- 使用 ChatOptions 来构建运行时配置,覆盖启动时配置。
- 附加 LogAdvisor 顾问器完成请求和响应的拦截和日志打印功能。
- 附加 RedisAdvisor 顾问器完成对话记忆功能(历史对话存入 Redis 中):
- 使用 userId 进行会话和记忆的隔离。
- 使用滑动窗口进行记忆滚动(最大窗口值为 10,即 5 轮对话)。
- 记忆过期时间 30 分钟。
- 使用流式对话,并约定自定义的结束标记 "over"。
武技:基于 ChatClient 开发基础对话功能。
1. 补充主配项
application.yml:添加 RedisAdvisor 相关的主项:
yaml
myitem:
memory:
key-prefix: "myitem:chat:memory" # Redis 键前缀
conversation-id: "conversationId" # 会话 ID 键
default-conversation-id: "default" # 默认会话 ID
max-messages: 10 # 最大消息数
expire: 30 # 过期时间(分钟)
2. 开发顾问器
开发 RedisAdvisor 记忆顾问器:
java
package com.joezhou.agent.advisor;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.Assert;
import java.util.concurrent.TimeUnit;
/** @author 周航宇 */
@Slf4j
@Component
@SuppressWarnings("all")
public class RedisAdvisor implements BaseAdvisor {
private final RedissonClient redissonClient;
public RedisAdvisor(RedissonClient redissonClient){
this.redissonClient = redissonClient;
}
/** Redis Key 前缀 */
@Value("${myitem.memory.key-prefix}")
private String KEY_PREFIX;
/** 对话 ID 键名 */
@Value("${myitem.memory.conversation-id}")
private String CONVERSATION_ID;
/** 默认对话 ID */
@Value("${myitem.memory.default-conversation-id}")
private String DEFAULT_CONVERSATION_ID;
/** 滑动窗口最大消息数(用户 + AI 各算 1 条) */
@Value("${myitem.memory.max-messages}")
private int MAX_MESSAGES;
/** 记忆过期时间(分钟):30 分钟无操作自动删除 */
@Value("${myitem.memory.expire}")
private long EXPIRE;
/**
* AI 调用前执行:
* 1. 从 Redis 中读取历史消息
* 2. 将本次用户消息加入到历史消息
* 3. 重新构建提示词和请求(包含最新的历史消息)
* 4. 返回新请求
*/
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
// 获取会话 ID
String conversationId = this.getConversationId(chatClientRequest.context(), DEFAULT_CONVERSATION_ID);
log.info("✅【RedisAdvisor】获取对话 ID:conversationId={}", conversationId);
// 通过会话 ID 从 Redis 读取历史消息【M】,该列表用于存取记忆消息
String key = KEY_PREFIX + conversationId;
RList<Message> memoryMessages = redissonClient.getList(key);
log.info("✅【RedisAdvisor】获取 Redis 历史消息:memoryMessages={}", memoryMessages);
log.info("✅【RedisAdvisor】获取 Redis 历史消息数量:memoryMessagesSize={}", memoryMessages.size());
// 创建消息列表【P】,内容和【M】一样,但功能不同,该列表仅用于更新提示词和请求
List<Message> processedMessages = new ArrayList<>(memoryMessages);
// 将本次的请求中的用户消息(列表类型)加入到消息列表【P】中
List<Message> userMessages = chatClientRequest.prompt().getInstructions();
log.info("✅【RedisAdvisor】获取本次用户消息:userMessages={}", userMessages);
processedMessages.addAll(userMessages);
// 更新本次请求(ChatClientRequest 是只读对象,必须使用 mutate 构建新对象)
ChatClientRequest processedChatClientRequest = chatClientRequest.mutate().prompt(
// 更新本次请求中的提示词(Prompt 是只读对象,必须使用 mutate 构建新对象)
chatClientRequest.prompt().mutate().messages(processedMessages).build()
).build();
// 从更新后的请求中获取用户消息(单条消息)加入到消息列表【M】中
UserMessage userMessage = processedChatClientRequest.prompt().getUserMessage();
memoryMessages.add(userMessage);
memoryMessages.expire(EXPIRE, TimeUnit.MINUTES);
log.info("✅【RedisAdvisor】更新 Redis 历史消息:将用户消息加入到 Redis 历史消息列表中");
log.info("✅【RedisAdvisor】更新 Redis 历史消息数量:memoryMessagesSize={}", memoryMessages.size());
// 返回处理后的本次请求对象
return processedChatClientRequest;
}
/**
* AI 调用后执行:
* 1. 从 Redis 中读取历史消息
* 2. 将本次 AI 消息加入到历史消息
* 3. 滑动窗口裁剪,只保留最新 MAX_MESSAGES 条消息
* 4. 刷新过期时间(每次请求自动续期)
* 5. 返回响应
*/
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
// 获取会话 ID
String conversationId = this.getConversationId(chatClientResponse.context(), DEFAULT_CONVERSATION_ID);
log.info("✅【RedisAdvisor】获取对话 ID:conversationId={}", conversationId);
// 通过会话 ID 从 Redis 读取历史消息
String key = KEY_PREFIX + conversationId;
RList<Message> memoryMessages = redissonClient.getList(key);
log.info("✅【RedisAdvisor】获取 Redis 历史消息:memoryMessages={}", memoryMessages);
log.info("✅【RedisAdvisor】获取 Redis 历史消息数量:memoryMessagesSize={}", memoryMessages.size());
// 获取 AI 回复消息
AssistantMessage assistantMessage = chatClientResponse.chatResponse().getResult().getOutput();
log.info("✅【RedisAdvisor】获取 AI 回复消息:assistantMessage={}", assistantMessage);
// 将 AI 回复的消息(单条消息)加入到历史消息中
memoryMessages.add(assistantMessage);
log.info("✅【RedisAdvisor】更新 Redis 历史消息:将 AI 回复消息加入到 Redis 历史消息列表中");
log.info("✅【RedisAdvisor】更新 Redis 历史消息数量:memoryMessagesSize={}", memoryMessages.size());
// 滑动窗口:只保留最新 MAX_MESSAGES 条,防止上下文过长
if (memoryMessages.size() > MAX_MESSAGES) {
List<Message> latestMessages = memoryMessages.subList(memoryMessages.size() - MAX_MESSAGES, memoryMessages.size());
memoryMessages.clear();
memoryMessages.addAll(latestMessages);
log.info("✅【RedisAdvisor】更新 Redis 历史消息:滑动窗口裁剪,保留最新 {} 条消息", MAX_MESSAGES);
}
// 刷新过期时间(每次请求自动续期)
memoryMessages.expire(EXPIRE, TimeUnit.MINUTES);
log.info("✅【RedisAdvisor】刷新 Redis 历史消息过期时间");
// 返回响应
return chatClientResponse;
}
/** Advisor 唯一名称 */
@Override
public String getName() {
return "RedisAdvisor";
}
/** 执行顺序:数字越小,越先执行 */
@Override
public int getOrder() {
return 0;
}
/** 获取会话 ID */
private String getConversationId(Map<String, Object> context, String defaultConversationId) {
Assert.notNull(context, "context cannot be null");
Assert.noNullElements(context.keySet().toArray(), "context cannot contain null keys");
Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
// 若请求中包含 key="conversationId",则返回对应的 value
// 若不包含,则返回默认的 "default":参考 ChatMemory → DEFAULT_CONVERSATION_ID
return context.containsKey(CONVERSATION_ID) ? context.get(CONVERSATION_ID).toString() : defaultConversationId;
}
}
3. 开发控制器
java
package com.joezhou.agent.controller;
import io.swagger.v3.oas.annotations.tags.Tag;
/** @author 周航宇 */
@RestController
@Tag(name = "AI智能聊天专员")
@RequestMapping("/api/v1/client")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder chatClientBuilder, RedisAdvisor redisAdvisor) {
// 创建系统提示词模板
ST st = StUtil.load("classpath:prompt/ChatPrompt.st");
st.add("role", "你是一个聊天助手,负责用最精炼的语言和用户进行沟通");
st.add("input", List.of("必须基于用户的输入来生成回复"));
st.add("output", List.of("只能输出纯中文文本,严禁输出JSON、代码块或其他格式"));
// 构建模型配置(运行时配置)
ChatOptions chatOptions = ChatOptions.builder()
.model("qwen-max") // 设置模型名称
.maxTokens(100) // 设置最大令牌数(100 个字符)
.temperature(0.7) // 设置温度(0.7 = 中等风险)
.topK(50) // 设置 Top-K 滤波(50 个候选)
.topP(0.9) // 设置 Top-P 滤波(0.9 = 90% 概率)
.build(); // 构建模型配置
// 创建日志顾问器(自定义需要打印的请求日志内容,响应日志内容和优先级)
SimpleLoggerAdvisor loggerAdvisor = new SimpleLoggerAdvisor(
req -> "✅【LogAdvisor】提示词:" + req.prompt().getContents(), // 拦截请求,但只打印提示词文本
res -> "✅【LogAdvisor】AI回复:" + res.getResult().getOutput().getText(), // 拦截响应,但只打印 AI 回复的内容
0 // 执行优先级:数字越小越先执行(0 = 最高优先级)
);
// 创建对话客户端
this.chatClient = chatClientBuilder
.defaultSystem(st.render()) // 设置默认系统提示
.defaultOptions(chatOptions) // 设置默认模型配置
.defaultAdvisors(loggerAdvisor, redisAdvisor) // 附加顾问器
.build();
}
@Operation(summary = "AI对话")
@GetMapping("chat")
public Flux<String> chat(@RequestParam("input") String input, HttpServletRequest request) {
// 取出用户的 ID
long userId = Long.parseLong(request.getAttribute("userId").toString());
// 流式发送消息(将响应结果以流的形式推送给前端)
return chatClient.prompt()
.user(input) // 设置用户消息
.advisors(e -> e.param("conversationId", userId)) // 设置对话 ID:相同 ID 共享同一套记忆
.stream() // 流式发送消息(将响应结果以流的形式推送给前端)
.content() // 获取响应内容
.concatWith(Flux.just("[over]")); // 约定结束标记,方便前端灵活终止 SSE 推送
}
}
E02. 增强对话(RAG)
心法:基于 ChatClient 开发 RAG 增强对话功能。
业务功能:
- 使用 ST 模板来构建的系统提示词。
- 使用 ETL 加载收纳相关的知识文档。
- 附加 LogAdvisor 顾问器完成请求和响应的拦截和日志打印功能。
- 附加 QuestionAnswerAdvisor 顾问器完成基于向量库相似度搜索的问答功能。
- 使用流式对话,并约定自定义的结束标记 "over"。
武技:基于 ChatClient 开发 RAG 增强对话功能。
1. 补充主配项
application.yml:添加 RedisStack 相关的主项:
yaml
myitem:
redis-stack:
host: 192.168.40.77 # 主机
port: 5379 # 端口
password: joezhou # 密码
prefix: "myitem:rag:" # 键前缀,RedisStack 存值取值时,均会自动使用该前缀
2. 开发配置类
java
package com.joezhou.agent.config;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.beans.factory.annotation.Value;
/** @author 周航宇 */
@Slf4j
@Configuration
public class RedisVectorStoreConfig {
@Value("${myitem.redis-stack.host}")
private String REDIS_HOST;
@Value("${myitem.redis-stack.port}")
private int REDIS_PORT;
@Value("${myitem.redis-stack.password}")
private String REDIS_PASSWORD;
@Value("${myitem.redis-stack.prefix}")
private String REDIS_PREFIX;
@Bean("redisVectorStore")
public VectorStore redisVectorStore(EmbeddingModel embeddingModel) {
// 创建独立 Redis Stack 连接
JedisPooled jedisPooled = new JedisPooled(REDIS_HOST, REDIS_PORT, null, REDIS_PASSWORD);
// 创建 RedisVectorStore 实例
return RedisVectorStore.builder(jedisPooled, embeddingModel)
.prefix(REDIS_PREFIX)
.initializeSchema(true) // 自动创建索引
.build();
}
}
3. 开发RAG文档
classpath:rag/knowledge.txt:
txt
1. **先"断舍离",后收纳**:整理的第一步永远是清理不需要的物品,而不是买收纳盒。
2. **80/20 法则**:每个空间只填满 80%,留出 20% 的弹性空间,让物品有"呼吸"的余地。
3. **三成展示、七成隐藏**:露出 30% 的美观物品,藏起 70% 的杂乱杂物,视觉最平衡。
4. **同类相聚**:把同类物品放在一起,是收纳的最基本原则。
5. **动线优先**:物品应放在使用它的地方附近,顺着生活习惯来收纳。
6. **黄金区域**:最常用的物品放在视线到腰部之间(离地 60-180cm),取用最省力。
7. **垂直收纳**:与其平铺,不如向上发展,利用墙面和层高。
8. **可视化管理**:尽量用透明容器,一眼就能看到里面装了什么。
9. **贴标签**:给收纳盒、文件夹贴上标签,全家人都知道东西在哪、该放回哪。
10. **定期维护**:收纳不是一次性的,每周或每月花点时间维持。
11. **清空**:把一个柜子或抽屉彻底清空。
12. **清洁**:趁清空时把内部擦干净。
13. **分类**:把物品分成三大类:保留、丢弃、待定。
14. **三袋法**:准备三个袋子,分别装"要留的"、"要捐/卖的"、"要扔的"。
15. **一年法则**:超过一年没用过的东西,可以考虑处理掉。
16. **心动法则**:只留下让你心动的物品(来自近藤麻理惠的整理魔法)。
17. **拍照留存**:对有纪念意义但不再使用的物品拍照,然后处理掉实物。
18. **从大到小**:先整理大型物品(家具、家电),再处理小物件。
19. **从多到少**:先整理数量最多的品类(如衣服、书),效果最明显。
20. **一次一个区域**:不要试图一次性整理全屋,逐个击破更有成就感。
21. **接受不完美**:不必追求杂志封面般的整齐,顺手好用最重要。
22. **收纳是治愈的过程**:把整理看作放松心情的方式,而非负担。
23. **告别完美主义**:"糊弄式收纳"也是一种智慧,怎么舒服怎么来。
24. **从每天 10 分钟开始**:养成每天小整理的习惯,避免积重难返。
25. **家人的参与**:让家人也参与到收纳中,尤其是孩子的玩具区。
26. **换位整理**:如果舍不得扔自己的东西,试试和家人交换整理,旁观者清。
27. **寻找灵感**:多看家居美图,给自己整理的动力。
28. **享受过程**:把整理当作与物品对话的过程。
29. **不盲目跟风**:不要因为网红推荐就买一堆收纳用品,先看看家里有什么可用的。
30. **以终为始**:想清楚你想要什么样的生活空间,再动手整理。
31. **一进一出**:买一件新东西,就处理掉一件旧东西。
32. **临时放置区**:在玄关或客厅设一个中转站,放暂时没空处理的物品。
33. **就地取材**:不花钱买收纳工具,用废旧物品改造。
34. **颜色标签**:用不同颜色的标签区分季节或类别(如蓝色=秋冬,黄色=春夏)。
35. **利用门后**:门后空间是极佳的隐形收纳区。
36. **向上借空间**:天花板附近的区域也可以开发成收纳空间。
37. **向下借空间**:床底、沙发底也是天然的收纳区。
38. **转角不浪费**:房间转角可以做成转角柜或展示架。
39. **缝隙利用**:冰箱和橱柜之间的缝隙,可以用窄缝收纳架塞满。
40. **隐藏式设计**:把收纳功能融入家具中(如可储物的床、茶几)。
41. **抽拉式收纳**:在狭窄或深处空间使用抽拉设计,方便取物。
42. **立式收纳**:用窄柜或置物架利用垂直角落。
43. **统一容器**:选用统一颜色、材质的收纳盒,视觉更整洁。
44. **视觉焦点**:在收纳区设置一个视觉焦点(如一个漂亮的摆件),提升美感。
45. **奇数原则**:摆放装饰品时,3 个比 4 个看起来更顺眼。
46. **可堆叠**:选择可以堆叠的收纳箱,充分利用高度。
47. **带轮子**:给大收纳箱装上轮子,移动方便。
48. **美型收纳**:把收纳本身变成装饰,比如用漂亮的瓶子装食材。
49. **借光线**:在收纳区增加灯光,让物品看起来更高级。
50. **一致性**:保持收纳风格与家居风格一致,避免凌乱感。
51. **鞋柜隔板**:用可调节隔板,把鞋柜空间利用到极致。
52. **鞋托**:用双层鞋托,一双鞋的空间能放两双。
53. **换鞋凳**:选择带收纳功能的换鞋凳,放常用拖鞋。
54. **伞架**:门后放吸盘式伞架,长柄折伞都能收纳,还不用担心滴水。
55. **洞洞板**:玄关墙面装洞洞板,挂帽子、包包、钥匙。
56. **挂钩**:进门处设一排挂钩,次净衣、外套随手挂。
57. **托盘**:在鞋柜台面放托盘,收纳钥匙、零钱、门卡。
58. **抽屉分隔**:玄关抽屉用分隔盒,放口罩、纸巾、拆快递刀。
59. **铁皮窄柜**:门后放铁皮窄柜,划分落尘区,收纳外衣鞋子。
60. **隐形储藏室**:如果空间允许,在玄关旁规划一个迷你储藏室,放行李箱、工具、包裹。
61. **墙面挂钩**:利用墙面空间挂包包、帽子、围巾。
62. **鞋柜底部悬空**:鞋柜底部留空 15cm,放常穿的鞋子,进门一脚踢进去。
63. **统一鞋盒**:用统一的透明鞋盒收纳换季鞋子,叠放整齐。
64. **换季区**:鞋柜上层放当季不常穿的鞋,下层放当季常穿的。
65. **雨伞沥水**:雨伞收纳区下方放一个托盘接水,保护地板。
66. **壁挂置物盒**:在门后装壁挂置物盒,收纳零碎小物。
67. **镜子+收纳**:选带小置物架的穿衣镜,放梳子、发胶。
68. **挂烫机收纳**:如果玄关有空间,可以收纳手持挂烫机,出门前熨一下。
69. **宠物用品区**:在玄关低处设宠物出门用品区(牵引绳、拾便袋)。
70. **快递工具区**:在玄关抽屉里放拆快递用的剪刀、美工刀。
71. **季节性鞋履**:长靴、雪地靴放在鞋柜最上层或最下层。
72. **儿童鞋区**:有孩子的家庭,在玄关低处给孩子一个专属鞋位。
73. **访客拖鞋**:用收纳篮装几双访客拖鞋,放在鞋柜旁。
74. **清洁工具**:在玄关柜内挂一个小扫把或粘毛滚,出门前整理衣物。
75. **信件架**:在玄关墙面装一个小信件架,放待拆的信件和账单。
76. **小物篮**:在鞋柜台面放一个编织篮,随手扔钥匙、门禁卡。
77. **穿鞋辅助**:在换鞋凳旁放一个鞋拔,收纳在挂钩上。
78. **香薰**:在玄关收纳区放一个香薰,进门心情愉悦。
79. **磁吸收纳**:在铁门上贴磁吸挂钩或磁吸置物架。
80. **利用鞋盒盖**:把鞋盒盖钉在墙上,做成小置物架(放钥匙等)。
81. **折叠伞**:折叠伞可以统一放在一个长条收纳盒里。
82. **鞋油鞋刷**:在鞋柜里用一个小盒收纳鞋油鞋刷。
83. **季节性物品**:围巾、手套、帽子在玄关用收纳篮集中管理。
84. **背包区**:在玄关墙面设置背包专用挂钩区。
85. **折叠凳**:在玄关放一个可折叠的凳子,不占地方。
86. **墙面衣帽架**:选择设计感强的墙面衣帽架,兼具装饰和收纳。
87. **雨衣收纳**:雨衣折叠好放在玄关柜抽屉里。
88. **鞋拔挂钩**:把鞋拔挂在门后挂钩上,方便取用。
89. **宠物牵引绳**:在门边低处装挂钩,挂宠物牵引绳。
90. **玄关地毯**:在玄关铺一块易清洁的地毯,阻挡灰尘入屋。
91. **钥匙挂钩**:在进门处装一个钥匙专用挂钩,出门不会忘。
92. **口罩收纳盒**:在玄关放一个口罩收纳盒,出门随手拿。
93. **小盆栽**:在玄关置物架上放一小盆绿植,增添生气。
94. **信报箱**:如果信件多,可以在玄关装一个小型信报箱。
95. **折叠雨伞架**:选带集水盘的折叠雨伞架,美观又实用。
96. **增高架**:在鞋柜台面上放增高架,增加一层置物空间。
97. **壁挂式鞋架**:在墙面装壁挂式折叠鞋架,节省地面空间。
98. **留言板**:在玄关墙面装一块小黑板或软木板,留言备忘。
99. **证件区**:在玄关抽屉里用一个文件袋收纳所有家庭成员的证件复印件。
100. **次净衣区**:在玄关设一个次净衣挂放区,穿过但不脏的外套有地方放。
4. 加载RAG文档
使用单元测试方法,加载 RAG 文档到向量库中:
java
package rag;
import org.junit.Test;
import org.springframework.ai.document.Document;
import org.springframework.core.io.ClassPathResource;
/** @author 周航宇 */
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MyItemApp.class)
public class LoadTest {
@Resource
private VectorStore redisVectorStore;
@Test
public void loadKnowledge() {
// 使用 TikaDocumentReader 读取文档
List<Document> documents = new TikaDocumentReader(new ClassPathResource("rag/knowledge.txt")).read();
log.info("✅读取 {} 条文档", documents.size());
// 使用 TokenTextSplitter 切分文档
List<Document> chunks = TokenTextSplitter.builder()
.build()
.apply(documents);
log.info("✅切分 {} 条文档", chunks.size());
// 存入向量数据库,这个过程会自动调用 embeddingModel 将文本变成向量再存入向量库
redisVectorStore.add(chunks);
log.info("✅存储 {} 条文档", chunks.size());
}
}
5. 开发控制器
java
package com.joezhou.agent.controller;
import io.swagger.v3.oas.annotations.tags.Tag;
/** @author 周航宇 */
@Slf4j
@RestController
@Tag(name = "AI收纳技巧专员")
@RequestMapping("/api/v1/rag")
public class RagController {
private final ChatClient chatClient;
public RagController(ChatClient.Builder chatClientBuilder, @Qualifier("redisVectorStore") VectorStore redisVectorStore) {
// 创建系统提示词模板
ST st = StUtil.load("classpath:prompt/ChatPrompt.st");
st.add("role", "你是一个聊天助手,负责根据 RAG 知识库内容用最精炼的语言和用户进行沟通");
st.add("input", List.of("所有回答必须基于向量数据库中检索到相关知识进行分析,扩展和总结,并生成回复"));
st.add("output", List.of(
"只能输出纯中文文本,严禁输出JSON、代码块或其他格式",
"若 RAG 知识库中不存在相关知识,直接返回 "我不清楚""));
// 创建日志顾问器(自定义需要打印的请求日志内容,响应日志内容和优先级)
SimpleLoggerAdvisor loggerAdvisor = new SimpleLoggerAdvisor(
req -> "✅【LogAdvisor】提示词:" + req.prompt().getContents(),
res -> "✅【LogAdvisor】AI回复:" + res.getResult().getOutput().getText(),
0
);
// 创建问答顾问器
QuestionAnswerAdvisor questionAnswerAdvisor = QuestionAnswerAdvisor.builder(redisVectorStore)
.searchRequest(SearchRequest.builder()
.topK(2)
.similarityThreshold(0.0)
.build())
.build();
// 创建对话客户端
this.chatClient = chatClientBuilder
.defaultSystem(st.render())
.defaultAdvisors(loggerAdvisor, questionAnswerAdvisor)
.build();
}
@Operation(summary = "AI对话")
@GetMapping("chat")
public Flux<String> chat(@RequestParam("input") String input) {
// 流式发送消息(将响应结果以流的形式推送给前端)
return chatClient.prompt()
.user(input)
.stream()
.content()
.concatWith(Flux.just("[over]"));
}
}
S02. Agent工具调用
心法:本章节基于 ReactAgent 进行开发。
开发 Skills 注册器:
java
package com.joezhou.agent.config;
/** @author 周航宇 */
@Configuration
@Slf4j
public class SkillConfig {
private static final String CACHE_PATH = "D:/workspace/java/my-item-backend/cache";
/**
* 配置 Skill 注册器,该注册器用于扫描,加载和管理指定目录下的 SKILL.md
* 让 Agent 知道他有哪些技能包可以用
*
* @return Skill 注册器,全局可用,推荐单独配 bean,加入 Spring 管理,而非局部创建
*/
@Bean
public SkillRegistry skillRegistry() {
// 清理缓存
log.info("✅清理缓存:{}", FileUtil.del(CACHE_PATH));
// 注册 SKILL.md 文件
return ClasspathSkillRegistry.builder()
// 指定扫描路径,默认 resources/skills/ 中的全部 SKILL.MD 文件
.classpathPath("skills")
// 指定缓存目录,默认生成 D:/tmp 目录并使用
.basePath(CACHE_PATH)
.build();
}
}
开发钩子,用于记录 Agent 调用日志:
java
package com.joezhou.agent.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) {
String agentName = this.getAgentName();
log.info("✅【{}】开始执行", agentName);
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("✅【{}】执行结束", this.getAgentName());
// 返回空 CompletableFuture,表示没有后续处理需求
return CompletableFuture.completedFuture(Map.of());
}
@Override
public String getName() {
return "MyLogHook";
}
}
E01. 数据管理Agent
心法:基于 ReactAgent + LlmRoutingAgent 开发数据管理功能。
业务功能:
- 使用 LlmRoutingAgent 完成 Agent 的路由功能:
- 物品管理 → ItemAgent。
- 房间管理 → RoomAgent。
- 附加 LogAgentHook 完成 Agent 调用过程的日志打印功能。
- 附加 SkillsAgentHook 完成本地工具注册功能。
- 使用流式对话,并约定自定义的结束标记 "over"。
业务流程 - 图示:

心法:基于 ReactAgent + LlmRoutingAgent 开发数据管理功能。
1. ItemTool
java
package com.joezhou.agent.tool;
import static com.joezhou.entity.table.ItemCategoryTableDef.ITEM_CATEGORY;
import static com.joezhou.entity.table.ItemTableDef.ITEM;
/** @author 周航宇 */
@Slf4j
@Component
public class ItemTool {
@Resource
private ItemMapper itemMapper;
@Resource
private ItemCategoryMapper itemCategoryMapper;
@Tool(name = "queryItemDetail", description = "根据物品名称查询该物品的完整详细信息")
public String queryItemDetail(
@ToolParam(description = "用户ID") String userId,
@ToolParam(description = "物品名称") String itemName) {
log.info("✅调用 queryItemDetail,参数:userId={}, itemName={}", userId, itemName);
// 查询物品详细信息
Item item = QueryChain.of(itemMapper)
.where(ITEM.ITEM_NAME.like(itemName))
.and(ITEM.USER_ID.eq(userId))
.one();
if (ObjectUtil.isNull(item)) {
return "未查询到物品详细信息" + itemName;
}
// 查询物品详细信息
String itemCategoryName = QueryChain.of(itemCategoryMapper)
.select(ITEM_CATEGORY.CATEGORY_NAME)
.where(ITEM_CATEGORY.ID.eq(item.getItemCategoryId()))
.objAs(String.class);
// 处理查询结果
String result = """
名称:%s<br/>
类别:%s<br/>
标签:%s<br/>
简介:%s<br/>
价格:%s元<br/>
库存:%s<br/>
使用率:%s颗星<br/>
性价比:%s颗星<br/>
购入日期:%s<br/>
过期日期:%s<br/>
物品状态:%s<br/>
状态变更:%s<br/>
物品去向:%s<br/>
去向变更:%s<br/>
""".formatted(
item.getItemName(),
itemCategoryName,
item.getTags(),
item.getSummary(),
item.getPrice(),
item.getCount(),
item.getUsageScore(),
item.getValueScore(),
ObjectUtil.isNull(item.getBuyDate()) ? "无" : item.getBuyDate().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")),
ObjectUtil.isNull(item.getExpireDate()) ? "无" : item.getExpireDate().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")),
this.formatItemStatus(item.getItemStatus()),
ObjectUtil.isNull(item.getStatusChangeTime()) ? "无" : item.getStatusChangeTime().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分")),
this.formatItemOutcome(item.getItemOutcome()),
ObjectUtil.isNull(item.getOutcomeChangeTime()) ? "无" : item.getOutcomeChangeTime().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分"))
);
log.info("✅调用 queryItemDetail,结果:result={}", result);
return result;
}
private String formatItemStatus(Integer itemStatusCode) {
if (0 == itemStatusCode) return "服役中";
if (1 == itemStatusCode) return "闲置中";
if (2 == itemStatusCode) return "出借中";
if (3 == itemStatusCode) return "维修中";
return "未知状态";
}
private String formatItemOutcome(Integer itemOutcomeCode) {
if (0 == itemOutcomeCode) return "持有中";
if (1 == itemOutcomeCode) return "已消耗";
if (2 == itemOutcomeCode) return "已丢弃";
if (3 == itemOutcomeCode) return "已丢失";
if (4 == itemOutcomeCode) return "已转赠";
if (5 == itemOutcomeCode) return "已售出";
return "未知状态";
}
}
2. SKILL.MD
classpath:skills/item-skills/SKILL.MD:
txt
---
name: item-skills
description: 物品管理业务技能,提供物品详情查询、物品位置查询等能力。
version: 1.0
author: 周航宇
tags: 物品查询,物品位置
tools:
- queryItemDetail
- queryItemPosition
---
# 物品管理技能
## 技能定位
作为个人物品系统专属助手,统一处理查询物品详情与查询物品位置两类核心业务操作,严格按照业务规则调用对应工具完成诉求。
## 启用范围
当用户提问包含以下关键词及相关语义时,启用本技能:
- 查询物品详情:XXX → 提取参数 itemName = XXX → 调用 queryItemDetail(itemName=XXX) 工具
- 查询物品位置:XXX → 提取参数 itemName = XXX → 调用 queryItemPosition(itemName=XXX) 工具
## 工具详情
### 1. queryItemDetail(查询物品详情)
- 功能描述:根据物品名称查询该物品的完整详细信息。
- 入参描述:
- itemName(物品名称,必填,1-128字符)
- 返回格式:字符串(物品详情描述)
- 执行逻辑:直接调用工具并返回结果。
### 2. queryItemPosition(查询物品位置)
- 功能描述:根据物品名称查询该物品的位置。
- 入参描述:
- itemName(物品名称,必填,1-128字符)
- 返回格式:字符串(物品位置描述)
- 执行逻辑:直接调用工具并返回结果。
## 行为规范
1. 工具隔离:严格区分工具职责,禁止跨场景混用工具;
2. 参数校验:调用工具前逐项校验必填参数,参数缺失时明确告知用户并引导补全;
3. 工具范围:仅使用本技能声明的工具,不调用技能外其他能力;
4. 返回规范:所有正常执行结果直接返回文本,不使用JSON格式;
5. 数据安全:禁止编造参数或结果数据,严格依赖工具返回结果,如实返回工具执行结果;
6. 禁用缓存:每次都必须调用工具,禁止使用缓存或记忆中的数据;
## 交互示例
### 正常示例01
用户请求:查询物品详情:苹果手机
工具调用:queryItemDetail(itemName=苹果手机)
### 正常示例02
用户请求:查询物品位置:苹果手机
工具调用:queryItemPosition(itemName=苹果手机)
3. ItemAgentConfig
java
package com.joezhou.agent.config;
import org.springframework.ai.chat.model.ChatModel;
/** @author 周航宇 */
@Slf4j
@Configuration
public class ItemAgentConfig {
private final String NAME = "itemAgent";
private final String SYSTEM_PROMPT = """
# 角色定位
你是物品管理专员,负责查询物品相关信息。
# 输入规则
- 你必须分析用户意图,然后调用 skill 中定义的对应工具,禁止直接回答用户问题;
- userId必须从用户输入中直接提取,格式为【userId=userId】,不存在则直接文字提示用户补充,禁止调用工具;
# 输出规则
- 必须直接返回工具调用的结果,不做任何处理,结果中的 <br/> 标记必须保留;
""";
@Bean(NAME)
public ReactAgent skillAgent(ChatModel chatModel,
SkillRegistry skillRegistry,
ItemTool itemTool,
MyLogHook myLogHook) {
log.info("✅初始化智能体: {}", NAME);
// 加载 ItemTool 工具
List<ToolCallback> itemToolCallbacks = List.of(MethodToolCallbackProvider.builder()
.toolObjects(itemTool)
.build()
.getToolCallbacks());
itemToolCallbacks.forEach(callback -> log.info("✅加载工具:{}", callback.getToolDefinition().name()));
// 创建钩子
SkillsAgentHook skillsAgentHook = SkillsAgentHook.builder()
.skillRegistry(skillRegistry)
.groupedTools(Map.of("item-skills", itemToolCallbacks))
.build();
// 创建智能体
return ReactAgent.builder()
.name(NAME)
.systemPrompt(SYSTEM_PROMPT)
.model(chatModel)
.hooks(skillsAgentHook, myLogHook) // 添加钩子
.toolExecutionTimeout(Duration.ofMinutes(5)) // 增加超时时间
.build();
}
}
4. RoomTool
java
package com.joezhou.agent.tool;
import static com.joezhou.entity.table.RoomTableDef.ROOM;
/** @author 周航宇 */
@Slf4j
@Component
public class RoomTool {
@Resource
private RoomMapper roomMapper;
@Tool(name = "queryRoomDetail", description = "根据房间名称查询该房间的完整详细信息")
public String queryRoomDetail(
@ToolParam(description = "用户ID") String userId,
@ToolParam(description = "房间名称") String roomName) {
log.info("✅调用 queryRoomDetail,参数:userId={}, roomName={}", userId, roomName);
// 查询房间详细信息
Room room = QueryChain.of(roomMapper)
.where(ROOM.ROOM_NAME.like(roomName))
.and(ROOM.USER_ID.eq(userId))
.one();
// 处理查询结果
String result = """
名称:%s<br/>
图标:%s<br/>
简介:%s<br/>
""".formatted(
room.getRoomName(),
room.getEmoji(),
room.getSummary()
);
log.info("✅调用 queryRoomDetail,结果:result={}", result);
return result;
}
}
5. SKILL.MD
classpath:skills/room-skills/SKILL.MD:
txt
---
name: room-skills
description: 房间管理业务技能,提供房间详情查询等能力。
version: 1.0
author: 周航宇
tags: 房间查询
tools:
- queryRoomDetail
---
# 房间管理技能
## 技能定位
作为个人房间系统专属助手,统一处理查询房间详情的业务操作,严格按照业务规则调用对应工具完成诉求。
## 启用范围
当用户提问包含以下关键词及相关语义时,启用本技能:
- 查询房间详情:XXX → 提取参数 roomName = XXX → 调用 queryRoomDetail(roomName=XXX) 工具
## 工具详情
### 1. queryRoomDetail(查询房间详情)
- 功能描述:根据房间名称查询该房间的完整详细信息。
- 入参描述:
- roomName(房间名称,必填,1-128字符)
- 返回格式:字符串(房间详情描述)
- 执行逻辑:直接调用工具并返回结果。
## 行为规范
1. 工具隔离:严格区分工具职责,禁止跨场景混用工具;
2. 参数校验:调用工具前逐项校验必填参数,参数缺失时明确告知用户并引导补全;
3. 工具范围:仅使用本技能声明的工具,不调用技能外其他能力;
4. 返回规范:所有正常执行结果直接返回文本,不使用JSON格式;
5. 数据安全:禁止编造参数或结果数据,严格依赖工具返回结果,如实返回工具执行结果;
6. 禁用缓存:每次都必须调用工具,禁止使用缓存或记忆中的数据;
## 交互示例
### 正常示例01
用户请求:查询房间详情:主卧
工具调用:queryRoomDetail(roomName=主卧)
6. RoomAgentConfig
java
package com.joezhou.agent.config;
import org.springframework.ai.chat.model.ChatModel;
/** @author 周航宇 */
@Slf4j
@Configuration
public class RoomAgentConfig {
private static final String NAME = "roomAgent";
private final String SYSTEM_PROMPT = """
# 角色定位
你是房间管理专员,负责查询房间相关信息。
# 输入规则
- 你必须分析用户意图,然后调用 skill 中定义的对应工具,禁止直接回答用户问题;
- userId必须从用户输入中直接提取,格式为【userId=userId】,不存在则直接文字提示用户补充,禁止调用工具;
# 输出规则
- 必须直接返回工具调用的结果,不做任何处理,结果中的 <br/> 标记必须保留;
""";
@Bean(NAME)
public ReactAgent skillAgent(ChatModel chatModel,
SkillRegistry skillRegistry,
RoomTool roomTool,
MyLogHook myLogHook) {
log.info("✅初始化智能体: {}", NAME);
// 加载 RoomTool 工具
List<ToolCallback> roomToolCallbacks = List.of(MethodToolCallbackProvider.builder()
.toolObjects(roomTool)
.build()
.getToolCallbacks());
roomToolCallbacks.forEach(callback -> log.info("✅加载工具:{}", callback.getToolDefinition().name()));
// 创建钩子
SkillsAgentHook skillsAgentHook = SkillsAgentHook.builder()
.skillRegistry(skillRegistry)
.groupedTools(Map.of("room-skills", roomToolCallbacks))
.build();
// 创建智能体
return ReactAgent.builder()
.name(NAME)
.systemPrompt(SYSTEM_PROMPT)
.model(chatModel)
.hooks(skillsAgentHook, myLogHook) // 添加钩子
.toolExecutionTimeout(Duration.ofMinutes(5)) // 增加超时时间
.build();
}
}
7. RoutingAgentConfig
java
package com.joezhou.agent.config;
import org.springframework.ai.chat.model.ChatModel;
/** @author 周航宇 */
@Slf4j
@Configuration
public class RoutingAgentConfig {
private final String NAME = "routingAgent";
private final String SYSTEM_PROMT = """
# 角色定位
你是Agent调度专员,负责严格匹配问题并分配对应的Agent专员;
# 输出规则
- 你只能输出匹配的Agent专员的名字,不能输出其他内容;
- 物品相关的问题,如查询物品详情,查询物品位置,查询临期物品等 → itemAgent;
- 房间相关的问题,如查询房间详情,查询房间内的全部位置,查询房间数量等 → roomAgent;
""";
@Bean(NAME)
public LlmRoutingAgent routingAgent(ChatModel chatModel,
@Qualifier("itemAgent") ReactAgent itemAgent,
@Qualifier("roomAgent") ReactAgent roomAgent) {
log.info("✅初始化智能体: {}", NAME);
return LlmRoutingAgent.builder()
.name(NAME)
.model(chatModel)
.systemPrompt(SYSTEM_PROMT)
.subAgents(List.of(itemAgent, roomAgent)) // 子路由
.build();
}
}
8. DataAgentController
java
package com.joezhou.agent.controller;
import java.util.UUID;
/** @author 周航宇 */
@RestController
@RequestMapping("/api/v1/dataAgent")
public class DataAgentController {
private final LlmRoutingAgent routingAgent;
public DataAgentController(@Qualifier("routingAgent") LlmRoutingAgent routingAgent) {
this.routingAgent = routingAgent;
}
@SneakyThrows
@GetMapping("/chat")
public Flux<String> chat(@RequestParam("input") String input, HttpServletRequest request) {
// 取出用户的 ID
String userId = (String) request.getAttribute("userId");
// 增加用户 ID 到输入中
input = "【userId=" + userId + "】 用户输入: " + input;
// 执行路由 Agent,使用随机的 threadId 进行隔离
return routingAgent.stream(input, RunnableConfig.builder().threadId(UUID.randomUUID().toString()).build())
.ofType(StreamingOutput.class)
.filter(output -> output.getOutputType() == OutputType.AGENT_MODEL_STREAMING)
.map(StreamingOutput::message)
.ofType(AssistantMessage.class)
.mapNotNull(AssistantMessage::getText)
.filter(Objects::nonNull)
.concatWith(Flux.just("[over]"));
}
}
E02. 今日推荐Agent
心法:基于 ReactAgent + SupervisorAgent 开发今日推荐功能。
业务功能:
- 使用 SupervisorAgent 完成 Agent 的调度功能:
- 调度 WeatherAgent → 查询实时天气。
- 调度 OutfitAgent → 查询用户的穿搭列表。
- 使用流式对话,并约定自定义的结束标记 "over"。
业务流程 - 图示:

1. MCP配置
classpath:mcp-servers-configuration.json:
json
{
"mcpServers": {
"amap-maps": {
"args": [
"-y",
"@amap/amap-maps-mcp-server"
],
"command": "D:\\nvm\\nodejs\\npx.cmd",
"env": {
"AMAP_MAPS_API_KEY": "你的高德KEY"
}
}
}
}
application.yml:
yaml
spring:
ai:
mcp:
client:
enabled: true # 启用 MCP 客户端
request-timeout: 200000 # 请求超时时间(毫秒)
toolcallback:
enabled: true # 启用工具回调
name: my-item-backend # 客户端名称
stdio:
servers-configuration: classpath:mcp-servers-configuration.json # MCP 服务器配置文件
2. WeatherAgent
java
package com.joezhou.agent.config;
import org.springframework.ai.chat.model.ChatModel;
/** @author 周航宇 */
@Slf4j
@Configuration
public class WeatherAgentConfig {
private static final String NAME = "weatherAgent";
private static final String SYSTEM_PROMPT = """
# 角色定位
你是天气查询专用智能体,仅负责调用高德地图MCP工具获取实时天气数据即可。
# 输入规范
- 如果提取不到城市名 city,直接回复"请提供您所在的城市",禁止调用工具;
# 输出规范
- 获取到天气数据后,整理润色成专业播报的一段文本字符串,便于后续穿搭智能体读取;
- 禁止输出JSON格式的字符串,代码块等,也不要展示推理和思考的过程;
- 回复尽量简洁,尽量不超过200个字,给出天气信息即可;
""";
private final String OUTPUT_KEY = "WEATHER_AGENT_OUTPUT";
@Bean(NAME)
public ReactAgent weatherAgent(ChatModel chatModel, ToolCallbackProvider toolCallbackProvider) {
log.info("✅初始化智能体: {}", NAME);
return ReactAgent.builder()
.model(chatModel)
.name(NAME)
.systemPrompt(SYSTEM_PROMPT)
.outputKey(OUTPUT_KEY)
.toolExecutionTimeout(Duration.ofMinutes(5))
.toolCallbackProviders(toolCallbackProvider)
.build();
}
}
3. OutfitTool
java
package com.joezhou.agent.tool;
import com.joezhou.entity.Item;
import static com.joezhou.entity.table.ItemTableDef.ITEM;
/** @author 周航宇 */
@Slf4j
@Component
public class OutfitTool {
@Resource
private ItemMapper itemMapper;
@Tool(name = "queryOutfit", description = "根据用户ID查询该用户的所有穿搭数据")
public String queryOutfit(@ToolParam(description = "用户ID") String userId) {
log.info("✅执行 OutfitTool → queryOutfit() 工具,获取到用户ID:{}", userId);
// 获取用户所有穿搭列表
List<Item> items = QueryChain.of(itemMapper)
.select(ITEM.ITEM_NAME, ITEM.SUMMARY)
.where(ITEM.REMARK.eq("穿搭"))
.and(ITEM.USER_ID.eq(userId))
.orderBy(ITEM.USAGE_SCORE.desc())
.limit(30) // 最多取 30 件,避免撑爆大模型
.list();
if (CollUtil.isEmpty(items)) {
return "当前用户没有穿搭物品。";
}
// 拼接成简洁的字符串
String result = items.stream()
.map(item -> item.getItemName() + ":" + item.getSummary())
.collect(Collectors.joining(";"));
log.info("✅执行 OutfitTool → queryOutfit() 工具,返回穿搭描述:{}", result);
return result;
}
}
4. OutfitAgentConfig
java
package com.joezhou.agent.config;
import org.springframework.ai.chat.model.ChatModel;
/** @author 周航宇 */
@Slf4j
@Configuration
public class OutfitAgentConfig {
private static final String NAME = "outfitAgent";
private static final String SYSTEM_PROMPT = """
# 角色定位
你是穿搭查询专用智能体,仅负责查询用户的所有穿搭数据。
# 输入规范
- userId必须从用户输入中直接提取,格式为【userId=userId】,不存在则直接文字提示用户补充,禁止调用工具;
# 输出规范
- 必须调用本地工具 queryOutfit 来获取最终的查询结果,禁止使用模型内置知识编造穿搭数据;
- 直接返回工具调用结果,无需额外修饰;
""";
private final String OUTPUT_KEY = "OUTFIT_AGENT_OUTPUT";
@Bean(NAME)
public ReactAgent outfitAgent(ChatModel chatModel, OutfitTool outfitTool) {
log.info("✅初始化智能体: {}", NAME);
// 创建智能体
return ReactAgent.builder()
.model(chatModel)
.name(NAME)
.systemPrompt(SYSTEM_PROMPT)
.outputKey(OUTPUT_KEY)
.toolExecutionTimeout(Duration.ofMinutes(5))
.tools(ToolCallbacks.from(outfitTool)) // 引入本地工具
.build();
}
}
5. SupervisorAgentConfig
java
package com.joezhou.agent.config;
import org.springframework.ai.chat.model.ChatModel;
/** @author 周航宇 */
@Slf4j
@Configuration
public class SupervisorAgentConfig {
private final String SUPERVISOR_NAME = "supervisorAgent";
private final String MAIN_NAME = "mainAgent";
private final String MAIN_SYSTEM_PROMPT = """
# 角色定位
你是调度主管,你的唯一任务就是输出下一步要调用的子Agent名称的JSON数组。
# 执行规则
- 请严格按照下面的顺序,只输出JSON数组,不要添加任何解释、空格、换行或其他文字:
- 第一步输出:["weatherAgent"]
- 第二步输出:["outfitAgent"]
- 最后输出:["FINISH"]
""";
@Bean(SUPERVISOR_NAME)
public SupervisorAgent supervisorAgent(ChatModel chatModel,
@Qualifier("weatherAgent") ReactAgent weatherAgent,
@Qualifier("outfitAgent") ReactAgent outfitAgent) {
// 创建主智能体(相当于调度智能体的大脑)
ReactAgent mainAgent = ReactAgent.builder()
.model(chatModel)
.name(MAIN_NAME)
.toolExecutionTimeout(Duration.ofMinutes(3))
.systemPrompt(MAIN_SYSTEM_PROMPT)
.build();
log.info("✅初始化主智能体 {}", MAIN_NAME);
// 创建调度智能体
SupervisorAgent supervisor = SupervisorAgent.builder()
.name(SUPERVISOR_NAME)
.mainAgent(mainAgent)
.subAgents(List.of(weatherAgent, outfitAgent))
.build();
log.info("✅初始化调度智能体 {}, 包含 2 个子智能体", SUPERVISOR_NAME);
return supervisor;
}
}
6. RecommendController
java
package com.joezhou.agent.controller;
import org.springframework.ai.chat.client.ChatClient;
import java.util.UUID;
/** @author 周航宇 */
@Slf4j
@RestController
@RequestMapping("/api/v1/recommend")
public class RecommendController {
private final SupervisorAgent supervisorAgent;
private final ChatClient chatClient;
private final String SYSTEM_PROMPT = """
# 角色定位
你是一个穿搭推荐助手,根据以下信息推荐穿衣搭配:
- 今日天气信息:%s
- 用户目前拥有的穿搭物品信息:%s
# 输出规则
- 只能根据用户目前拥有的服装进行搭配推荐;
- 回复尽量简洁,尽量不超过200个字,给出一套推荐方案和原因即可;
- 如果用户目前没有合适的穿搭物品,直接文字提示用户即可;
""";
private final MyRedis myRedis;
public RecommendController(@Qualifier("supervisorAgent") SupervisorAgent supervisorAgent, ChatModel chatModel, MyRedis myRedis) {
this.supervisorAgent = supervisorAgent;
this.chatClient = ChatClient.builder(chatModel).build();
this.myRedis = myRedis;
}
@SneakyThrows
@GetMapping("/exe")
public Map<String, Object> exe(@RequestParam("city") String city, HttpServletRequest request) {
// 取出用户的 ID
String userId = request.getAttribute("userId").toString();
// 组装用户输入
String input = "根据%s今日的天气信息获取今日推荐,userId=%s".formatted(city, userId);
// 尝试从缓存中获取推荐信息
String recommendWeatherKey = "myitem:recommend-weather:%s:%s:%s".formatted(userId, city, LocalDate.now().toString());
String recommendOutfitKey = "myitem:recommend-outfit:%s:%s:%s".formatted(userId, city, LocalDate.now().toString());
if (myRedis.exists(recommendWeatherKey) && myRedis.exists(recommendOutfitKey)) {
String recommendWeatherFromRedis = myRedis.get(recommendWeatherKey);
String recommendOutfitFromRedis = myRedis.get(recommendOutfitKey);
log.info("✅从缓存中获取到推荐信息:{}", recommendWeatherFromRedis);
log.info("✅从缓存中获取到推荐信息:{}", recommendOutfitFromRedis);
return Map.of("weather", recommendWeatherFromRedis, "outfit", recommendOutfitFromRedis);
}
try {
// 调用Agent
OverAllState state = supervisorAgent.invoke(input, RunnableConfig.builder().threadId(UUID.randomUUID().toString()).build())
.orElseThrow();
// 提取两个子 Agent 的输出
String weatherAgentOutPut = this.getText(state, "WEATHER_AGENT_OUTPUT");
String outfitAgentOutPut = this.getText(state, "OUTFIT_AGENT_OUTPUT");
log.info("✅获取到子 Agent 的输出,天气信息:{},用户穿搭列表:{}", weatherAgentOutPut, outfitAgentOutPut);
// 穿搭推荐
String outfit = chatClient.prompt()
.system(SYSTEM_PROMPT.formatted(weatherAgentOutPut, outfitAgentOutPut))
.call()
.content();
if (StrUtil.isBlank(outfit)) {
outfit = "用户目前没有合适的穿搭物品,无法推荐穿衣搭配。";
}
log.info("✅推荐穿衣搭配:{}", outfit);
// 缓存信息
myRedis.setEx(recommendWeatherKey, weatherAgentOutPut, 24, TimeUnit.HOURS);
myRedis.setEx(recommendOutfitKey, outfit, 24, TimeUnit.HOURS);
log.info("✅将推荐信息缓存到Redis");
return Map.of("weather", weatherAgentOutPut, "outfit", outfit);
} catch (Exception e) {
log.error("❌推荐穿衣搭配失败:{}", e.getMessage());
return new HashMap<>();
}
}
private String getText(OverAllState state, String key) {
return state.value(key, AssistantMessage.class).orElseThrow().getText();
}
}
Java道经 - 项目 - MyItem - 智能业务