基于 Spring AI 构建具备记忆与情绪的多角色 Agent 系统

随着大模型技术的普及,单纯的"一问一答"式 LLM 已经无法满足复杂的业务需求。我们越来越需要具备持续记忆鲜明人格 以及外部工具调用能力的智能体(Agent)。

今天,我将和大家分享一个基于 Spring AI 构建的多角色 AI 聊天系统架构笔记。该系统通过记忆分层、角色人格插件化、工具动态调度以及底层 Reactive 全异步编排,实现了一个高度可扩展的 Agent 架构。

核心架构概览

本 Agent 系统可以拆分为三大核心模块:

  1. 记忆系统(Memory):负责对话的存储、检索与提炼。

  2. 人格系统(Persona & Emotion):定义 AI 角色,并管理动态的情绪流转。

  3. 工具系统(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)

这是系统最出彩的设计之一。用户的话语不会全部原封不动地存入向量库,而是经过双重过滤

  1. 规则预过滤:丢弃少于 10 个字符或命中无意义词表(如"哈哈"、"在吗")的消息。

  2. 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 引擎会执行以下逻辑:

  1. 组装工具 Prompt:将当前角色绑定的工具列表及其描述发送给 LLM。

  2. LLM 决策:LLM 决定是否需要使用工具。如果需要,它会返回一个包含工具名和提取参数的 JSON 结构。

  3. 解析与执行 :系统解析 JSON,路由到具体的工具实现类(例如调用 Jsoup 爬取百度搜索的 WebSearchTool),并拿到结果。

  4. 上下文注入:将工具执行结果作为"最新情报"拼接到最终的 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 走向生产环境的决定性力量。

相关推荐
火山引擎开发者社区15 分钟前
当 Agent 自己做 SRE:详解 ArkClaw 自动化可观测体系的工程实践
人工智能
Coffeeee2 小时前
两个例子,帮你快速理解什么是Token
人工智能·程序员·ai编程
饼干哥哥2 小时前
用AI全自动剪辑,日更 100条爆款视频——HyperFrames、Remotion、Git使用入门
人工智能·机器学习·ai编程
用户83244598541323 小时前
深入拆解 AlexNet:跟着一张猫咪照片,看数据如何流动
人工智能
饼干哥哥3 小时前
开源Skills|搭建亚马逊动态关键词库系统,每天抓SSS级机会词
人工智能·深度学习·数据分析
Weigang3 小时前
别等 Agent 上线后补评估:先用 DeepEval 写失败样本
人工智能
MomentYY3 小时前
AI 到底是“懂”,还是在“猜”?
前端·人工智能·ai编程
拾光拾趣录4 小时前
为什么采用多路检索而不是单一向量检索?
人工智能
拾光拾趣录4 小时前
Agent 编排器是怎么设计的?为什么这样设计?
人工智能
拾光拾趣录4 小时前
为什么选择 ReAct 模式而不是 Plan-and-Execute?
人工智能