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
旧代码:
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
调用:
chatClient.prompt()
.user(message)
.call()
.content();
这里注册了Memory Advisor,却没有给当前请求传入会话ID。
二、为什么新版本强制显式会话ID
旧版默认会话ID容易造成:
所有用户共用同一会话
测试数据污染生产
多租户消息串线
无法追踪对话归属
新版本采用快速失败:
没有conversationId
→ 立即抛出异常
这比静默串话更安全。
三、正确调用方式
String answer = chatClient.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
错误:
String conversationId =
UUID.randomUUID().toString();
如果每次请求都执行,三轮对话会变成三个独立会话。
正确流程:
创建会话
→ 返回conversationId
→ 前端持续保存
→ 后续消息重复携带
请求示例:
{
"conversationId": "conv_01JXYZ",
"message": "继续解释第二点"
}
六、不要直接用userId当conversationId
同一用户可能同时打开:
- 客服咨询;
- 技术问答;
- 售前方案;
- 数据分析。
如果:
conversationId = userId
这些窗口会被合并。
推荐归属结构:
tenantId
+userId
+业务场景
+conversationId
对外只暴露随机ID,后端保存完整归属。
七、多租户必须校验会话所有权
不能相信客户端提交的conversationId。
服务端必须确认:
会话存在
属于当前tenantId
属于当前userId
状态可用
public Conversation loadOwned(
String tenantId,
String userId,
String conversationId
) {
Conversation conversation =
repository.findById(conversationId)
.orElseThrow();
if (!conversation.tenantId().equals(tenantId)
|| !conversation.userId().equals(userId)) {
throw new AccessDeniedException("无权访问该会话");
}
return conversation;
}
完成所有权校验后,才能把会话ID交给Memory Advisor。
八、流式调用也要传conversationId
Flux response = chatClient.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不会自动进入新线程。
推荐显式传递:
public record AiRequestContext(
String tenantId,
String userId,
String conversationId,
String requestId
) {
}
异步任务接收完整上下文,不依赖ThreadLocal。
十、检查是否注入了正确的ChatClient
配置:
@Bean("memoryChatClient")
ChatClient memoryChatClient(
ChatClient.Builder builder,
ChatMemory chatMemory
) {
return builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
业务却重新执行:
this.chatClient = builder.build();
新客户端没有Memory Advisor。
应注入:
public AiService(
@Qualifier("memoryChatClient")
ChatClient chatClient
) {
this.chatClient = chatClient;
}
十一、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 Advisor;
- ToolCallingAdvisor是否自己维护循环历史;
- 当前Prompt是否已经包含Memory;
- Advisor顺序是否合理。
十五、清空会话要处理哪些数据
chatMemory.clear(conversationId);
还需要按业务规则处理:
- 完整Chat History;
- 摘要;
- 长期记忆;
- 缓存;
- 附件;
- 向量索引。
“清空页面”与“物理删除数据”不是同一件事。
十六、推荐Controller写法
@PostMapping("/conversations/{conversationId}/messages")
public ChatResponse chat(
@PathVariable String conversationId,
Authentication authentication,
@RequestBody ChatRequest request
) {
UserContext user = currentUser(authentication);
conversationService.assertOwner(
user.tenantId(),
user.userId(),
conversationId
);
String answer = chatClient.prompt()
.advisors(spec -> spec.param(
ChatMemory.CONVERSATION_ID,
conversationId
))
.user(request.message())
.call()
.content();
return new ChatResponse(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
延伸阅读
如果你正在关注企业级 AI 应用、Spring AI、RAG、Agent 与 MCP 工程化落地,欢迎访问 智元界:
https://www.zyentor.com/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。