企业级 AI 记忆三层架构 —— 数据流转与保存流程详解
企业级 AI 记忆三层架构 —— 数据流转与保存流程详解
技术栈:Spring AI 1.0.0-M6 + Spring Boot 3.4.0 + Redis + MySQL/PostgreSQL + Milvus/Qdrant
场景:AI 对话系统从"无记忆"到"企业级全链路记忆"的演进方案
核心理念:热数据(毫秒级响应)、温数据(秒级查询)、冷数据(语义检索)三层分离
目录
- 一、架构全景概览
- 二、三层详细设计
- L1 — 实时短期上下文(Redis)
- L2 — 全量对话归档审计(MySQL)
- L3 — 跨会话长期语义记忆(向量库)
- 三、Spring AI 代码实现示例
- 3.1 依赖引入
- 3.2 Redis 短期记忆(ChatMemory)
- 3.3 MySQL 全量归档(AuditLog)
- 3.4 向量库语义记忆(VectorStore)
- 3.5 Advisor 编排记忆读写流程(完整版)
- 3.6 Controller 层完整调用示例
- 四、完整数据流转时序图
- 五、生产环境关键配置
- 六、常见问题与最佳实践
一、架构全景概览
┌──────────────────────────────────────────┐ │ API Gateway │ │ POST /api/ai/chat/memory │ └──────────────────┬───────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ Spring AI Advisor 拦截链 │ │ │ │ MemoryAdvisor: before 读取(L1+L3) → 注入上下文 → 调用 LLM → after 写入(L1+L2+L3)│ └──────────┬───────────────┬───────────────────────┬───────────────────────────┘ │ │ │ ▼ ▼ ▼ ┌───────────┐ ┌──────────────┐ ┌──────────────────────┐ │ Redis │ │ MySQL │ │ Milvus / Qdrant │ │ (L1 热) │ │ (L2 温) │ │ (L3 冷) │ │ │ │ │ │ │ │ • 对话窗口 │ │ • 全量归档 │ │ • 向量嵌入 (embedding)│ │ • TTL 30min│ │ • 合规审计 │ │ • 语义检索 │ │ • 毫秒读写 │ │ • 永久存储 │ │ • 历史召回 (Top-K) │ │ • 无需索引 │ │ • 索引优化 │ │ • 跨会话关联 │ └───────────┘ └──────────────┘ └──────────────────────┘为什么是三层?
| 维度 | 只存 Redis | 只存 DB | 三层分离 |
|---|---|---|---|
| 上下文连续性 | ✅ 可以 | ❌ 需每次都查 | ✅ 毫秒级恢复 |
| 合规审计 | ❌ 过期丢失 | ✅ 永久 | ✅ 归档完整 |
| 跨会话记忆 | ❌ 丢失 | ❌ 无法语义检索 | ✅ 向量召回 |
| 成本 | 低 | 中 | 中高(可控) |
| 生产适用 | 仅 POC | 仅审计场景 | ✅ 企业级 |
二、三层详细设计
L1 — 实时短期上下文(Redis)
定位:当前对话窗口的"工作记忆",保证多轮对话的连贯性。
存储结构:
Key: chat:memory:{conversationId} Type: List (或 JSON String) Value: [ {"role":"user","content":"帮我查一下订单 O20240101"}, {"role":"assistant","content":"好的,查询到您的订单..."}, {"role":"user","content":"帮我修改收货地址为北京"}, ... ] TTL: 30分钟(每轮对话刷新 TTL)数据流转:
用户发消息 │ ▼ 从 Redis 读取历史消息 (chat:memory:{conversationId}) │ ▼ 将历史消息 + 当前消息拼接为完整上下文 │ ▼ 发送给 LLM 推理 │ ▼ LLM 返回结果后,将本轮 Q&A 追加到 Redis │ ▼ 刷新 TTL = 30min读写策略:
| 操作 | 时机 | 说明 |
|---|---|---|
| 读 | 请求到达时 | 取出当前会话完整历史,注入到 Prompt |
| 写 | LLM 响应后 | 追加本轮 Q&A |
| 清理 | TTL 到期 | Redis 自动删除,无需手动干预 |
TTL 为什么是 30 分钟?
- 用户注意力窗口通常 15-30 分钟
- 过长浪费内存,过短丢失上下文
- 根据业务可调整为 15min ~ 2h
L2 — 全量对话归档审计(MySQL)
定位:永久存储每一轮对话,满足合规、数据分析、客户投诉追溯等需求。
存储结构:
-- 会话表CREATETABLEt_conversation(idBIGINTAUTO_INCREMENTPRIMARYKEY,conversation_idVARCHAR(64)NOTNULL,-- 会话唯一标识user_idVARCHAR(64)NOTNULL,-- 用户 IDtitleVARCHAR(255)DEFAULT'',-- 会话标题(首轮截取)modelVARCHAR(64)NOTNULL,-- 使用的模型(qwen-plus / qwen-max)statusTINYINTDEFAULT1,-- 1:进行中 2:已结束create_timeDATETIMEDEFAULTNOW(),update_timeDATETIMEDEFAULTNOW()ONUPDATENOW(),INDEXidx_user_id(user_id),INDEXidx_conversation_id(conversation_id));-- 消息明细表CREATETABLEt_message(idBIGINTAUTO_INCREMENTPRIMARYKEY,conversation_idVARCHAR(64)NOTNULL,-- 关联会话 IDseqINTNOTNULL,-- 消息序号(第几轮)roleVARCHAR(16)NOTNULL,-- user / assistant / systemcontentTEXTNOTNULL,-- 消息内容token_countINTDEFAULT0,-- Token 消耗latency_msINTDEFAULT0,-- 响应延迟(ms)create_timeDATETIMEDEFAULTNOW(),INDEXidx_conversation_id(conversation_id),INDEXidx_create_time(create_time));-- 审计日志表(可选,用于更严格的合规场景)CREATETABLEt_audit_log(idBIGINTAUTO_INCREMENTPRIMARYKEY,conversation_idVARCHAR(64)NOTNULL,operatorVARCHAR(64)NOTNULL,-- 操作人/系统actionVARCHAR(32)NOTNULL,-- SEND / RECEIVE / RECALLdetail JSON,-- 详细信息(脱敏后的)ip_addressVARCHAR(45),create_timeDATETIMEDEFAULTNOW(),INDEXidx_conversation_id(conversation_id),INDEXidx_create_time(create_time));数据流转:
LLM 响应返回 │ ▼ 【异步写入】→ MySQL ├── 判断 conversation_id 是否已存在会话记录 │ ├── 否 → INSERT t_conversation │ └── 是 → UPDATE t_conversation.update_time ├── INSERT t_message (user 消息) ← 在发请求前异步写入 └── INSERT t_message (assistant 消息) ← 在响应后异步写入为什么必须异步?
- 同步写 DB 会增加 50-200ms 延迟,影响用户体验
- 使用
@Async+ 线程池,主流程不阻塞 - 即使归档失败,也不影响对话功能(降级策略)
L3 — 跨会话长期语义记忆(向量库)
定位:将对话内容转化为语义向量,支持跨会话的历史记忆召回,让 AI 具备"长期记忆"能力。
存储结构:
Milvus/Qdrant Collection: ai_long_term_memory ┌──────────────────┬───────────────┬──────────────────────────────────────┐ │ 字段 │ 类型 │ 说明 │ ├──────────────────┼───────────────┼──────────────────────────────────────┤ │ id │ VARCHAR(64) │ 主键,UUID │ │ user_id │ VARCHAR(64) │ 用户 ID(分区键) │ │ content │ TEXT │ 记忆片段原文 │ │ embedding │ FLOAT[1536] │ 向量(text-embedding-v3 生成) │ │ memory_type │ VARCHAR(32) │ preference / fact / intent / ... │ │ importance │ FLOAT │ 重要性评分 0.0~1.0 │ │ source_conv_id │ VARCHAR(64) │ 来源会话 ID(可追溯) │ │ create_time │ DATETIME │ 创建时间 │ │ access_count │ INT │ 被召回次数(活跃度) │ │ last_access_time │ DATETIME │ 最后被召回时间 │ │ ttl │ INT │ 过期天数(0=永久) │ └──────────────────┴───────────────┴──────────────────────────────────────┘记忆提取流程(写入):
每轮对话结束 │ ▼ 【异步】提取语义记忆 │ ▼ ① 调用 LLM 提取可记忆信息 Prompt: "从以下对话中提取值得长期记住的信息,以JSON返回: [{\"type\":\"preference|fact|intent\",\"content\":\"...\",\"importance\":0.8}]" │ ▼ ② 对每条记忆调用 Embedding 模型,生成向量 │ ▼ ③ 写入向量库 (INSERT with upsert) │ ▼ ④ 低重要性记忆(importance < 0.3)写入时设置 ttl = 30天 高重要性记忆(importance >= 0.7)ttl = 0(永久)记忆召回流程(读取):
用户发起新对话 │ ▼ ① 将用户当前消息转为向量 (embedding) │ ▼ ② 向量库 ANN 检索 Top-K (K=5~10) - 过滤条件:user_id = 当前用户 - 相似度阈值:score > 0.7 - 排序:score DESC │ ▼ ③ 将召回的记忆注入 System Prompt 例如: "以下是用户的历史偏好和相关信息,请在回答时参考: 1. 用户偏好简洁的回答风格 2. 用户之前关注过订单 O20240101 3. 用户的默认收货地址是北京市朝阳区..." │ ▼ ④ 更新被召回记忆的 access_count + last_access_time记忆淘汰策略:
定时任务(每天凌晨 3:00) │ ▼ ① 删除 ttl 到期记录 ② 删除 access_count=0 且 create_time > 90天 的记录 ③ 删除 importance < 0.2 且 60天未访问 的记录三、Spring AI 代码实现示例
以下代码基于当前项目结构(Spring AI 1.0.0-M6)进行扩展。
3.1 依赖引入
在pom.xml中新增以下依赖:
<!-- Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- MySQL --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- Spring AI 向量库(以 Qdrant 为例) --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId><version>1.0.0-M6</version></dependency><!-- 异步支持(Spring Boot 内置,无需额外依赖) -->3.2 Redis 短期记忆(ChatMemory)
Spring AI 提供了ChatMemory接口和InMemoryChatMemory默认实现,我们扩展为 Redis 版本。
packagecom.example.qwen.memory;importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.ai.chat.messages.Message;importorg.springframework.ai.chat.messages.MessageType;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Component;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.TimeUnit;/** * L1 层:基于 Redis 的短期对话记忆 * * 数据流: * 读:用户请求 → get(conversationId) → 获取最近 N 轮历史 → 注入 ChatClient * 写:LLM 响应后 → add(conversationId, messages) → 追加本轮 Q&A → 刷新 TTL */@ComponentpublicclassRedisChatMemoryimplementsChatMemory{privatestaticfinalStringKEY_PREFIX="chat:memory:";privatestaticfinalintMAX_HISTORY=20;// 最多保留 20 条消息privatestaticfinalintTTL_MINUTES=30;// 30 分钟过期privatefinalRedisTemplate<String,Object>redisTemplate;publicRedisChatMemory(RedisTemplate<String,Object>redisTemplate){this.redisTemplate=redisTemplate;}@Overridepublicvoidadd(StringconversationId,List<Message>messages){Stringkey=KEY_PREFIX+conversationId;// 追加到 Redis List 右侧redisTemplate.opsForList().rightPushAll(key,messages.toArray());// 裁剪:只保留最近 MAX_HISTORY 条redisTemplate.opsForList()