Spring AI企业级应用实战(4):Chat Memory、会话隔离、持久化与上下文压缩

发布时间:2026/7/28 20:38:24
Spring AI企业级应用实战(4):Chat Memory、会话隔离、持久化与上下文压缩 文章摘要前几篇已经完成Spring AI统一调用层和流式输出。本篇继续实现企业级多轮对话使用MessageChatMemoryAdvisor管理近期消息要求每次请求显式提供conversationId通过PostgreSQL保存完整Chat History与持久化Memory校验租户和用户所有权并在上下文接近预算时生成结构化摘要。最终形成一个支持多实例部署、流式调用、审计和安全删除的会话系统。一、本篇目标完成以下能力创建会话 稳定conversationId 多租户所有权校验 Chat Memory持久化 完整Chat History 同步与流式调用 窗口控制 上下文摘要 清空与删除 可观测性最终链路HTTP请求 → 身份认证 → 会话所有权校验 → 写入Chat History → MessageChatMemoryAdvisor → ChatClient → 模型 → 保存结果 → 更新指标二、项目结构src/main/java/com/zyentor/ai ├── config │ └── ChatMemoryConfig.java ├── conversation │ ├── Conversation.java │ ├── ConversationRepository.java │ ├── ConversationService.java │ └── ConversationController.java ├── history │ ├── ChatMessageRecord.java │ ├── ChatHistoryRepository.java │ └── JdbcChatHistoryRepository.java ├── memory │ ├── MemoryCompactionService.java │ └── ConversationSummary.java ├── service │ └── EnterpriseConversationAiService.java └── security └── UserContext.java三、依赖dependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion2.0.0/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-deepseek/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-chat-memory-repository-jdbc/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-webflux/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependencydependencygroupIdorg.postgresql/groupIdartifactIdpostgresql/artifactIdscoperuntime/scope/dependency/dependencies具体Starter名称应以当前Spring AI 2.0.x文档和依赖清单为准。四、配置数据源和模型spring:datasource:url:jdbc:postgresql://localhost:5432/spring_ai_demousername:ai_userpassword:${DB_PASSWORD}ai:model:chat:deepseekdeepseek:api-key:${DEEPSEEK_API_KEY}chat:model:deepseek-v4-flashtemperature:0.2max-tokens:2048生产环境不要把密钥写进仓库。五、配置Chat MemoryConfigurationpublicclassChatMemoryConfig{BeanChatMemorychatMemory(ChatMemoryRepositoryrepository){returnMessageWindowChatMemory.builder().chatMemoryRepository(repository).maxMessages(30).build();}Bean(conversationChatClient)ChatClientconversationChatClient(ChatClient.Builderbuilder,ChatMemorychatMemory){returnbuilder.defaultSystem( 你是企业AI助手。 只能使用当前用户有权访问的上下文。 不得泄露其他用户或租户的信息。 不确定时明确说明。 ).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();}}maxMessages30只是示例生产应通过真实Token分布评测。六、为什么每次请求必须传conversationIdSpring AI 2.0内置Memory Advisor要求显式提供ChatMemory.CONVERSATION_ID调用chatClient.prompt().advisors(spec-spec.param(ChatMemory.CONVERSATION_ID,conversationId)).user(message).call().content();不要使用默认会话也不要把conversationId写死在Advisor Bean中。七、会话表CREATETABLEai_conversation(idVARCHAR(64)PRIMARYKEY,tenant_idVARCHAR(64)NOTNULL,user_idVARCHAR(64)NOTNULL,titleVARCHAR(200),statusVARCHAR(20)NOTNULL,memory_versionBIGINTNOTNULLDEFAULT1,created_atTIMESTAMPNOTNULL,updated_atTIMESTAMPNOTNULL);memory_version可以用于缓存失效摘要版本调试会话重置。八、完整消息表CREATETABLEai_chat_message(idVARCHAR(64)PRIMARYKEY,conversation_idVARCHAR(64)NOTNULL,tenant_idVARCHAR(64)NOTNULL,user_idVARCHAR(64)NOTNULL,roleVARCHAR(20)NOTNULL,contentTEXTNOTNULL,statusVARCHAR(20)NOTNULL,request_idVARCHAR(64)NOTNULL,modelVARCHAR(100),prompt_versionVARCHAR(50),input_tokensBIGINT,output_tokensBIGINT,created_atTIMESTAMPNOTNULL);Chat Memory表只服务模型上下文该表保存完整历史。九、会话创建ServicepublicclassConversationService{privatefinalConversationRepositoryrepository;publicConversationcreate(UserContextuser){InstantnowInstant.now();ConversationconversationnewConversation(conv_UUID.randomUUID(),user.tenantId(),user.userId(),null,ConversationStatus.ACTIVE,1L,now,now);repository.save(conversation);returnconversation;}}tenantId和userId来自认证上下文。十、所有权校验publicConversationrequireOwned(UserContextuser,StringconversationId){returnrepository.findOwned(conversationId,user.tenantId(),user.userId()).orElseThrow(()-newAccessDeniedException(无权访问该会话));}不能只验证会话存在。十一、同步对话服务ServicepublicclassEnterpriseConversationAiService{privatefinalChatClientchatClient;privatefinalConversationServiceconversationService;privatefinalChatHistoryRepositoryhistoryRepository;publicStringchat(UserContextuser,StringconversationId,Stringmessage){conversationService.requireOwned(user,conversationId);StringrequestIdUUID.randomUUID().toString();historyRepository.saveUserMessage(user,conversationId,requestId,message);try{StringanswerchatClient.prompt().advisors(spec-spec.param(ChatMemory.CONVERSATION_ID,conversationId).param(tenantId,user.tenantId())).user(message).call().content();historyRepository.saveAssistantMessage(user,conversationId,requestId,answer,MessageStatus.COMPLETED);returnanswer;}catch(RuntimeExceptionexception){historyRepository.markFailed(conversationId,requestId,exception.getClass().getSimpleName());throwexception;}}}十二、避免长数据库事务模型调用可能持续数秒。不要开启数据库事务 → 写用户消息 → 等模型十几秒 → 写助手消息 → 提交推荐事务1写用户消息 → 模型调用 → 事务2写助手消息或失败状态通过request_id关联两段记录。十三、流式调用publicFluxServerSentEventChatStreamEventstream(UserContextuser,StringconversationId,Stringmessage){conversationService.requireOwned(user,conversationId);StringrequestIdUUID.randomUUID().toString();historyRepository.saveUserMessage(user,conversationId,requestId,message);StringBuilderbuffernewStringBuilder();returnchatClient.prompt().advisors(spec-spec.param(ChatMemory.CONVERSATION_ID,conversationId)).user(message).stream().content().doOnNext(buffer::append).map(chunk-ServerSentEvent.builder(newChatStreamEvent(requestId,delta,chunk)).build()).doOnComplete(()-historyRepository.saveAssistantMessage(user,conversationId,requestId,buffer.toString(),MessageStatus.COMPLETED)).doOnCancel(()-historyRepository.markCancelled(conversationId,requestId,buffer.toString()));}需要继续验证Provider在取消订阅后是否真正终止上游生成。十四、半段回答是否进入Memory用户取消后可能只生成根据你的问题主要有三个方面第一……不建议把这段内容当作完整Assistant消息进入稳定Memory。可选策略取消时不写Memory 完整Chat History保存CANCELLED消息 下一轮提示用户上一轮已中断十五、为什么需要上下文压缩长期会话会不断消耗Token。只扩大Message Window会导致成本上升延迟增加重要信息被噪声淹没超出上下文窗口。推荐上下文结构化摘要 最近若干轮消息 当前RAG证据十六、摘要表CREATETABLEai_conversation_summary(idVARCHAR(64)PRIMARYKEY,conversation_idVARCHAR(64)NOTNULL,tenant_idVARCHAR(64)NOTNULL,summary_versionBIGINTNOTNULL,covered_until_message_idVARCHAR(64)NOTNULL,content JSONBNOTNULL,created_atTIMESTAMPNOTNULL);结构化内容{goal:构建企业RAG方案,facts:[客户使用PostgreSQL,数据不能离开私有云],decisions:[向量存储选择pgvector],openQuestions:[峰值并发尚未确认]}十七、什么时候触发压缩可以使用消息数量阈值 Token预算阈值 会话持续时间 工具事件数量推荐以Token预算为主预计上下文Token 可用历史预算的80% → 触发压缩十八、压缩流程1. 读取上次摘要 2. 读取摘要之后的新消息 3. 生成新结构化摘要 4. 校验Schema 5. 保存summary_version 6. 更新covered_until_message_id 7. 后续Prompt加载摘要和近期消息原始消息不删除用于审计和重新生成摘要。十九、摘要不能替代业务事实模型摘要可能错误。以下信息必须从业务系统实时获取订单状态当前权限余额-审批结果产品库存合同版本。摘要只能作为对话背景不能作为强一致事实来源。二十、会话清空与删除清空模型上下文chatMemory.clear(conversationId);同时增加memory_version使缓存失效。删除会话流程标记DELETING → 删除Memory → 删除摘要和向量记忆 → 处理附件 → 按策略删除或匿名化History → 标记DELETED二十一、监控指标chat_conversation_created_count chat_memory_message_count chat_memory_load_duration chat_memory_clear_count conversation_cross_owner_denied_count summary_generation_count summary_validation_failed_count stream_cancelled_count history_memory_write_mismatch_count二十二、测试场景同一会话连续多轮 同一用户两个窗口 同租户两个用户 两个不同租户 应用重启 请求进入不同实例 流式取消 模型超时 Memory清空 摘要触发 非法conversationId访问最重要的测试是多租户隔离。二十三、本篇完整架构认证用户 → Conversation所有权 → 完整History写入 → 显式conversationId → MessageChatMemoryAdvisor → 持久化Memory Repository → ChatClient → 模型 → 保存状态 → Token预算触发摘要总结生产级Spring AI多轮对话不能只注册一个Memory Advisor。完整方案必须同时解决稳定会话ID 租户与用户隔离 持久化Memory 独立Chat History 流式状态 上下文压缩 审计与删除下一篇将继续实现Spring AI企业级应用实战5Tool Calling、权限校验、幂等与人工确认。