Spring AI 2.0 Chat Memory报conversationId不能为空?升级后的完整排查方法

📅 2026/7/28 23:36:56
Spring AI 2.0 Chat Memory报conversationId不能为空?升级后的完整排查方法
文章摘要Spring AI 2.0升级后使用MessageChatMemoryAdvisor或VectorStoreChatMemoryAdvisor的项目可能直接抛出conversationId不能为空、缺少ChatMemory.CONVERSATION_ID或历史对话突然失效。原因是新版本要求每次经过内置Memory Advisor的调用都显式提供会话ID旧版默认会话ID和Builder上的.conversationId()方式已经不再适用。本文给出正确传参、多租户会话ID设计、流式接口、异步线程和常见误区的完整排查方法。一、典型错误IllegalArgumentException: conversationId must not be null旧代码ChatClientchatClientChatClient.builder(chatModel).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();调用chatClient.prompt().user(message).call().content();这里注册了Memory Advisor却没有给当前请求传入会话ID。二、为什么新版本强制显式会话ID旧版默认会话ID容易造成所有用户共用同一会话 测试数据污染生产 多租户消息串线 无法追踪对话归属新版本采用快速失败没有conversationId → 立即抛出异常这比静默串话更安全。三、正确调用方式StringanswerchatClient.prompt().advisors(spec-spec.param(ChatMemory.CONVERSATION_ID,conversationId)).user(message).call().content();关键是把ChatMemory.CONVERSATION_ID放进Advisor上下文而不是拼进User Prompt。四、旧版Builder方式为什么不能继续用旧代码可能写MessageChatMemoryAdvisor.builder(chatMemory).conversationId(default).build();Spring AI 2.0已经移除这种在Advisor构建时固定会话ID的方式。原因很直接一个ChatClient Bean → 服务很多用户 → 固定会话ID会导致串话正确方式是Advisor在Bean中注册 会话ID在每次请求中传入五、不要每次随机生成新ID错误StringconversationIdUUID.randomUUID().toString();如果每次请求都执行三轮对话会变成三个独立会话。正确流程创建会话 → 返回conversationId → 前端持续保存 → 后续消息重复携带请求示例{conversationId:conv_01JXYZ,message:继续解释第二点}六、不要直接用userId当conversationId同一用户可能同时打开客服咨询技术问答售前方案数据分析。如果conversationId userId这些窗口会被合并。推荐归属结构tenantId userId 业务场景 conversationId对外只暴露随机ID后端保存完整归属。七、多租户必须校验会话所有权不能相信客户端提交的conversationId。服务端必须确认会话存在 属于当前tenantId 属于当前userId 状态可用publicConversationloadOwned(StringtenantId,StringuserId,StringconversationId){Conversationconversationrepository.findById(conversationId).orElseThrow();if(!conversation.tenantId().equals(tenantId)||!conversation.userId().equals(userId)){thrownewAccessDeniedException(无权访问该会话);}returnconversation;}完成所有权校验后才能把会话ID交给Memory Advisor。八、流式调用也要传conversationIdFluxStringresponsechatClient.prompt().advisors(spec-spec.param(ChatMemory.CONVERSATION_ID,conversationId)).user(message).stream().content();同步接口正常、流式接口失忆时检查是否使用同一个ChatClient流式调用是否传Advisor参数自定义Advisor是否只实现CallAdvisor取消流后是否错误保存半段消息。九、异步线程容易丢上下文错误CompletableFuture.supplyAsync(()-chatClient.prompt().user(message).call().content());原请求中的tenantId、userId、conversationId和Trace ID不会自动进入新线程。推荐显式传递publicrecordAiRequestContext(StringtenantId,StringuserId,StringconversationId,StringrequestId){}异步任务接收完整上下文不依赖ThreadLocal。十、检查是否注入了正确的ChatClient配置Bean(memoryChatClient)ChatClientmemoryChatClient(ChatClient.Builderbuilder,ChatMemorychatMemory){returnbuilder.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();}业务却重新执行this.chatClientbuilder.build();新客户端没有Memory Advisor。应注入publicAiService(Qualifier(memoryChatClient)ChatClientchatClient){this.chatClientchatClient;}十一、InMemory导致重启后失忆默认内存仓库适合本地开发单元测试单实例Demo。不适合多实例容器重启长期会话生产环境。生产应使用JDBC、Cassandra、Neo4j或自定义持久化Repository。十二、Chat Memory不是完整聊天记录Chat Memory只保存模型当前需要的上下文窗口策略可能淘汰旧消息。完整Chat History应单独保存chat_conversation chat_message推荐完整消息 → Chat History数据库 模型上下文 → Chat Memory十三、MessageWindow为什么少了旧消息MessageWindowChatMemory会保留最近N条消息超过窗口后淘汰旧内容。Spring AI 2.0会尽量按用户回合边界裁剪避免从一次交互中间截断但它仍然不是长期存档。长期对话应采用近期窗口 结构化摘要 必要时的长期语义记忆十四、工具调用出现历史重复怎么办检查是否同时使用手工拼接历史 MessageChatMemoryAdvisor还要检查是否注册了多个Memory AdvisorToolCallingAdvisor是否自己维护循环历史当前Prompt是否已经包含MemoryAdvisor顺序是否合理。十五、清空会话要处理哪些数据chatMemory.clear(conversationId);还需要按业务规则处理完整Chat History摘要长期记忆缓存附件向量索引。“清空页面”与“物理删除数据”不是同一件事。十六、推荐Controller写法PostMapping(/conversations/{conversationId}/messages)publicChatResponsechat(PathVariableStringconversationId,Authenticationauthentication,RequestBodyChatRequestrequest){UserContextusercurrentUser(authentication);conversationService.assertOwner(user.tenantId(),user.userId(),conversationId);StringanswerchatClient.prompt().advisors(spec-spec.param(ChatMemory.CONVERSATION_ID,conversationId)).user(request.message()).call().content();returnnewChatResponse(conversationId,answer);}tenantId和userId应从认证上下文读取而不是相信请求Body。十七、排查清单□ 每次请求传ChatMemory.CONVERSATION_ID □ 不再使用DEFAULT_CONVERSATION_ID □ 不再调用Builder.conversationId() □ 同一会话使用稳定ID □ 没有每次随机生成ID □ 没有直接用userId合并所有窗口 □ 会话归属经过租户和用户校验 □ 同步与流式调用都传会话ID □ 异步线程显式传上下文 □ 注入了注册Memory Advisor的ChatClient □ 生产环境没有只使用InMemory仓库 □ Chat History与Chat Memory分开保存总结Spring AI 2.0要求显式提供conversationId是一次安全性和可维护性提升。正确方案不是寻找新的“默认会话ID”而是建立稳定的会话生命周期 租户和用户归属校验 每次请求显式传递 持久化Memory Repository 独立Chat History