Spring Ai 1.0.1中存在的问题:使用MessageChatMemoryAdvisor导致System未被正确的放在首位

使用MessageChatMemoryAdvisor导致System未被正确的放在首位

如下是使用Spring Ai实现多轮对话的官方例子(文档地址:https://docs.spring.io/spring-ai/reference/api/chat-memory.html):

java 复制代码
 @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(); // 提取响应中的文本内容
    }

如上示例是根据官方文档写的,实际测下来是有问题的,例如:有的模型在二轮对话的时候返回顺序问题报错,有的模型在二轮对话的时候丢失人设。(这个和模型的兼容性有关系)

经过排查可以发现二轮对话的Message中内容顺序会有问题(System人设被放到了倒数第二句):

java 复制代码
[
  {
    "role": "user",
    "content": "您好"
  },
  {
    "role": "assistant",
    "content": "\n\n您好!很高兴为您提供服务。请问有什么可以帮助您的吗?"
  },
  {
    "role": "system",
    "content": "你是智能助理小明"
  },
  {
    "role": "user",
    "content": "你叫什么名字啊"
  }
]

已上述的格式请求大模型会出现各种问题,因为正常规定的顺序就是S- U- A-U。

在GitHub中有大佬给出临时解决办法,创建一个SystemFirstSortingAdvisor来确保System人设始终保持第一位:(GitHub原问题地址:https://github.com/spring-projects/spring-ai/issues/4170)

java 复制代码
/**
 * 保证SYSTEM在最前面的增强
 */
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
    }
}

最后正确调用(在advisors中加入SystemFirstSortingAdvisor):

java 复制代码
 @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(),   // 启用记忆管理功能
                        new SystemFirstSortingAdvisor() // 确保系统消息优先排序
                )
                // 3.4 设置当前用户输入
                .user(userInput)
                // 3.5 执行调用
                .call() // 发送同步请求到对话服务
                // 3.6 处理响应
                .content(); // 提取响应中的文本内容
    }

综上所述,下个版本可能会优化掉这个BUG,目前就先使用SystemFirstSortingAdvisor来保证正常调用。