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:latest model for testing)
  • Dependencies: spring-ai-starter-model-chat-memory, spring-ai-starter-model-openai

Steps to reproduce

  1. Create a ChatClient with defaultSystem() prompt and MessageChatMemoryAdvisor configured
  2. Make a chat call with a conversation id
  3. Attempt a second chat call with the same conversation id
  4. 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:

  1. UserMessage
  2. AssistantMessage
  3. SystemMessage
  4. UserMessage

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:

  1. SystemMessage
  2. UserMessage
  3. AssistantMessage
  4. UserMessage

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:

  1. Final assembled request – shows memory messages first, then the new System/User:

Image

  1. ChatMemory contents – it looks like only User/Assistant/User are persisted, while System isn’t retained in memory:

Image

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:

  1. SystemMessage is not being correctly saved to the chat memory
    The relevant logic resides in org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor#before, and there is clearly a bug here. This is evidenced by the org.springframework.ai.chat.memory.MessageWindowChatMemory#process method, which contains extensive handling logic for SystemMessage. This indicates that, by design, SystemMessage is intended to be included in the chat memory. Therefore, the current behavior—failing to persist SystemMessage—deviates from the intended design.

  2. Ordering logic of SystemMessage within the memory
    The current handling of SystemMessage can be clearly observed in MessageWindowChatMemory#process:

  3. When adding a SystemMessage that 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 existing SystemMessage is removed.
  4. When adding a SystemMessage that 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 expect SystemMessage to 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:
SystemMessageUserMessageAssistantMessageSystemMessageUserMessageAssistantMessage

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.