告别 AI 对话 “失忆”!Spring AI 聊天记忆底层原理与全场景落地实战

0 阅读18分钟

很多开发者在集成AI对话功能时,都会遇到一个核心痛点:多轮对话中AI完全无法记住之前的沟通内容,每次对话都像首次交互,用户体验极差。而Spring AI作为Spring官方推出的AI应用开发框架,提供了一套完整、低侵入、高扩展的聊天记忆解决方案,彻底解决了大模型对话的无状态问题。

一、大模型对话的无状态本质与聊天记忆的核心逻辑

1.1 大模型为什么会“失忆”

大语言模型(LLM)的核心能力是基于输入的文本序列进行概率推理,生成符合语义的下一个token。它本身没有内置的持久化存储能力,每一次API调用都是完全独立的无状态请求,模型不会自动保存、读取上一次对话的任何内容。 举个通俗的例子:你第一次问“Java的三大特性是什么?”,AI给出了封装、继承、多态的回答;当你第二次问“那第一个特性的核心设计思想是什么?”,如果没有记忆机制,模型完全无法感知“第一个特性”对应的上下文,只能给出无意义的泛化回答。

1.2 聊天记忆的本质与核心三要素

聊天记忆的本质,是对用户与AI的历史对话消息进行有序管理、持久化存储、按需调度,并在每次对话请求时,将符合规则的历史内容注入到当前请求的Prompt中,让模型能够感知完整的上下文,实现连贯的多轮对话。 一套合格的聊天记忆体系,必须满足三个核心要素:

  1. 消息有序性:严格区分消息类型,保证用户提问与AI回复的时间顺序与成对关系,避免上下文错乱
  2. 会话隔离性:不同用户、不同会话的记忆数据完全隔离,杜绝数据串扰与越权访问
  3. 窗口可控性:可灵活控制历史消息的长度,避免内容过多导致模型token超限,保障请求稳定性与成本可控

二、Spring AI 聊天记忆的核心架构与组件

Spring AI的聊天记忆体系基于Spring的原生设计理念,采用分层架构,解耦了消息管理、持久化、请求拦截三大核心能力,具备极高的扩展性。

2.1 核心架构图

2.2 核心组件详解

Spring AI的聊天记忆能力围绕三个核心顶级接口展开,配合消息模型与拦截器机制,构成了完整的能力闭环。

2.2.1 Message 消息体系

Spring AI中所有对话内容都被封装为Message接口的实现类,核心实现分为三类,是记忆体系的最小数据单元:

  • SystemMessage:系统提示词,用于给模型设定角色、行为规则、输出边界,通常放在对话序列的最开头,单个会话一般仅保留一个
  • UserMessage:用户输入的提问内容,对应对话中的用户侧消息
  • AssistantMessage:AI模型生成的回复内容,对应对话中的AI侧消息

历史对话序列由UserMessageAssistantMessage成对组成,严格按照交互时间排序,是模型感知上下文的核心依据。

2.2.2 ChatMemory 会话记忆接口

ChatMemory是会话级记忆的核心接口,负责管理单个会话的全量消息,是记忆体系的内存操作入口,核心方法包括:

  • add(Message... messages):向当前会话追加消息
  • getMessages():获取当前会话的全量有序消息
  • clear():清空当前会话的所有消息
  • getConversationId():获取当前会话的唯一标识,用于会话隔离

Spring AI提供了默认的InMemoryChatMemory实现,基于ConcurrentHashMap实现内存级的消息管理,适合单机测试与低并发场景。

2.2.3 ChatMemoryRepository 持久化仓库接口

ChatMemoryRepository是持久化层的核心抽象接口,负责全量会话记忆的加载与持久化,是实现自定义存储的核心扩展点,核心方法包括:

  • getMessages(String conversationId):根据会话ID加载对应的历史消息序列
  • addMessage(String conversationId, Message message):向指定会话追加单条消息
  • addMessages(String conversationId, List<Message> messages):向指定会话批量追加消息
  • clear(String conversationId):清空指定会话的所有消息
  • listConversationIds():获取全量会话ID列表

该接口完全解耦了记忆逻辑与存储介质,开发者可以通过实现该接口,对接MySQL、Redis、MongoDB等任意存储系统,实现生产级的持久化能力。

2.2.4 ChatMemoryAdvisor 拦截器

ChatMemoryAdvisor是Spring AI记忆机制的核心调度器,基于Spring的AOP设计理念,实现了记忆能力与对话流程的无侵入集成:

  • 前置拦截:在对话请求发送给大模型之前,自动从ChatMemoryRepository中加载对应会话的历史消息,注入到当前请求的Prompt中
  • 后置拦截:在收到大模型的回复后,自动将当前用户消息与AI回复追加到ChatMemoryRepository中,完成记忆的持久化

开发者无需手动处理历史消息的拼接、存储,仅需通过配置即可实现完整的记忆能力,极大降低了开发成本。

三、环境搭建与基础依赖

3.1 项目基础环境

项目基于JDK 17开发,采用Maven进行项目管理,基础环境如下:

  • Spring Boot 3.4.5
  • Spring AI 1.2.0
  • MyBatis-Plus 3.5.7
  • MySQL 8.0
  • Lombok 1.18.30
  • FastJSON2 2.0.52
  • Guava 33.2.1-jre
  • SpringDoc OpenAPI 2.6.0

3.2 Maven 核心依赖配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.5</version>
        <relativePath/>
    </parent>
    <groupId>com.jam</groupId>
    <artifactId>spring-ai-chat-memory-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-ai-chat-memory-demo</name>
    <description>Spring AI Chat Memory Demo</description>
    
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.2.0</spring-ai.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <fastjson2.version>2.0.52</fastjson2.version>
        <guava.version>33.2.1-jre</guava.version>
        <springdoc.version>2.6.0</springdoc.version>
    </properties>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-openai</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.3 应用配置文件

spring:
  application:
    name: spring-ai-chat-memory-demo
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/ai_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: your_password
  ai:
    openai:
      api-key: your_api_key
      base-url: your_base_url
      chat:
        options:
          model: gpt-3.5-turbo
          temperature: 0.7
          max-tokens: 2048
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: isDeleted
      logic-delete-value: 1
      logic-not-delete-value: 0
springdoc:
  api-docs:
    enabled: true
  swagger-ui:
    enabled: true
    path: /swagger-ui.html
server:
  port: 8080

四、快速上手:内存级聊天记忆实现

基于Spring AI内置的InMemoryChatMemory,可以快速实现内存级的聊天记忆能力,无需额外的存储依赖,适合本地测试与功能验证。

4.1 ChatClient 配置类

package com.jam.demo.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.ChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ChatClient配置类
 * @author ken
 */
@Configuration
public class ChatClientConfig {

    /**
     * 配置内存级会话记忆
     * @return 会话记忆实例
     */
    @Bean
    public ChatMemory inMemoryChatMemory() {
        return new InMemoryChatMemory();
    }

    /**
     * 配置带记忆能力的ChatClient
     * @param builder ChatClient构建器
     * @param chatMemory 会话记忆实例
     * @return 带记忆能力的ChatClient实例
     */
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
        return builder
                .defaultAdvisors(ChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}

4.2 对话服务层实现

package com.jam.demo.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * 内存级对话服务实现
 * @author ken
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class InMemoryChatService {

    private final ChatClient chatClient;

    /**
     * 带记忆的对话
     * @param conversationId 会话唯一标识
     * @param content 用户提问内容
     * @return AI回复内容
     */
    public String chatWithMemory(String conversationId, String content) {
        if (!StringUtils.hasText(conversationId)) {
            throw new IllegalArgumentException("会话ID不能为空");
        }
        if (!StringUtils.hasText(content)) {
            throw new IllegalArgumentException("提问内容不能为空");
        }

        return chatClient.prompt()
                .user(content)
                .conversationId(conversationId)
                .call()
                .content();
    }

    /**
     * 清空指定会话的记忆
     * @param conversationId 会话唯一标识
     */
    public void clearChatMemory(String conversationId) {
        if (!StringUtils.hasText(conversationId)) {
            throw new IllegalArgumentException("会话ID不能为空");
        }
        chatClient.prompt()
                .conversationId(conversationId)
                .call()
                .chatMemory()
                .clear();
        log.info("会话{}的记忆已清空", conversationId);
    }
}

4.3 对话接口实现

package com.jam.demo.controller;

import com.jam.demo.service.InMemoryChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * 内存级对话接口
 * @author ken
 */
@RestController
@RequestMapping("/api/chat/in-memory")
@RequiredArgsConstructor
@Tag(name = "内存级对话接口", description = "基于内存的带记忆对话能力")
public class InMemoryChatController {

    private final InMemoryChatService inMemoryChatService;

    /**
     * 带记忆的对话接口
     */
    @PostMapping("/send")
    @Operation(summary = "发送带记忆的对话请求", description = "同一个conversationId的请求会共享上下文记忆")
    public ResponseEntity<Map<StringString>> chat(
            @Parameter(description = "会话唯一标识", required = true) @RequestParam String conversationId,
            @Parameter(description = "用户提问内容", required = true) @RequestParam String content) {
        String result = inMemoryChatService.chatWithMemory(conversationId, content);
        return ResponseEntity.ok(Map.of("content", result));
    }

    /**
     * 清空会话记忆接口
     */
    @DeleteMapping("/clear")
    @Operation(summary = "清空指定会话的记忆", description = "清空后该会话的历史上下文将丢失")
    public ResponseEntity<VoidclearMemory(
            @Parameter(description = "会话唯一标识", required = true) @RequestParam String conversationId) {
        inMemoryChatService.clearChatMemory(conversationId);
        return ResponseEntity.noContent().build();
    }
}

4.4 内存级实现的优缺点

  • 优点:集成简单、无额外存储依赖、请求延迟低、适合本地功能验证
  • 缺点:应用重启后记忆数据完全丢失、无法在分布式环境中共享会话、内存占用会随会话数量持续增长,仅适合测试场景,不可用于生产环境

五、生产级落地:基于MySQL的持久化聊天记忆实现

生产环境中,我们需要解决内存级实现的核心缺陷,通过实现Spring AI的ChatMemoryRepository接口,对接MySQL数据库,实现记忆数据的持久化,支持分布式环境部署,保障数据不丢失。

5.1 数据库表结构设计

CREATE TABLE `ai_chat_memory` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `conversation_id` varchar(64NOT NULL COMMENT '会话唯一标识',
  `message_type` varchar(16NOT NULL COMMENT '消息类型:SYSTEM/USER/ASSISTANT',
  `content` text NOT NULL COMMENT '消息内容',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标识:0-未删除,1-已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_conversation_id_create_time` (`conversation_id`,`create_time`),
  KEY `idx_conversation_id` (`conversation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI对话记忆表';

5.2 实体类定义

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 对话记忆实体类
 * @author ken
 */
@Data
@TableName("ai_chat_memory")
@Schema(description = "对话记忆实体")
public class ChatMemoryEntity {

    @Schema(description = "主键ID")
    @TableId(type = IdType.AUTO)
    private Long id;

    @Schema(description = "会话唯一标识")
    @TableField("conversation_id")
    private String conversationId;

    @Schema(description = "消息类型:SYSTEM/USER/ASSISTANT")
    @TableField("message_type")
    private String messageType;

    @Schema(description = "消息内容")
    @TableField("content")
    private String content;

    @Schema(description = "创建时间")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @Schema(description = "逻辑删除标识")
    @TableField("is_deleted")
    @TableLogic
    private Integer isDeleted;
}

5.3 Mapper接口定义

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.ChatMemoryEntity;
import org.apache.ibatis.annotations.Mapper;

/**
 * 对话记忆Mapper接口
 * @author ken
 */
@Mapper
public interface ChatMemoryMapper extends BaseMapper<ChatMemoryEntity> {
}

5.4 自定义持久化ChatMemoryRepository实现

package com.jam.demo.repository;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import com.jam.demo.entity.ChatMemoryEntity;
import com.jam.demo.mapper.ChatMemoryMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 基于MySQL的对话记忆持久化仓库实现
 * @author ken
 */
@Slf4j
@Repository
@RequiredArgsConstructor
public class JdbcChatMemoryRepository implements ChatMemoryRepository {

    private final ChatMemoryMapper chatMemoryMapper;
    private final TransactionTemplate transactionTemplate;

    @Override
    public List<MessagegetMessages(String conversationId) {
        if (!StringUtils.hasText(conversationId)) {
            return Lists.newArrayList();
        }

        List<ChatMemoryEntity> entityList = chatMemoryMapper.selectList(
                new LambdaQueryWrapper<ChatMemoryEntity>()
                        .eq(ChatMemoryEntity::getConversationId, conversationId)
                        .orderByAsc(ChatMemoryEntity::getCreateTime)
        );

        if (CollectionUtils.isEmpty(entityList)) {
            return Lists.newArrayList();
        }

        List<Message> messageList = Lists.newArrayList();
        for (ChatMemoryEntity entity : entityList) {
            MessageType messageType = MessageType.valueOf(entity.getMessageType());
            switch (messageType) {
                case SYSTEM -> messageList.add(new SystemMessage(entity.getContent()));
                case USER -> messageList.add(new UserMessage(entity.getContent()));
                case ASSISTANT -> messageList.add(new AssistantMessage(entity.getContent()));
                default -> log.warn("不支持的消息类型:{}", messageType);
            }
        }
        return messageList;
    }

    @Override
    public void addMessage(String conversationId, Message message) {
        if (!StringUtils.hasText(conversationId) || ObjectUtils.isEmpty(message)) {
            return;
        }

        ChatMemoryEntity entity = buildChatMemoryEntity(conversationId, message);
        chatMemoryMapper.insert(entity);
        log.debug("会话{}新增消息成功,消息类型:{}", conversationId, message.getMessageType());
    }

    @Override
    public void addMessages(String conversationId, List<Message> messages) {
        if (!StringUtils.hasText(conversationId) || CollectionUtils.isEmpty(messages)) {
            return;
        }

        transactionTemplate.execute(status -> {
            List<ChatMemoryEntity> entityList = Lists.newArrayList();
            for (Message message : messages) {
                entityList.add(buildChatMemoryEntity(conversationId, message));
            }
            for (ChatMemoryEntity entity : entityList) {
                chatMemoryMapper.insert(entity);
            }
            return Boolean.TRUE;
        });
        log.debug("会话{}批量新增{}条消息成功", conversationId, messages.size());
    }

    @Override
    public void clear(String conversationId) {
        if (!StringUtils.hasText(conversationId)) {
            return;
        }

        chatMemoryMapper.delete(
                new LambdaQueryWrapper<ChatMemoryEntity>()
                        .eq(ChatMemoryEntity::getConversationId, conversationId)
        );
        log.info("会话{}的记忆已清空", conversationId);
    }

    @Override
    public List<StringlistConversationIds() {
        List<ChatMemoryEntity> entityList = chatMemoryMapper.selectList(
                new LambdaQueryWrapper<ChatMemoryEntity>()
                        .select(ChatMemoryEntity::getConversationId)
                        .distinct()
        );

        return entityList.stream()
                .map(ChatMemoryEntity::getConversationId)
                .toList();
    }

    /**
     * 构建对话记忆实体
     * @param conversationId 会话ID
     * @param message 消息对象
     * @return 对话记忆实体
     */
    private ChatMemoryEntity buildChatMemoryEntity(String conversationId, Message message) {
        ChatMemoryEntity entity = new ChatMemoryEntity();
        entity.setConversationId(conversationId);
        entity.setMessageType(message.getMessageType().name());
        entity.setContent(message.getText());
        return entity;
    }
}

5.5 持久化ChatClient配置

package com.jam.demo.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.ChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * 持久化ChatClient配置类
 * @author ken
 */
@Configuration
public class PersistentChatClientConfig {

    /**
     * 配置带滑动窗口的持久化会话记忆
     * @param chatMemoryRepository 持久化仓库
     * @return 会话记忆实例
     */
    @Bean
    @Primary
    public ChatMemory persistentChatMemory(ChatMemoryRepository chatMemoryRepository) {
        return MessageWindowChatMemory.builder()
                .maxMessages(20)
                .chatMemoryRepository(chatMemoryRepository)
                .build();
    }

    /**
     * 配置带持久化记忆能力的ChatClient
     * @param builder ChatClient构建器
     * @param persistentChatMemory 持久化会话记忆
     * @return 带持久化记忆能力的ChatClient实例
     */
    @Bean
    @Primary
    public ChatClient persistentChatClient(ChatClient.Builder builder, ChatMemory persistentChatMemory) {
        return builder
                .defaultAdvisors(ChatMemoryAdvisor.builder(persistentChatMemory).build())
                .build();
    }
}

5.6 持久化对话服务实现

package com.jam.demo.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 持久化对话服务实现
 * @author ken
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class PersistentChatService {

    private final ChatClient persistentChatClient;
    private final ChatMemoryRepository chatMemoryRepository;

    /**
     * 带持久化记忆的对话
     * @param conversationId 会话唯一标识
     * @param content 用户提问内容
     * @return AI回复内容
     */
    public String chatWithPersistentMemory(String conversationId, String content) {
        if (!StringUtils.hasText(conversationId)) {
            throw new IllegalArgumentException("会话ID不能为空");
        }
        if (!StringUtils.hasText(content)) {
            throw new IllegalArgumentException("提问内容不能为空");
        }

        return persistentChatClient.prompt()
                .user(content)
                .conversationId(conversationId)
                .call()
                .content();
    }

    /**
     * 清空指定会话的持久化记忆
     * @param conversationId 会话唯一标识
     */
    public void clearChatMemory(String conversationId) {
        if (!StringUtils.hasText(conversationId)) {
            throw new IllegalArgumentException("会话ID不能为空");
        }
        chatMemoryRepository.clear(conversationId);
    }

    /**
     * 获取指定会话的历史消息
     * @param conversationId 会话唯一标识
     * @return 历史消息列表
     */
    public List<MessagegetChatHistory(String conversationId) {
        if (!StringUtils.hasText(conversationId)) {
            throw new IllegalArgumentException("会话ID不能为空");
        }
        return chatMemoryRepository.getMessages(conversationId);
    }

    /**
     * 获取所有会话ID列表
     * @return 会话ID列表
     */
    public List<StringlistAllConversations() {
        return chatMemoryRepository.listConversationIds();
    }
}

5.7 持久化对话接口实现

package com.jam.demo.controller;

import com.jam.demo.service.PersistentChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.messages.Message;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

/**
 * 持久化对话接口
 * @author ken
 */
@RestController
@RequestMapping("/api/chat/persistent")
@RequiredArgsConstructor
@Tag(name = "持久化对话接口", description = "基于MySQL的带记忆对话能力,支持分布式部署")
public class PersistentChatController {

    private final PersistentChatService persistentChatService;

    /**
     * 带持久化记忆的对话接口
     */
    @PostMapping("/send")
    @Operation(summary = "发送带持久化记忆的对话请求", description = "同一个conversationId的请求会共享持久化上下文记忆,应用重启不丢失")
    public ResponseEntity<Map<StringString>> chat(
            @Parameter(description = "会话唯一标识", required = true) @RequestParam String conversationId,
            @Parameter(description = "用户提问内容", required = true) @RequestParam String content) {
        String result = persistentChatService.chatWithPersistentMemory(conversationId, content);
        return ResponseEntity.ok(Map.of("content", result));
    }

    /**
     * 清空会话记忆接口
     */
    @DeleteMapping("/clear")
    @Operation(summary = "清空指定会话的持久化记忆", description = "清空后该会话的历史上下文将从数据库中删除")
    public ResponseEntity<VoidclearMemory(
            @Parameter(description = "会话唯一标识", required = true) @RequestParam String conversationId) {
        persistentChatService.clearChatMemory(conversationId);
        return ResponseEntity.noContent().build();
    }

    /**
     * 获取会话历史消息接口
     */
    @GetMapping("/history")
    @Operation(summary = "获取指定会话的历史消息", description = "查询该会话的所有历史对话消息")
    public ResponseEntity<List<Message>> getChatHistory(
            @Parameter(description = "会话唯一标识", required = true) @RequestParam String conversationId) {
        List<Messagehistory = persistentChatService.getChatHistory(conversationId);
        return ResponseEntity.ok(history);
    }

    /**
     * 获取所有会话ID接口
     */
    @GetMapping("/conversations")
    @Operation(summary = "获取所有会话ID列表", description = "查询系统中所有的会话ID")
    public ResponseEntity<List<String>> listAllConversations() {
        List<Stringconversations = persistentChatService.listAllConversations();
        return ResponseEntity.ok(conversations);
    }
}

六、高级特性:滑动窗口与总结式记忆

大模型的上下文窗口有固定的token上限,无限制累加历史消息会导致token超限、请求失败、成本飙升,Spring AI提供了两种核心解决方案,分别应对不同的业务场景。

6.1 滑动窗口记忆

滑动窗口记忆是最常用的token控制方案,核心逻辑是仅保留最近的N轮对话,当消息数量超过设定的阈值时,自动删除最早的历史消息,保证消息总数始终在可控范围内。 Spring AI内置的MessageWindowChatMemory已经实现了该能力,在之前的持久化配置中,我们通过maxMessages(20)设置了最大保留20条消息,也就是10轮完整的对话,完全满足绝大多数业务场景的需求。

滑动窗口记忆的核心配置

@Bean
public ChatMemory messageWindowChatMemory(ChatMemoryRepository chatMemoryRepository) {
    return MessageWindowChatMemory.builder()
            .maxMessages(20)
            .chatMemoryRepository(chatMemoryRepository)
            .build();
}

6.2 总结式记忆

总结式记忆适用于需要保留长周期上下文的业务场景,核心逻辑是当历史消息超过阈值时,不直接删除最早的消息,而是通过大模型对历史消息进行核心信息总结,用简短的总结文本替换原来的长历史消息,既保留了完整的上下文语义,又严格控制了token数量

总结式记忆实现示例

package com.jam.demo.memory;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
 * 总结式记忆处理器
 * @author ken
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class SummarizingChatMemoryHandler {

    private final ChatClient chatClient;
    private final ChatMemoryRepository chatMemoryRepository;

    private static final int MAX_MESSAGES_BEFORE_SUMMARY = 30;
    private static final String SUMMARY_PROMPT = "请对以下对话历史进行核心信息总结,保留关键的上下文语义,去除冗余内容,总结内容不超过500字。对话历史:\n%s";

    /**
     * 处理会话的历史消息总结
     * @param conversationId 会话ID
     */
    public void summarizeHistoryIfNeeded(String conversationId) {
        List<Message> messageList = chatMemoryRepository.getMessages(conversationId);
        if (CollectionUtils.isEmpty(messageList) || messageList.size() < MAX_MESSAGES_BEFORE_SUMMARY) {
            return;
        }

        StringBuilder historyBuilder = new StringBuilder();
        for (Message message : messageList) {
            historyBuilder.append(message.getMessageType().name())
                    .append(": ")
                    .append(message.getText())
                    .append("\n");
        }

        String summary = chatClient.prompt()
                .user(String.format(SUMMARY_PROMPT, historyBuilder.toString()))
                .call()
                .content();

        chatMemoryRepository.clear(conversationId);
        chatMemoryRepository.addMessage(conversationId, new SystemMessage("历史对话总结:" + summary));

        int startIndex = Math.max(0, messageList.size() - 10);
        List<Message> recentMessages = messageList.subList(startIndex, messageList.size());
        chatMemoryRepository.addMessages(conversationId, recentMessages);

        log.info("会话{}的历史消息总结完成,原消息数量:{},总结后保留消息数量:{}",
                conversationId, messageList.size(), recentMessages.size() + 1);
    }
}

七、带记忆对话的全流程底层原理

7.1 全流程执行时序图

7.2 核心环节原理拆解

  1. 拦截器调度ChatMemoryAdvisor基于Spring的AOP机制,在ChatClient的请求流程中插入了两个扩展点:请求发送前的前置处理,和响应返回后的后置处理,实现了记忆能力的完全无侵入集成,开发者无需修改业务代码即可接入。
  2. 会话隔离实现:通过conversationId作为唯一标识,每个会话对应独立的消息列表,Spring AI在加载和保存消息时,严格按照conversationId进行过滤,完全杜绝了不同会话之间的数据串扰。
  3. 消息注入逻辑:在前置处理环节,ChatMemoryAdvisor会将加载到的历史消息,按照时间顺序插入到当前用户消息的前面,保证模型能够按照交互顺序读取完整的上下文,同时保证SystemMessage始终在最开头的位置。
  4. 记忆持久化逻辑:在后置处理环节,ChatMemoryAdvisor会自动将本次交互的UserMessage和AssistantMessage成对追加到ChatMemoryRepository中,无需开发者手动调用保存接口,保证了记忆数据的完整性。

7.3 易混淆概念区分

概念核心职责适用场景
ChatMemory单个会话的内存级消息管理,负责当前会话的消息读写单次请求的消息处理,会话级的内存操作
ChatMemoryRepository全量会话的持久化管理,负责消息的加载、保存、清空跨请求、跨实例的记忆持久化,分布式场景
InMemoryChatMemory基于内存的ChatMemory实现,数据随应用重启丢失本地测试、单机低并发场景
MessageWindowChatMemory带滑动窗口的ChatMemory实现,自动控制消息数量生产环境绝大多数业务场景,控制token成本

八、生产环境最佳实践与坑点规避

8.1 会话安全与隔离最佳实践

  1. 会话ID生成规范:采用UUID、雪花算法等非连续的唯一ID生成策略,禁止使用自增数字作为会话ID,避免被恶意遍历。
  2. 用户与会话绑定:生产环境中,必须将conversationId与用户ID进行强绑定,在所有记忆操作前,校验当前登录用户是否有权限操作该会话ID,杜绝越权访问与数据泄露。
  3. 会话权限控制:禁止普通用户查询全量会话ID列表,该接口仅对管理员开放,且必须做分页与权限控制。

8.2 Token成本与稳定性最佳实践

  1. 强制设置滑动窗口:无论使用哪种记忆实现,都必须设置合理的消息数量上限,禁止无限制累加历史消息,根据使用的模型的上下文窗口,预留至少30%的token空间给当前请求与模型回复。
  2. 分级窗口策略:针对不同的业务场景设置不同的窗口大小,比如客服对话场景可以设置较大的窗口,普通问答场景设置较小的窗口,平衡体验与成本。
  3. Token数校验:在请求发送前,对全量Prompt的token数进行校验,如果超过模型的上限,自动触发历史消息的截断或总结,避免请求直接失败。

8.3 性能优化最佳实践

  1. 活跃会话缓存:针对高频访问的活跃会话,采用Redis进行缓存,缓存过期时间设置为30分钟,减少数据库的查询压力,提升请求响应速度。
  2. 批量操作优化:针对批量消息保存场景,采用编程式事务进行批量提交,减少数据库的IO次数,提升写入性能。
  3. 历史数据归档:设置定时任务,定期清理超过3个月未活跃的会话数据,或者将冷数据归档到离线存储,避免主表数据量过大导致的查询性能下降。

8.4 常见坑点规避

  1. SystemMessage重复累加:禁止将SystemMessage重复添加到历史消息中,否则会导致token的严重浪费,Spring AI的ChatMemoryAdvisor会自动处理SystemMessage的位置,无需手动追加。
  2. 消息顺序错乱:必须保证历史消息的顺序是UserMessage与AssistantMessage成对出现,禁止手动修改消息的创建时间与顺序,否则会导致模型无法正确理解上下文。
  3. 敏感信息泄露:在保存历史消息前,必须对用户输入的手机号、身份证号、密码等敏感信息进行脱敏处理,禁止明文存储敏感信息到数据库中。
  4. 分布式环境数据一致性:分布式环境中,必须使用持久化的ChatMemoryRepository实现,禁止使用InMemoryChatMemory,否则会导致不同实例之间的会话记忆不一致。

九、总结

Spring AI的聊天记忆体系,为Java开发者提供了一套优雅、低侵入、高扩展的多轮对话解决方案,完全屏蔽了底层的消息拼接、持久化、token控制等复杂逻辑,开发者仅需通过简单的配置,即可实现从本地测试到生产级落地的全流程能力。