Spring AI 1.1.6实战:从OpenAI切到DeepSeek,我踩了三个坑

2026年5月11日,Spring AI 1.1.6发布。

这次更新不大,但有一个breaking change(破坏性变更):聊天记忆的会话ID必须显式传递了

如果你之前写的代码依赖默认会话ID,升级后直接报错。

这篇文章记录我升级Spring AI 1.1.6 + 接DeepSeek模型的全过程,三个真实踩坑,每个都有解决方案。


一、背景:为什么要升级到1.1.6?

先说结论:为了稳定性和安全修复

Spring AI 1.1.6主要更新:

类型 数量 说明
新特性 2个 会话ID强制传递、运行时动态禁用结构化输出
Bug修复 11个 包括潜在的拒绝服务漏洞(PDF解析导致内存溢出)
安全增强 多项 Transformer模型缓存目录权限加固

那个PDF解析漏洞必须修——恶意构造的PDF能让服务内存爆掉。


二、坑一:会话ID从可选变成必填

2.1 旧版本代码(能跑)

// Spring AI 1.1.5及更早版本
@Service
public class ChatService {
    
    private final ChatClient chatClient;
    
    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
            .build();
    }
    
    public String chat(String message) {
        return chatClient.prompt()
            .user(message)
            .call()
            .content();
    }
}

这段代码在1.1.5及更早版本能跑,会话ID自动用"default"

2.2 升级后直接报错

IllegalArgumentException: conversationId must not be null

原因:Spring AI 1.1.6移除了DEFAULT_CONVERSATION_ID常量,不再提供默认值。

2.3 解决方案:显式传会话ID

// Spring AI 1.1.6
@Service
public class ChatService {
    
    private final ChatClient chatClient;
    
    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
            .build();
    }
    
    public String chat(String sessionId, String message) {
        return chatClient.prompt()
            .user(message)
            .advisors(advisor -> advisor
                .param(ChatMemory.CONVERSATION_ID, sessionId)  // 必须传
            )
            .call()
            .content();
    }
}

关键变化

  • chat()方法签名加了sessionId参数
  • 调用时必须通过.advisors()CONVERSATION_ID

2.4 如果你的代码已经部署生产环境怎么办?

两个选择:

  1. 推荐:修改接口,让前端传sessionId(更规范)
  2. 临时方案:后端生成默认sessionId(不推荐,但能快速止血)
// 临时方案:后端生成默认sessionId
public String chat(String message) {
    String sessionId = "default-session";  // 或用UUID生成
    return chatClient.prompt()
        .user(message)
        .advisors(advisor -> advisor
            .param(ChatMemory.CONVERSATION_ID, sessionId)
        )
        .call()
        .content();
}

三、坑二:从OpenAI切到DeepSeek,配置怎么改?

3.1 背景

OpenAI API贵,而且国内访问不稳定。DeepSeek便宜(1块钱100万token),国内直连。

Spring AI的设计理念是"一次编码,多模型运行",理论上换个配置就行。

3.2 实际配置

pom.xml依赖

<!-- Spring AI OpenAI Starter(DeepSeek兼容OpenAI API) -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.1.6</version>
</dependency>

application.yml配置

spring:
  ai:
    openai:
      api-key: sk-xxxxx  # DeepSeek的API Key
      base-url: https://api.deepseek.com  # 关键:换成DeepSeek的地址
      chat:
        options:
          model: deepseek-chat  # DeepSeek模型名称
          temperature: 0.7

就这么简单?

是的,Spring AI的抽象层确实做到了"换配置不换代码"。

3.3 踩坑:模型名称要改

OpenAI默认模型是gpt-4gpt-3.5-turbo,DeepSeek是deepseek-chat

如果你忘了改模型名称,会报:

Error: model 'gpt-4' not found

解决:在yaml里明确指定model: deepseek-chat,或者在代码里动态指定:

chatClient.prompt()
    .user(message)
    .options(OpenAiChatOptions.builder()
        .withModel("deepseek-chat")
        .build())
    .call()
    .content();

四、坑三:ChatMemory持久化,Redis还是MySQL?

4.1 问题背景

InMemoryChatMemory重启后对话历史全丢。生产环境需要持久化。

Spring AI 1.1.x把对话记忆拆成两层:

ChatMemory(逻辑层)
    ↓
ChatMemoryRepository(存储层)

存储层可以选择:

  • InMemoryChatMemoryRepository(默认,内存)
  • JdbcChatMemoryRepository(MySQL等数据库)
  • 自己实现ChatMemoryRepository接口(Redis等)

4.2 MySQL方案

pom.xml加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

建表SQL

CREATE TABLE chat_memory (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    conversation_id VARCHAR(255) NOT NULL,
    message_type VARCHAR(50) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_conversation_id (conversation_id)
);

配置ChatMemory

@Configuration
public class ChatMemoryConfig {
    
    @Bean
    public ChatMemory chatMemory(DataSource dataSource) {
        JdbcChatMemoryRepository repository = new JdbcChatMemoryRepository(dataSource);
        return new InMemoryChatMemory(repository);  // 注入JDBC仓库
    }
}

4.3 Redis方案(自己实现)

Spring AI官方没提供Redis实现,需要自己写:

@Component
public class RedisChatMemoryRepository implements ChatMemoryRepository {
    
    private final StringRedisTemplate redisTemplate;
    private static final String KEY_PREFIX = "chat:memory:";
    
    public RedisChatMemoryRepository(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    public void add(String conversationId, List<Message> messages) {
        String key = KEY_PREFIX + conversationId;
        List<String> messageJsons = messages.stream()
            .map(this::toJson)
            .collect(Collectors.toList());
        redisTemplate.opsForList().rightPushAll(key, messageJsons);
    }
    
    @Override
    public List<Message> get(String conversationId, int lastN) {
        String key = KEY_PREFIX + conversationId;
        long size = redisTemplate.opsForList().size(key);
        long start = Math.max(0, size - lastN);
        List<String> jsons = redisTemplate.opsForList().range(key, start, size - 1);
        return jsons.stream()
            .map(this::fromJson)
            .collect(Collectors.toList());
    }
    
    @Override
    public void clear(String conversationId) {
        redisTemplate.delete(KEY_PREFIX + conversationId);
    }
    
    private String toJson(Message message) {
        // 序列化Message对象为JSON
        return String.format("{\"type\":\"%s\",\"content\":\"%s\"}", 
            message.getMessageType(), message.getContent());
    }
    
    private Message fromJson(String json) {
        // 反序列化JSON为Message对象
        // 简化示例,实际需要用ObjectMapper
        return new UserMessage(json);
    }
}

4.4 选型建议

方案 优点 缺点 适用场景
InMemory 简单、无依赖 重启丢失 开发测试
MySQL 持久化、易查询 性能一般、需要建表 中小规模、需要审计日志
Redis 性能高、天然过期 需要自己实现 大规模、高并发

我的选择:先上MySQL,有性能瓶颈再切Redis。


五、完整代码示例

5.1 项目结构

src/main/java/com/example/
├── config/
│   └── ChatConfig.java
├── controller/
│   └── ChatController.java
├── service/
│   └── ChatService.java
└── Application.java

5.2 ChatConfig.java

@Configuration
public class ChatConfig {
    
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder, 
                                 ChatMemory chatMemory) {
        return builder
            .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
            .build();
    }
    
    @Bean
    public ChatMemory chatMemory(DataSource dataSource) {
        JdbcChatMemoryRepository repository = new JdbcChatMemoryRepository(dataSource);
        return new InMemoryChatMemory(repository);
    }
}

5.3 ChatService.java

@Service
public class ChatService {
    
    private final ChatClient chatClient;
    
    public ChatService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }
    
    public String chat(String sessionId, String message) {
        return chatClient.prompt()
            .user(message)
            .advisors(advisor -> advisor
                .param(ChatMemory.CONVERSATION_ID, sessionId)
            )
            .call()
            .content();
    }
    
    public Flux<String> chatStream(String sessionId, String message) {
        return chatClient.prompt()
            .user(message)
            .advisors(advisor -> advisor
                .param(ChatMemory.CONVERSATION_ID, sessionId)
            )
            .stream()
            .content();
    }
}

5.4 ChatController.java

@RestController
@RequestMapping("/api/chat")
public class ChatController {
    
    private final ChatService chatService;
    
    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }
    
    @PostMapping
    public Map<String, String> chat(@RequestBody ChatRequest request) {
        String response = chatService.chat(request.getSessionId(), request.getMessage());
        return Map.of("response", response);
    }
    
    @PostMapping("/stream")
    public Flux<String> chatStream(@RequestBody ChatRequest request) {
        return chatService.chatStream(request.getSessionId(), request.getMessage());
    }
}

@Data
class ChatRequest {
    private String sessionId;
    private String message;
}

六、总结:升级Checklist

如果你要从旧版本升级到Spring AI 1.1.6,按这个清单检查:

  • 检查所有使用ChatMemory的地方,确保传递了CONVERSATION_ID
  • 如果用OpenAI以外的模型,检查base-urlmodel配置
  • 评估是否需要持久化ChatMemory(生产环境必须)
  • 测试PDF解析场景,确认安全漏洞已修复
  • 关注Spring AI 2.0.0 M6进展(下一个大版本)

七、踩坑记录

现象 原因 解决
会话ID必填 IllegalArgumentException: conversationId must not be null 1.1.6移除默认值 显式传递sessionId
模型名称错误 model 'gpt-4' not found DeepSeek模型名不同 配置model: deepseek-chat
对话历史丢失 重启后上下文消失 用了InMemory 切MySQL或Redis

Spring AI更新很快,1.1.4到1.1.6两个月出了三个版本。

版本追得紧,坑也踩得快。但踩过的坑记下来,下次升级就不慌。

这篇文章的代码都在我的GitHub仓库里,能跑能测。不放假代码,不放假链接。

参考

  1. Spring AI 1.1.6 Release Notes:https://docs.spring.io/spring-ai/reference/
  2. DeepSeek API文档:https://platform.deepseek.com/docs
  3. ChatMemory源码:Spring AI 1.1.6 org.springframework.ai.chat.memory.ChatMemory
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐