企业级 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 返回结果后,将本轮 QA 追加到 Redis │ ▼ 刷新 TTL = 30min读写策略:操作时机说明读请求到达时取出当前会话完整历史,注入到 Prompt写LLM 响应后追加本轮 QA清理TTL 到期Redis 自动删除,无需手动干预TTL 为什么是 30 分钟?用户注意力窗口通常 15-30 分钟过长浪费内存,过短丢失上下文根据业务可调整为 15min ~ 2hL2 — 全量对话归档审计(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 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency!-- MySQL --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-jdbc/artifactId/dependencydependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactIdscoperuntime/scope/dependency!-- Spring AI 向量库(以 Qdrant 为例) --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-qdrant-store-spring-boot-starter/artifactIdversion1.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) → 追加本轮 QA → 刷新 TTL */@ComponentpublicclassRedisChatMemoryimplementsChatMemory{privatestaticfinalStringKEY_PREFIX="chat:memory:";privatestaticfinalintMAX_HISTORY=20;// 最多保留 20 条消息privatestaticfinalintTTL_MINUTES=30;// 30 分钟过期privatefinalRedisTemplateString,ObjectredisTemplate;publicRedisChatMemory(RedisTemplateString,ObjectredisTemplate){this.redisTemplate=redisTemplate;}@Overridepublicvoidadd(StringconversationId,ListMessagemessages){Stringkey=KEY_PREFIX+conversationId;// 追加到 Redis List 右侧redisTemplate.opsForList().rightPushAll(key,messages.toArray());// 裁剪:只保留最近 MAX_HISTORY 条redisTemplate.opsForList()