Spring AI 1.x + DeepSeek:给AI装个“大脑”,多轮对话不再失忆

阅读时长:6分钟 | 适合已接入DeepSeek但想做多轮对话的开发者

先说说你大概率遇到过的情况

写了个ChatBot,第一句问“我叫小明”,AI记住了。

第二句问“我叫什么”,AI回答“你叫小明”。完美。

第三句问“我今年18岁”,AI记住了。

第四句问“我多大了”,AI回答……

“抱歉,我没有上下文信息,请告诉我你的年龄。”

我当场裂开。

不是刚跟你说完吗?你转头就忘了?

后来我查了源码才发现,每次调用API都是独立的,默认不带记忆。你用ChatClient.create()发请求,它只认当前这轮对话,前面聊了什么一概不记得。

所以就有了今天这篇:怎么用Spring AI 1.x给AI加上“记忆”功能,让多轮对话真正连贯起来。

第一步:搞清楚Spring AI的记忆机制

Spring AI 1.x提供了ChatMemory接口,专门用来存储对话历史。

它是这么工作的:

组件作用
ChatMemory存储用户的历史消息列表
ChatMemoryStore持久化接口(可自定义)
InMemoryChatMemory默认实现(存在内存里)
ChatClient调用API时自动从ChatMemory读取历史

关键点是:每次调用chatClient.call()之前,Spring AI会自动把之前的历史消息拼到请求里,然后一起发给大模型。这样AI就能“记得”之前说过什么。

默认是内存存储,应用重启就丢数据。生产环境建议换成Redis持久化,但本地测试先用内存版就行。

下面是Spring AI记忆机制的工作流程图:

记忆存储层

用户发送消息

ChatService.chat()

chatMemory.addUserMessage()
存储用户消息

chatMemory.get(userId)
获取历史消息

ChatClient.call()
自动拼接历史消息

大模型API调用
(包含完整上下文)

获取AI回复

chatMemory.addAssistantMessage()
存储AI回复

返回回复给用户

ChatMemoryStore
(内存/Redis)

ChatMemory
管理消息列表

第二步:写个带记忆的Service

废话不多说,直接上代码。

@Service
public class ChatService {

    private final ChatClient chatClient;
    private final ChatMemory chatMemory;

    public ChatService(ChatClient.Builder chatClientBuilder) {
        // 创建内存存储
        ChatMemoryStore store = new InMemoryChatMemoryStore();
        // 设置记忆窗口:保留最近10条消息
        this.chatMemory = new InMemoryChatMemory(store, 10);
        this.chatClient = chatClientBuilder
                .withChatMemory(chatMemory)
                .build();
    }

    public String chat(String userId, String userMessage) {
        // 把用户消息存入记忆
        chatMemory.addUserMessage(userId, userMessage);
        
        // 调用API(会自动带上历史消息)
        ChatResponse response = chatClient.call(
            new Prompt(chatMemory.get(userId))
        );
        
        String aiMessage = response.getResult().getOutput().getContent();
        
        // 把AI回复也存入记忆
        chatMemory.addAssistantMessage(userId, aiMessage);
        
        return aiMessage;
    }
}

关键点注释:

  • InMemoryChatMemoryStore:存内存,重启就丢了,适合测试
  • 构造方法里的10:保留最近10条历史消息(再多容易超Token上限)
  • addUserMessage() / addAssistantMessage():手动把用户和AI的消息加入记忆库
  • chatMemory.get(userId):获取该用户的所有历史消息

下面是带记忆的ChatService类结构图:

依赖

组合

组合

实现

实现

ChatService

-ChatClient chatClient

-ChatMemory chatMemory

+ChatService(ChatClient.Builder)

+chat(String userId, String userMessage) : String

ChatClient

+call(Prompt prompt) : ChatResponse

+Builder withChatMemory(ChatMemory)

ChatMemory

+addUserMessage(String userId, String message)

+addAssistantMessage(String userId, String message)

+get(String userId) : List<Message>

InMemoryChatMemory

-ChatMemoryStore store

-int windowSize

InMemoryChatMemoryStore

+store(String userId, List<Message> messages)

+retrieve(String userId) : List<Message>

ChatMemoryStore

第三步:写个Controller测试

@RestController
@RequestMapping("/chat")
public class ChatController {

    @Autowired
    private ChatService chatService;

    @PostMapping("/memory")
    public Map<String, String> chatWithMemory(@RequestBody Map<String, String> request) {
        String userId = request.get("userId");
        String message = request.get("message");
        
        String reply = chatService.chat(userId, message);
        
        return Map.of("reply", reply);
    }
}

测试请求:

POST /chat/memory
{
    "userId": "zhangsan",
    "message": "我叫张三,今年18岁"
}

返回:

好的,张三,我记住了你今年18岁。

再问一句:

POST /chat/memory
{
    "userId": "zhangsan", 
    "message": "我多大了?"
}

返回:

你今年18岁。

这次它真的记住了。

下面是多轮对话的时序图:

大模型APIChatMemoryChatServiceChatController用户大模型APIChatMemoryChatServiceChatController用户第一轮对话第二轮对话POST /chat/memory{userId: "zhangsan", message: "我叫张三"}chat("zhangsan", "我叫张三")addUserMessage("zhangsan", "我叫张三")get("zhangsan") → []调用API(仅当前消息)"好的,张三"addAssistantMessage("zhangsan", "好的,张三")"好的,张三"{reply: "好的,张三"}POST /chat/memory{userId: "zhangsan", message: "我多大了?"}chat("zhangsan", "我多大了?")addUserMessage("zhangsan", "我多大了?")get("zhangsan") → ["我叫张三", "好的,张三"]调用API(带历史消息)"你叫张三"addAssistantMessage("zhangsan", "你叫张三")"你叫张三"{reply: "你叫张三"}

我踩过的两个坑

坑一:历史消息太长导致超时

记忆窗口设了10条消息,但每条消息本身可能很长(比如你丢了一大段代码进去),10条加起来可能就超Token了。

解决方案:结合max_tokens限制输出长度,同时在存入消息前做截断或摘要。如果消息超过2000字,先让模型做摘要再存。

坑二:忘记存AI的回复

很多人只存了用户消息,忘了存AI的回复。结果下一轮对话时,AI能听到你说过的所有话,但听不到自己说过什么。

结果就是:它知道张三18岁,但不记得自己确认过“张三18岁”这件事。对话依然不连贯。

解决方案:addAssistantMessage()这一步不能漏。

下面是消息存储策略对比图:

消息存储策略

内存存储
InMemoryChatMemoryStore

持久化存储
RedisChatMemoryStore

优点:
• 简单快速
• 无需额外依赖

缺点:
• 重启丢失数据
• 不适合生产

优点:
• 数据持久化
• 支持分布式
• 适合生产环境

缺点:
• 需要Redis依赖
• 网络延迟

消息窗口管理

固定窗口
(如:最近10条)

Token限制
(如:最大4000 tokens)

优点:
• 简单可控
• 避免无限增长

缺点:
• 可能丢失早期重要信息

优点:
• 更精确控制
• 避免超Token

缺点:
• 实现复杂
• 需要计算Token

进阶玩法:换成Redis持久化

内存存储重启就丢数据,生产环境肯定不行。

Spring AI支持自定义ChatMemoryStore,自己写个Redis实现就行。框架层面的示例代码官方仓库有,按需取用即可。

完整配置(抄作业版)

pom.xml依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- 如果要用Redis持久化 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.yml

spring:
  ai:
    openai:
      api-key: ${DEEPSEEK_API_KEY}
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
          timeout: 60000

说点实在的

多轮对话的记忆功能,本质是个“消息管理”问题。不是说AI记不住,而是你需要告诉它哪些是历史消息,然后把历史消息和当前问题一起发过去

Spring AI的ChatMemory把这层封装好了,开发者只需要关注两件事:

  1. 存储策略:用内存还是Redis?
  2. 窗口大小:保留多少条历史消息?

这两个定下来,剩下的就是调参和业务逻辑的事了。

明天准备发一篇 CSDN 8月技术热榜复盘,聊聊 Agent 战争、Java 生态、K8s 十周年这些有意思的事。

评论区告诉我:你现在的多轮对话,是存在哪里的?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值