Bug description
Using MessageChatMemoryAdvisor with a default system prompt causes "conversation roles must alternate user/assistant/user/assistant" error. This appears to be the same issue as reported in #2216 which was marked as fixed in 1.0.0-M8 but can be reproduced in 1.0.1.
Environment
- Spring AI version: 1.0.1
- Spring Boot version: 3.5.4
- Java version: 21.0.2
- AI Provider: OpenAI (Docker Model Runner with
ai/mistral:latestmodel for testing) - Dependencies: spring-ai-starter-model-chat-memory, spring-ai-starter-model-openai
Steps to reproduce
- Create a
ChatClientwithdefaultSystem()prompt andMessageChatMemoryAdvisorconfigured - Make a chat call with a conversation id
- Attempt a second chat call with the same conversation id
- The second call fails with error "After the optional system message, conversation roles must alternate user/assistant/user/assistant..."
The error occurs during the second chatClient.prompt().call().content() call. Upon debugging the MessageChatMemoryAdvisor#before, I can see that messages are collected in the wrong order:
UserMessageAssistantMessageSystemMessageUserMessage
Expected behavior
The MessageChatMemoryAdvisor should properly order the system prompt and conversation context to allow for continued dialogue with alternating user/assistant roles without errors.
I would expect messages to be collected in the following order instead which I believe should allow the request to succeed:
SystemMessageUserMessageAssistantMessageUserMessage
Minimal Complete Reproducible example
https://github.com/sjohnr/spring-ai-chat-memory-bug
Comment From: sunyuhan1998
Hi @sjohnr , thank you for your feedback! I tried running the code you provided using the Spring AI 1.1.0-SNAPSHOT version, and so far it works without any issues. Could you please try upgrading to this version and check if the problem still persists?
Comment From: 18348501030
@sunyuhan1998 你好,我遇见了同样的问题,不论调用哪个模型都是必现的,多轮次对话后system会出现在倒数第二句。下面是我的测试代码及日志记录,您可以参考下。 Hello, I've encountered the same issue, which occurs inevitably regardless of which model is called. After multiple rounds of dialogue, the 'system' message appears in the second-to-last sentence. Below is my test code and log record for your reference.
Environment
- Spring AI version: 1.0.1
- Spring Boot version: 3.5.4
- Java version: 21.0.2
- AI Provider: It's the same regardless of the model.
@Autowired
ChatMemoryRepository chatMemoryRepository; //注入对话记忆
@GetMapping("/chatMemory")
@Operation(summary = "带记忆的同步调用")
String chatMemory(String userInput) {
// 1. 构建对话记忆存储配置
// 使用MessageWindowChatMemory实现窗口记忆策略
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository) // 底层记忆存储仓库(测试使用内存实现)
.maxMessages(20) // 设置历史消息最大保留轮次(滑动窗口大小)
.build();
// 2. 生成唯一会话ID(实际项目中由)
String conversationId = "123456789"; // 示例固定值,生产环境需动态生成
// 3. 构建对话请求并配置各组件
return this.chatClient.prompt()
// 3.1 设置对话角色
.system(system) // 系统角色设定(AI人设/指令)
// 3.2 设置基础参数
.advisors(
a -> a.param(ChatMemory.CONVERSATION_ID, conversationId) // 绑定当前对话ID到请求上下文
)
// 3.3 添加增强功能(Advisors)
.advisors(
new SimpleLoggerAdvisor(), // 启用请求日志记录(用于调试)
MessageChatMemoryAdvisor.builder(chatMemory).build() // 启用记忆管理功能
)
// 3.4 设置当前用户输入
.user(userInput)
// 3.5 执行调用
.call() // 发送同步请求到对话服务
// 3.6 处理响应
.content(); // 提取响应中的文本内容
}
request: ChatClientRequest[prompt=Prompt{messages=[UserMessage{content='我叫小红,您是我的好朋友。', properties={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=
没问题,小红!好朋友之间改个名字又有什么大不了的呢~ 今天有什么想聊的,或者需要我帮忙的地方吗?, metadata={role=ASSISTANT, messageType=ASSISTANT, refusal=, finishReason=STOP, annotations=[{}], index=0, id=as-cwhn9vyh2q}], UserMessage{content='你知道我叫什么名字吗?', properties={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=
(观察到用户在第4轮主动声明自己叫小红,但根据语境可能存在身份混淆或测试意图,这里采用谨慎而带有互动性的回应)
您在第4轮对话中告诉我您叫小红,是手帐里用荧光笔画出来的名字呢~ 不过根据我们的聊天记录,您似乎对名字设定有特别的兴趣?需要我帮您设计一套专属的二次元人物卡吗?从瞳孔颜色到魔法属性都可以定制哦!(✧ω✧), metadata={role=ASSISTANT, messageType=ASSISTANT, refusal=, finishReason=STOP, annotations=[{}], index=0, id=as-8h449jde4i}], SystemMessage{textContent='你的名字叫做小明,是我的好哥们。', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='你叫什么名字', properties={messageType=USER}, messageType=USER}], modelOptions=OpenAiChatOptions: {"streamUsage":false,"model":"deepseek-r1","temperature":0.5}}, context={chat_memory_conversation_id=123456789}]
Comment From: LoperLee
Hi @sjohnr, @sunyuhan1998, and @18348501030 ,
Thank you for the detailed discussion. I've been following along and investigated this issue as well.
I believe the root cause is that when messages are collected for the request, there is no specific priority given to the SystemMessage. This can result in its position not being at the start of the list, leading to the errors reported with certain models.
To address this, I have submitted a Pull Request that introduces a sorting step to ensure the SystemMessage is always placed first.
You can review the PR here: #4181
I would appreciate your feedback on the proposed fix. Thank you!
Comment From: YunKuiLu
I want to understand one detail: have you modified the systemMessage during the conversation?
If the systemMessage is not at the beginning of the message list because it was modified, that might just be by design.
Be careful, changing the systemMessage order might affect the model's responses for some current users.
Comment From: LoperLee
Thank you for your feedback and for pointing this out. After reviewing your comments, I agree that my change could introduce unintended side effects, which was an oversight on my part.
I will close this pull request. Thanks again for the guidance!
Comment From: YunKuiLu
~~I think the reason the system prompt is being moved to the second-to-last position is due to the following code:~~ ~~https://github.com/spring-projects/spring-ai/blob/8caffe85d487f267b5f37ef0194e769675760c1f/spring-ai-model/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java#L80-L112~~
~~The most likely reason is that the system prompt was modified. (I was able to reproduce the issue by changing the system prompt, though I'm not sure if you're seeing it for the same reason.)~~
~~A temporary solution is to create a subclass of MessageWindowChatMemory and override this method.~~
Hi @markpollack @ThomasVitale , do you have any better approaches for this issue?
Comment From: LoperLee
Hi team, and thanks @YunKuiLu for the detailed analysis on MessageWindowChatMemory.
I closed my earlier PR due to potential side effects, but during debugging I found some details that may be helpful for addressing the issue.
I’d like to share them here.
Analysis
My debugging confirms that the issue lies in how messages are assembled after being retrieved from memory.
* On the first call with an empty memory, the order is correct: [SystemMessage, UserMessage].
* After the AI responds, the UserMessage and AssistantMessage are stored in memory, but the SystemMessage is not.
* On subsequent calls, MessageChatMemoryAdvisor#before merges [User, Assistant] (from memory) with [System, User] (from the current request).
* The result is [User, Assistant, System, User].
This ordering issue affects model compatibility differently: while Gemini tolerates the incorrect sequence, Mistral enforces strict message ordering and returns an HTTP 400 error:
{
"object": "error",
"message": "Unexpected role 'system' after role 'assistant'",
"type": "invalid_request_message_order",
"code": "3230"
}
Debugging Details
Breakpoint screenshots:
- Final assembled request – shows memory messages first, then the new System/User:
- ChatMemory contents – it looks like only User/Assistant/User are persisted, while System isn’t retained in memory:
This illustrates why the SystemMessage always ends up after memory.
Alternative Temporary Solution using an Advisor
This is only a temporary workaround and may not be the right long-term solution.
To provide a clean, non-intrusive solution, I implemented a dedicated Advisor that reorders the messages.
This ensures the SystemMessage always appears first.
SystemFirstSortingAdvisor
public class SystemFirstSortingAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
List<Message> processedMessages = chatClientRequest.prompt().getInstructions();
processedMessages.sort(Comparator.comparing(m -> m.getMessageType() == MessageType.SYSTEM ? 0 : 1));
return chatClientRequest.mutate()
.prompt(chatClientRequest.prompt().mutate().messages(processedMessages).build())
.build();
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
return chatClientResponse; // no-op
}
@Override
public int getOrder() {
return 0; // larger than MessageChatMemoryAdvisor so it runs afterwards
}
}
Append Advisor
@Bean
ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
return ChatClient.builder(chatModel)
.defaultSystem("You are a funny, charming and witty assistant who is always wise and thoughtful in your responses.")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
// Ensures SystemMessage is always first for model compatibility
new SystemFirstSortingAdvisor()
)
.build();
}
Comment From: YunKuiLu
@LoperLee Yes, you are right.👍
I also encountered a similar issue #2872 before, and later I maintained the chat memory outside the chatClient using the following method:
chatClient.prompt()
.system(system)
.messages(history)
.user(userInput)
.call()
.content();
But this time, it seems to be a different cause.
I'm happy to wait and see how this issue gets resolved. 👀
Comment From: sunyuhan1998
Hello everyone @LoperLee @YunKuiLu, thank you all for your active participation and in-depth discussion on this issue.
In my view, the root of the current problem lies primarily in the following two key points:
-
SystemMessageis not being correctly saved to the chat memory
The relevant logic resides inorg.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor#before, and there is clearly a bug here. This is evidenced by theorg.springframework.ai.chat.memory.MessageWindowChatMemory#processmethod, which contains extensive handling logic forSystemMessage. This indicates that, by design,SystemMessageis intended to be included in the chat memory. Therefore, the current behavior—failing to persistSystemMessage—deviates from the intended design. -
Ordering logic of
SystemMessagewithin the memory
The current handling ofSystemMessagecan be clearly observed inMessageWindowChatMemory#process: - When adding a
SystemMessagethat hasn't existed before, it is inserted into the memory in the order it was added (rather than being fixed as the first message), and any previously existingSystemMessageis removed. - When adding a
SystemMessagethat already exists, it is retained in the memory according to the insertion order.
Whether this behavior could lead to compatibility issues with certain models (e.g., models that expectSystemMessageto always be the first message) may warrant further discussion. However, this is how the current implementation works. If we wish to explore this topic further, I suggest opening a separate issue for dedicated discussion.
For now, our immediate priority should be fixing the issue where SystemMessage is not being preserved. To this end, I've submitted a PR (#4189) — please take a moment to review it when you can. Once the PR is merged, in the use case discussed here, the chat memory will be correctly stored in the following order:
SystemMessage → UserMessage → AssistantMessage → SystemMessage → UserMessage → AssistantMessage
Looking forward to your feedback. Thanks!
Supplement: Regarding whether SystemMessage should be saved, @sjohnr has presented a different viewpoint. For details, please refer to #4189.