使用MessageChatMemoryAdvisor导致System未被正确的放在首位
如下是使用Spring Ai实现多轮对话的官方例子(文档地址:https://docs.spring.io/spring-ai/reference/api/chat-memory.html):
@AutowiredChatMemoryRepository 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人设被放到了倒数第二句):
[{"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)
/*** 保证SYSTEM在最前面的增强*/
public class SystemFirstSortingAdvisor implements BaseAdvisor {@Overridepublic 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();}@Overridepublic ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {return chatClientResponse; // no-op}@Overridepublic int getOrder() {return 0; // larger than MessageChatMemoryAdvisor so it runs afterwards}
}
最后正确调用(在advisors中加入SystemFirstSortingAdvisor):
@AutowiredChatMemoryRepository 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来保证正常调用。