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;  
    }  
}
相关推荐
李梨同学丶2 小时前
0201好虫子周刊
后端
思想在飞肢体在追2 小时前
Springboot项目配置Nacos
java·spring boot·后端·nacos
Loo国昌4 小时前
【垂类模型数据工程】第四阶段:高性能 Embedding 实战:从双编码器架构到 InfoNCE 损失函数详解
人工智能·后端·深度学习·自然语言处理·架构·transformer·embedding
玄同7655 小时前
Trae国际版与国内版深度测评:AI原生IDE的双生花
ide·人工智能·ai编程·cursor·ai-native·trae
乱世刀疤5 小时前
Claude Code实战:生成植物大战僵尸游戏
ai编程
ONE_PUNCH_Ge5 小时前
Go 语言泛型
开发语言·后端·golang
良许Linux5 小时前
DSP的选型和应用
后端·stm32·单片机·程序员·嵌入式
不光头强6 小时前
spring boot项目欢迎页设置方式
java·spring boot·后端
怪兽毕设6 小时前
基于SpringBoot的选课调查系统
java·vue.js·spring boot·后端·node.js·选课调查系统
学IT的周星星6 小时前
Spring Boot Web 开发实战:第二天,从零搭个“会卖萌”的小项目
spring boot·后端·tomcat