一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

用Spring AI实现多轮对话记忆:别再让AI每次都失忆

时间:2026-07-22 08:36:49 编辑:袖梨 来源:一聚教程网

用Spring AI实现多轮对话记忆,别再让AI每次都"失忆"


做AI对话功能,最容易被用户吐槽的一句话是什么?

用Spring AI实现多轮对话记忆,别再让AI每次都"失忆"

"你刚才不是说过了吗?怎么又问我?"

大模型天生是"金鱼记忆"——每次调用都是独立的,上一次说了什么,它完全不记得。

这个问题我上周刚解决。用的Spring AI自带的ChatMemory机制,20行代码搞定。今天把方案拆干净。

问题有多蠢

先看一个真实场景。用户跟AI聊天:

 复制代码用户:我叫张三,做Java开发的
AI:你好张三!Java开发挺不错的。
用户:推荐几本适合我的书?
AI:请问您是从事什么行业的呢?

用户直接炸了:我刚说了我做Java的你没听到?

这不是模型笨,是架构问题。每次调API,你只传了当前这一句话进去,历史对话根本没带。模型看不到之前的内容,当然不记得。

最蠢的解法:自己拼字符串

很多人第一反应是:把聊天记录拼起来不就行了?

 复制代码// 别这么干
List<String> history = new ArrayList<>();public String chat(String userId, String userInput) {
    history.add("用户:" + userInput);
    
    // 把所有历史拼成一个长字符串
    String fullPrompt = String.join("n", history);
    
    String response = chatClient.prompt()
        .user(fullPrompt)
        .call()
        .content();
    
    history.add("AI:" + response);
    return response;
}

能用。但3个问题:

  1. token爆炸 — 聊了50轮后,每次调用都带上全部历史,token费用线性增长
  2. 没隔离 — 用户A和用户B的对话混在一起(上面代码是全局变量,真实场景更隐蔽)
  3. 没上限 — 聊了500轮,prompt长度超过模型上下文窗口,直接报错

Spring AI的正解:ChatMemory

Spring AI内置了一套对话记忆机制,核心就3个东西:

作用
ChatMemory存储对话历史的接口
MessageChatMemoryAdvisor自动把历史注入Prompt的拦截器
InMemoryChatMemory内存版实现(生产环境换持久化版)

第一步:配置ChatMemory

 复制代码@Configuration
public class AiConfig {    // 对话记忆存储(内存版,重启丢失)
    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }    // ChatClient配置,挂上记忆拦截器
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
        return builder
            .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(chatMemory)
                    .chatMemoryRetrieveSize(20)  // 最多带最近20条消息
                    .build()
            )
            .build();
    }
}

关键参数是chatMemoryRetrieveSize(20)——每次调用最多带最近20条消息(10轮对话),防止token爆炸。

第二步:对话时传入conversationId

 复制代码@Service
public class ChatService {    @Autowired
    private ChatClient chatClient;    public String chat(String userId, String userInput) {
        return chatClient.prompt()
            .user(userInput)
            .advisors(a -> a.param(
                ChatMemory.CONVERSATION_ID,  // 关键:用userId做会话隔离
                userId
            ))
            .call()
            .content();
    }
}

CONVERSATION_ID就是会话隔离的关键。不同userId对应不同的对话历史,互不干扰。

第三步:验证效果

 复制代码用户(userId=1001):我叫张三,做Java开发的
AI:你好张三!Java开发挺好的方向。用户(userId=1001):推荐几本适合我的书?
AI:根据你的Java开发背景,推荐以下几本:
    1.《Spring实战》—— Spring生态必读
    2.《Effective Java》—— 进阶必备
    ...

这次AI记住了。因为Spring AI自动把上一轮的对话历史注入到了Prompt里。

生产环境:换成持久化存储

InMemoryChatMemory重启就丢数据。生产环境必须持久化。

Spring AI提供了一个简洁的接口,自己实现就行:

 复制代码public class JdbcChatMemory implements ChatMemory {    @Autowired
    private JdbcTemplate jdbcTemplate;    @Override
    public void add(String conversationId, List<Message> messages) {
        // 把消息存到数据库
        for (Message msg : messages) {
            jdbcTemplate.update(
                "INSERT INTO chat_memory (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
                conversationId, msg.getMessageType().getValue(), msg.getText(), LocalDateTime.now()
            );
        }
    }    @Override
    public List<Message> get(String conversationId, int lastN) {
        // 查最近N条消息
        return jdbcTemplate.query(
            "SELECT role, content FROM chat_memory WHERE conversation_id = ? ORDER BY created_at DESC LIMIT ?",
            (rs, rowNum) -> {
                String role = rs.getString("role");
                String content = rs.getString("content");
                if ("user".equals(role)) {
                    return UserMessage.builder().text(content).build();
                }
                return AssistantMessage.builder().text(content).build();
            },
            conversationId, lastN
        );
    }    @Override
    public void clear(String conversationId) {
        jdbcTemplate.update("DELETE FROM chat_memory WHERE conversation_id = ?", conversationId);
    }
}

建张表就行:

 复制代码CREATE TABLE chat_memory (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    conversation_id VARCHAR(64) NOT NULL,
    role VARCHAR(16) NOT NULL,
    content TEXT NOT NULL,
    created_at DATETIME NOT NULL,
    INDEX idx_conv_time (conversation_id, created_at)
);

换掉Bean配置就完事了:

 复制代码@Bean
public ChatMemory chatMemory() {
    return new JdbcChatMemory();  // 替换InMemoryChatMemory
}

其他代码一行不用改。这就是接口的价值。

我踩的3个坑

坑1:System Prompt被历史消息"冲掉"

加了ChatMemory后,我发现AI的性格变了——之前System Prompt设定的"你是一个Java技术专家"不管用了。

原因:ChatMemory的消息列表里,System消息排在最前面,但历史消息多了之后,System消息的权重被稀释了。

解决:用defaultSystem()而不是手动拼System消息。Spring AI会确保System消息始终在消息列表的最前面。

 复制代码@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
    return builder
        .defaultSystem("你是一个Java技术专家,回答要简洁直接")  // 这个始终在最前面
        .defaultAdvisors(
            MessageChatMemoryAdvisor.builder(chatMemory).build()
        )
        .build();
}

坑2:流式响应和记忆冲突

.stream()做流式输出时,ChatMemory拿不到完整的AI回复——因为回复是一个token一个token回来的,不是一次性返回的。

结果:历史记录里存了一条空的AI回复。下一轮对话时,AI看到的上一轮回复是空的。

解决:Spring AI 1.0+的MessageChatMemoryAdvisor已经处理了这个问题,会自动等流式输出完成后存入完整内容。但如果用的是老版本(0.9.x),需要自己处理:

 复制代码// 老版本需要手动存历史
public Flux<String> streamChat(String userId, String userInput) {
    // 先手动添加用户消息到记忆
    chatMemory.add(userId, List.of(new UserMessage(userInput)));
    
    StringBuilder fullResponse = new StringBuilder();
    
    return chatClient.prompt()
        .user(userInput)
        .stream()
        .content()
        .doOnNext(token -> fullResponse.append(token))
        .doOnComplete(() -> {
            // 流结束后手动存入AI回复
            chatMemory.add(userId, List.of(
                new AssistantMessage(fullResponse.toString())
            ));
        });
}

坑3:并发用户的线程安全

InMemoryChatMemory底层用ConcurrentHashMap,线程安全没问题。但我自己写的JdbcChatMemory一开始没加事务,高并发下出现了消息顺序错乱。

解决add()方法加事务,保证同一会话的消息按顺序写入:

 复制代码@Transactional
public void add(String conversationId, List<Message> messages) {
    for (Message msg : messages) {
        jdbcTemplate.update(...);
    }
}

实际效果

我用这套方案做了一个内部知识库问答助手,200人同时使用。

指标无记忆有记忆
用户满意度3.2分4.6分
"你刚才说了"类投诉每天15+0
平均对话轮次2.3轮8.7轮
单次调用token波动大(无上下文)稳定(固定窗口)

结论很明显:对话记忆不是可选功能,是必备功能。

什么场景必须加记忆

我的经验,这几类场景不加记忆就没法用:

  1. 客服/助手类 — 用户会连续追问,上下文关联紧密
  2. 教学类 — 学生问问题,AI要根据学生的水平调整回答
  3. 代码审查类 — 先给了一段代码,后面问的问题依赖前面的上下文
  4. 多步骤任务 — 先配置A,再配置B,最后一起部署

相反,如果是"一问一答"的工具型场景(翻译、摘要、格式转换),不加也行。

小结

Spring AI的ChatMemory方案总结:

  1. 开发环境:用InMemoryChatMemory + MessageChatMemoryAdvisor,20行代码搞定
  2. 生产环境:实现ChatMemory接口,换成数据库/Redis存储
  3. 关键参数chatMemoryRetrieveSize控制历史消息数量,防止token爆炸
  4. 会话隔离:用CONVERSATION_ID区分不同用户/会话

我整理了一个 Spring AI 起步模板,clone下来改3行配置就能跑: GitHub搜 spring-ai-starter(lincyang720) 想聊Java转AI的具体路线,加我微信 newboy2004 备注"AI"

热门栏目