SpringAI 多轮对话报错 400 Bad Request

版本依赖

xml 复制代码
<dependencyManagement>  
    <dependencies>  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-dependencies</artifactId>  
            <version>3.5.4</version>  
            <type>pom</type>  
            <scope>import</scope>  
        </dependency>  
        <dependency>            
            <groupId>org.springframework.ai</groupId>  
            <artifactId>spring-ai-bom</artifactId>  
            <version>1.1.2</version>  
            <type>pom</type>  
            <scope>import</scope>  
        </dependency>  
    </dependencies>  
</dependencyManagement>

BUG 描述

当进行多轮对话的时候,会出现报错

php 复制代码
2026-01-12 20:17:02.116 [http-nio-8438-exec-5] ERROR -RuntimeException  
org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from POST https://xxxx.com/v1/chat/completions  
    at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:321)  
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:   
Assembly trace from producer [reactor.core.publisher.MonoIgnoreThen] :  
    reactor.core.publisher.Mono.then(Mono.java:4879)

刚开始的时候怀疑是因为有 advisor 干扰了 SYSTEM message 的顺序,但是我使用最小 demo 也不行

less 复制代码
@RestController  
@RequestMapping("/chat")  
public class ChatController {  
  
    private final ChatClient chatClient;  
    private final JdbcChatMemoryRepository jdbcChatMemoryRepository;  
  
    public ChatController(ChatClient.Builder chatClientBuilder, JdbcChatMemoryRepository jdbcChatMemoryRepository) {  
        this.chatClient = chatClientBuilder.build();  
        this.jdbcChatMemoryRepository = jdbcChatMemoryRepository;  
    }  
  
    @GetMapping(value = "/stream/{sessionId}")  
    public Flux<String> stream(@PathVariable(name = "sessionId") String sessionId, @RequestParam(name = "message") String message) {  
        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()  
                .chatMemoryRepository(jdbcChatMemoryRepository)  
                .maxMessages(100)  
                .build();  
        return chatClient.prompt()  
                .system("You are a helpful assistant.")  
                .user(message)  
                .advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, sessionId))  
                .advisors(MessageChatMemoryAdvisor.builder(messageWindowChatMemory).build())  
                .stream()  
                .content();  
    }
}

DEBUG

debug 位置 org.springframework.ai.openai.api.OpenAiApi#chatCompletionStream

第一次对话:

SYSTEM 类型在 0 位置

第二次对话:

SYSTEM 类型位置在 1,不符合 OpenAI 的规范。

OpenAI API 规范要求:

System 消息必须是消息数组中的第一条消息。如果 system 消息不在第一位,API 将返回 400 Bad Request 错误。

解决方案

使用一个 advisor 调整一下最后 message 的顺序

注意: 这里的 order 需要尽量小,也就是尽量最后执行,防止中间被修改

java 复制代码
import lombok.extern.slf4j.Slf4j;  
import org.springframework.ai.chat.client.ChatClientRequest;  
import org.springframework.ai.chat.client.ChatClientResponse;  
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;  
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;  
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;  
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;  
import org.springframework.ai.chat.messages.Message;  
import org.springframework.ai.chat.messages.SystemMessage;  
import org.springframework.ai.chat.prompt.Prompt;  
import reactor.core.publisher.Flux;  
  
import java.util.ArrayList;  
import java.util.Comparator;  
import java.util.List;  
  
/**  
 * 确保 SYSTEM 信息始终在 [0] 位置,否者报错 400  
 * @author leikooo  
 */@Slf4j  
public class SystemMessageFirstAdvisor implements CallAdvisor, StreamAdvisor {  
  
    private static final int DEFAULT_ORDER = Integer.MAX_VALUE - 1000;  
  
    @Override  
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {  
        ChatClientRequest newRequest = reorderSystemMessageFirst(chatClientRequest);  
        return callAdvisorChain.nextCall(newRequest);  
    }  
  
    @Override  
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {  
        ChatClientRequest newRequest = reorderSystemMessageFirst(chatClientRequest);  
        return streamAdvisorChain.nextStream(newRequest);  
    }  
  
    private ChatClientRequest reorderSystemMessageFirst(ChatClientRequest request) {  
        Prompt prompt = request.prompt();  
        List<Message> messages = new ArrayList<>(prompt.getInstructions());  
        if (messages.size() <= 1) {  
            return request;  
        }  
        messages.sort(Comparator.comparing(m -> !(m instanceof SystemMessage) ? 1 : 0));  
        Prompt newPrompt = new Prompt(messages, prompt.getOptions());  
        return new ChatClientRequest(newPrompt, request.context());  
    }  
  
    @Override  
    public String getName() {  
        return "SystemMessageFirstAdvisor";  
    }  
  
    @Override  
    public int getOrder() {  
        return DEFAULT_ORDER;  
    }  
}
less 复制代码
@GetMapping(value = "/stream/{sessionId}")  
public Flux<String> stream(@PathVariable(name = "sessionId") String sessionId, @RequestParam(name = "message") String message) {  
    MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()  
            .chatMemoryRepository(jdbcChatMemoryRepository)  
            .maxMessages(100)  
            .build();  
    return chatClient.prompt()  
            .system("You are a helpful assistant.")  
            .user(message)  
            .advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, sessionId))  
            .advisors(MessageChatMemoryAdvisor.builder(messageWindowChatMemory).build())  
            .advisors(new SystemMessageFirstAdvisor())  
            .stream()  
            .content();  
}

debug 发现 SYSTEM 在 0 位置,可以正常对话


关于顺序问题:

测试的话,创建了一个 LogAdvisor 他的 order 是 0。然后 SystemMessageFirstAdvisor 的 order 是 Integer.MAX_VALUE - 1000

debug 位置 org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain#nextStream

typescript 复制代码
import lombok.extern.slf4j.Slf4j;  
import org.springframework.ai.chat.client.ChatClientRequest;  
import org.springframework.ai.chat.client.ChatClientResponse;  
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;  
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;  
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;  
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;  
import reactor.core.publisher.Flux;  
  
/**  
 * @author <a href="https://github.com/lieeew">leikooo</a>  
 * @date 2026/1/1  
 * @description 输出日志 log  
 */@Slf4j  
public class LogAdvisor implements StreamAdvisor,  CallAdvisor {  
    @Override  
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {  
        log.info("log advisor before request {}", callAdvisorChain);  
        ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);  
        log.info("log advisor after request {}", chatClientResponse);  
        return chatClientResponse;  
    }  
  
    @Override  
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {  
        log.info("stream log advisor before request {}", chatClientRequest);  
        return streamAdvisorChain.nextStream(chatClientRequest);  
    }  
  
    @Override  
    public String getName() {  
        return "LogAdvisor";  
    }  
  
    @Override  
    public int getOrder() {  
        return 0;  
    }  
}
相关推荐
逍遥德1 天前
如何学编程之01.理论篇.如何通过阅读代码来提高自己的编程能力?
前端·后端·程序人生·重构·软件构建·代码规范
猫头虎1 天前
2026年AI产业13大趋势预测:Vibe Coding创作者经济元年到来,占冰强专家解读AIGC未来图景
人工智能·开源·prompt·aigc·ai编程·远程工作·agi
MX_93591 天前
Spring的bean工厂后处理器和Bean后处理器
java·后端·spring
程序员泠零澪回家种桔子1 天前
Spring AI框架全方位详解
java·人工智能·后端·spring·ai·架构
kjkdd1 天前
6.1 核心组件(Agent)
python·ai·语言模型·langchain·ai编程
源代码•宸1 天前
大厂技术岗面试之谈薪资
经验分享·后端·面试·职场和发展·golang·大厂·职级水平的薪资
晚霞的不甘1 天前
CANN 编译器深度解析:UB、L1 与 Global Memory 的协同调度机制
java·后端·spring·架构·音视频
喵叔哟1 天前
06-ASPNETCore-WebAPI开发
服务器·后端·c#
Charlie_lll1 天前
力扣解题-移动零
后端·算法·leetcode
打工的小王1 天前
Spring Boot(三)Spring Boot整合SpringMVC
java·spring boot·后端