随着大模型技术的普及,单纯的"一问一答"式 LLM 已经无法满足复杂的业务需求。我们越来越需要具备持续记忆 、鲜明人格 以及外部工具调用能力的智能体(Agent)。
今天,我将和大家分享一个基于 Spring AI 构建的多角色 AI 聊天系统架构笔记。该系统通过记忆分层、角色人格插件化、工具动态调度以及底层 Reactive 全异步编排,实现了一个高度可扩展的 Agent 架构。
核心架构概览
本 Agent 系统可以拆分为三大核心模块:
-
记忆系统(Memory):负责对话的存储、检索与提炼。
-
人格系统(Persona & Emotion):定义 AI 角色,并管理动态的情绪流转。
-
工具系统(Tooling):注册与调度外部工具(如搜索引擎)。
这三大模块由 AgentServicer.chat() 作为统一的编排入口,采用 Project Reactor 进行异步并行处理,最终拼装 Prompt 触发大模型的流式输出。
一、大脑的构建:三级记忆架构
记忆是 Agent 维持上下文的关键。如果把所有的历史记录都塞给 LLM,不仅成本高昂,还会导致上下文溢出。因此,系统采用了三级存储架构,并结合了 LLM 提炼与向量检索(RAG)。
1. 短期记忆(Short-Term Memory)
-
介质:Redis List
-
策略 :保留最近 15 分钟内的最多 16 条对话。使用
leftPush写入头部,并通过trim截断。每次读取时刷新 TTL 并反转列表,保证对话顺序。这保证了 AI 能够顺畅处理当前的话题上下文。
2. 长期记忆(Long-Term Memory)
这是系统最出彩的设计之一。用户的话语不会全部原封不动地存入向量库,而是经过双重过滤:
-
规则预过滤:丢弃少于 10 个字符或命中无意义词表(如"哈哈"、"在吗")的消息。
-
LLM 提炼 :调用
SummaryUtil让大模型提取用户消息中的核心价值(如兴趣、目标、背景)。只有产生有效提炼(非 NULL)的信息,才会被转化为向量存入。
向量存储与防重复:
系统采用 Spring AI 的 VectorStore(对接 OpenAI text-embedding-v3 1024 维模型)。在写入前,系统会先进行一次相似度阈值(> 0.75)检索,只有当不存在高度相似的记忆时才会落库,极大地减少了冗余数据。
3. RAG 缓存与分布式锁机制
为了防止高并发下向量库被频繁查询击穿,系统在长记忆检索上引入了 Redis 缓存,并搭配了自旋分布式锁:
// 简化版的锁逻辑
while (System.currentTimeMillis() < endTime) {
if (chatHistoryService.locked(lockKey, lockValue, 5)) { // SET NX
isLock = true; break;
}
Thread.sleep(50);
}
if (!isLock) {
// 没抢到锁:降级直接查库,不阻塞等待
return queryVectorAndCache();
}
try {
// 抢到锁:双重检查缓存(Double Check)
String redisMsg = chatHistoryService.getLongMemoryCache(...);
if (redisMsg != null) return redisMsg;
return queryVectorAndCache();
} finally {
chatHistoryService.unlock(lockKey, lockValue);
}
这种设计兼顾了系统的响应速度和底层向量库的并发安全。最后,所有的聊天原始记录均会异步落入 MySQL,作为永久的对话凭证。
二、灵魂的注入:插件化角色与情绪系统
为了让 AI 告别机械感,系统设计了策略模式的 AISetPrompts 接口,实现了角色的热插拔。
角色隔离
系统中预设了两个角色:
-
Arona :温柔天然,核心 Prompt 设定为系统的操控者。绑定工具:
webSearch。 -
Prana :冷静理性,机械感稍强。绑定工具:
choose。
PromptManager 维护了这些角色的注册表,根据用户选择动态加载对应的"灵魂"。
动态情绪流转
纯静态的 Prompt 是死板的。系统引入了 EmotionState(包含 happy, angry, tired, favorability)。
每次用户发送消息时,后台会触发一个异步的 LLM 分析任务,判断用户的意图是"赞美"、"攻击"还是"中立",从而更新这些情绪值。
在最终生成 Prompt 时,系统会根据当前情绪注入行为指令:
-
如果
happy > 70:指令 AI 回复更温柔、主动。 -
如果
angry > 70:指令 AI 回复稍微冷淡。
更有趣的是,情绪具有自然衰减机制,几分钟不交互,情绪会慢慢回落到基础值,完美模拟了真实人类的情绪波动。
三、双手的延伸:动态工具调度
系统的 AgentTool 接口定义了工具的标准规范(名称、描述、执行逻辑)。
当请求到达时,ToolRuntime 引擎会执行以下逻辑:
-
组装工具 Prompt:将当前角色绑定的工具列表及其描述发送给 LLM。
-
LLM 决策:LLM 决定是否需要使用工具。如果需要,它会返回一个包含工具名和提取参数的 JSON 结构。
-
解析与执行 :系统解析 JSON,路由到具体的工具实现类(例如调用 Jsoup 爬取百度搜索的
WebSearchTool),并拿到结果。 -
上下文注入:将工具执行结果作为"最新情报"拼接到最终的 Prompt 中。
四、神经中枢:Reactive 并行编排与线程池隔离
整个 AgentServicer.chat() 的核心是高度异步化的。长记忆读取、短记忆读取、情绪查询、工具调用,这四个耗时操作之间互不依赖。
系统采用了 Project Reactor 的 Mono.zip 进行优雅的并行编排:
// 工具调用、短期记忆、长期记忆、情绪状态 并行获取
Mono<String> toolMono = toolRuntime.useTools(...);
Mono<String> shortMemoryMono = getShortMemory(...);
// ...
return Mono.zip(shortMemoryMono, longMemoryMono, emotionMono, toolMono)
.flatMapMany(tuple -> {
// 四个数据源全部就绪,拼装 Prompt
String toolResult = tuple.getT4();
// 组装最终 Prompt 并发起流式对话
return chatClient.prompt(sysPrompt).user(userPrompt).stream().content();
});
极致的线程池隔离
为了防止阻塞 I/O(如向量检索)拖垮 Netty 的事件循环,系统精心设计了职责分离的线程池:
-
vectorScheduler(30-40 线程):专门处理向量库的阻塞查询。 -
memoryExecutorPool(15-30 线程):处理数据库异步落库(拒绝策略为 AbortPolicy)。 -
llmExecutorPool(10-20 线程):专用于 LLM 记忆提炼。考虑到 API 限流,拒绝策略设为 DiscardOldestPolicy。 -
AiToolExecutor:专门处理工具调度。
这种舱壁隔离模式(Bulkhead Pattern)确保了系统在高并发场景下的极强稳定性。
总结
这个基于 Spring AI 的 Agent 架构向我们展示了如何通过严谨的工程化手段,将大模型包装成一个有血有肉的智能体:
-
性能上,利用 Reactive 并行加载和多级缓存突破了 I/O 瓶颈。
-
智能上,依靠 LLM 提炼记忆、决策工具,实现了举一反三。
-
体验上,动态情绪和策略化的人格让对话告别枯燥。
在 AI 应用爆发的今天,优秀的提示词固然重要,但坚实的底层工程架构才是支撑高阶 Agent 走向生产环境的决定性力量。