🧠 Spring AI 对话记忆与工具调用完全指南

让大语言模型拥有"记忆力",并学会使用工具

一、为什么需要对话记忆?

1.1 大模型的"失忆症"

大语言模型(LLM)本质上是无状态的,每次请求都是独立的。就像金鱼只有7秒记忆,模型不会记住你上一秒说了什么。

Chat Memory Flow

场景对比:

无记忆对话 有记忆对话
用户:我叫张三
AI:你好张三!
用户:我叫什么?
AI:抱歉,我不知道你的名字。
用户:我叫张三
AI:你好张三!
用户:我叫什么?
AI:你刚才告诉我你叫张三。

二、Spring AI 记忆架构

Spring AI Architecture

Spring AI 通过 Advisor 机制 实现记忆功能,核心组件:

┌─────────────────────────────────────────┐
│           ChatClient (客户端)            │
├─────────────────────────────────────────┤
│  ┌─────────────────────────────────┐   │
│  │     MessageChatMemoryAdvisor    │   │
│  │         (记忆顾问)               │   │
│  └─────────────────────────────────┘   │
├─────────────────────────────────────────┤
│  ┌─────────────────────────────────┐   │
│  │        ChatMemory (接口)         │   │
│  │   ┌─────────────────────────┐   │   │
│  │   │ ChatMemoryRepository    │   │   │
│  │   │    (存储仓库)            │   │   │
│  │   └─────────────────────────┘   │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

三、环境与依赖

3.1 版本约束

  • JDK:17
  • Spring Boot:3.5.7
  • Spring AI:1.1.3
  • 数据库:MySQL 8.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.5.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springboot-ai-memory</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-ai-memory</name>
    <description/>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.1.3</spring-ai.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

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

        <!-- Redis 缓存(用于AI对话记忆) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>
    <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <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 配置文件 application.yml

spring:
  # MySQL 数据源
  datasource:
    url: jdbc:mysql://localhost:3306/spring_ai?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

  # AI 模型配置
  ai:
    openai:
      api-key: sk-xxxxxxx
      chat:
        options:
          model: gpt-3.5-turbo
          temperature: 0.7

    # JDBC 记忆自动建表(生产环境改为 never)
    chat:
      memory:
        jdbc:
          initialize-schema: always

四、基础实现:内存记忆

4.1 快速开始

使用 MessageWindowChatMemory 实现基于内存的对话记忆:

@Test
void testMemory() {
    // 1. 创建记忆容器
    ChatMemory memory = MessageWindowChatMemory.builder()
            .chatMemoryRepository(new InMemoryChatMemoryRepository())
            .maxMessages(20)
            .build();

    // 2. 构建客户端并添加记忆顾问
    ChatClient client = ChatClient.builder(chatModel)
            .defaultAdvisors(new SimpleLoggerAdvisor(),
                    MessageChatMemoryAdvisor.builder(memory).build())
            .build();

    // 3. 第一轮对话
    String response1 = client.prompt()
            .user("我叫张三,今年28岁")
            .call()
            .content();
    System.out.println("AI: " + response1);
    // 输出:你好张三,很高兴认识你!

    // 4. 第二轮对话(测试记忆)
    String response2 = client.prompt()
            .user("我今年多大了?")
            .call()
            .content();
    System.out.println("AI: " + response2);
    // 输出:你今年28岁。
}

4.2 原理解析

MessageChatMemoryAdvisor 工作流程:

// 伪代码展示Advisor的工作流程
public class MessageChatMemoryAdvisor implements CallAroundAdvisor {
    
    @Override
    public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
        // 1. 从记忆中读取历史消息
        List<Message> history = chatMemory.get(conversationId);
        
        // 2. 将历史消息添加到当前请求
        request.messages().addAll(0, history);
        
        // 3. 调用模型获取响应
        AdvisedResponse response = chain.nextAroundCall(request);
        
        // 4. 将新对话存入记忆
        chatMemory.add(conversationId, request.userMessage());
        chatMemory.add(conversationId, response.response());
        
        return response;
    }
}

五、生产级方案:持久化存储

生产环境必须使用持久化方案,保证重启不丢失、可追溯、可归档
Spring AI 自动建表 SPRING_AI_CHAT_MEMORY,结构如下:

CREATE TABLE SPRING_AI_CHAT_MEMORY (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    conversation_id VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    type VARCHAR(32) NOT NULL, -- USER/ASSISTANT
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_conversation (conversation_id, timestamp DESC)
);

5.1 配置 JDBC 记忆配置类

@Configuration
public class ChatMemoryConfig {

    // 内存记忆
    @Bean("inMemoryChatMemory")
    public ChatMemory inMemoryChatMemory() {
        return MessageWindowChatMemory.builder()
                .maxMessages(10)
                .build();
    }

    // 数据库持久化记忆(主 Bean)
    @Primary
    @Bean("jdbcChatMemory")
    public ChatMemory jdbcChatMemory(JdbcChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(repository)
                .maxMessages(20)
                .build();
    }
}

5.2 抽象聊天业务类

为了避免内存、JDBC 两套实现写重复代码,使用模板方法抽象公共逻辑。

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import java.util.List;

public abstract class AbstractChatService {

    protected final ChatClient chatClient;

    public AbstractChatService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    // 模板方法:统一对话流程
    public final String chat(String conversationId, String message) {
        ChatMemory memory = getChatMemory();
        List<Message> history = memory.get(conversationId);

        // 调用 AI
        String reply = chatClient.prompt()
                .messages(history)
                .user(message)
                .call()
                .content();

        // 保存对话历史
        memory.add(conversationId, new UserMessage(message));
        memory.add(conversationId, new AssistantMessage(reply));

        return reply;
    }

    // 清空会话
    public final void clear(String conversationId) {
        getChatMemory().clear(conversationId);
    }

    // 子类实现:切换存储介质
    protected abstract ChatMemory getChatMemory();
}

5.3 数据库记忆实现

@Service("jdbcChatService")
public class JdbcChatService extends AbstractChatService {

    @Resource(name = "jdbcChatMemory")
    private ChatMemory jdbcChatMemory;

    public JdbcChatService(ChatClient chatClient) {
        super(chatClient);
    }

    @Override
    protected ChatMemory getChatMemory() {
        return jdbcChatMemory;
    }
}

5.4 ConversationId

生产环境不能使用简单字符串,必须满足:

  • 全局唯一
  • 不可预测
  • 与用户绑定
  • 支持多会话
package com.example.ai.util;

import java.util.UUID;

public class ConversationIdUtil {

    private static final String PREFIX = "chat";
    private static final String SEP = ":";

    public static String generate(String userId) {
        String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        return PREFIX + userId + ":" + uuid;
    }

    public static String generateGuest() {
        return PREFIX + SEP + "guest" + SEP + UUID.randomUUID();
    }
}

示例结果:

chat0001:b9d6f25fd0c448a8

5.5 接口层提供 HTTP 服务

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

    @Autowired
    @Qualifier("jdbcChatService")
    private JdbcChatService jdbcChatService;

   // 数据库持久化对话
  @GetMapping("/jdbc")
  public String chatJdbc(
          @RequestParam String conversationId,
          @RequestParam String message) {
      return jdbcChatService.chat(conversationId, message);
  }
    
    // 清空记忆
    @PostMapping("/clear")
    public String clear(@RequestParam String type,
                        @RequestParam String conversationId) {
        if ("jdbc".equals(type)) {
            jdbcChatService.clear(conversationId);
        } else {
            memoryChatService.clear(conversationId);
        }
        return "会话已清空";
    }
}

5.6 测试效果

  1. 启动项目,自动创建记忆表
  2. 请求接口:
POST /ai/chat/jdbc
?userId=001
&message=我叫张三
  1. 继续提问:
&message=我叫什么名字?

AI 会正确回答:你叫张三,并且重启服务后依然有效。


六、生产级方案:Redis持久化存储

6.1 🚀 Redis 缓存存储(高性能方案)

对于高并发场景,Redis 是更好的选择,提供亚毫秒级响应。

Redis Cache Architecture

6.2 为什么选 Redis?

特性 JDBC Redis
读取速度 ~10ms ~1ms
支持过期策略 不支持 支持
分布式共享 支持 支持
内存占用 中等
适用场景 中小规模 高并发/实时

6.3 自定义 RedisChatMemory

Spring AI 官方暂未提供 Redis 实现,我们可以自定义:

package com.example.ai.chatmemory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Component
public class RedisChatMemory implements ChatMemory {

    private final StringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper = new ObjectMapper();

    private static final String KEY_PREFIX = "ai:chat:memory:";
    private static final long EXPIRE_DAYS = 7;
    private static final int MAX_MESSAGES = 20;

    public RedisChatMemory(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // ===================== 读取消息 =====================
    @Override
    public List<Message> get(String conversationId) {
        String key = KEY_PREFIX + conversationId;
        List<String> jsonList = redisTemplate.opsForList().range(key, 0, -1);

        if (jsonList == null || jsonList.isEmpty()) {
            return List.of();
        }

        return jsonList.stream()
                .map(this::deserializeMessage)
                .collect(Collectors.toList());
    }

    // ===================== 新增单条消息 =====================
    @Override
    public void add(String conversationId, Message message) {
        try {
            String key = KEY_PREFIX + conversationId;
            String json = objectMapper.writeValueAsString(message);

            redisTemplate.opsForList().rightPush(key, json);
            redisTemplate.expire(key, EXPIRE_DAYS, TimeUnit.DAYS);
            trimToMaxSize(key);

        } catch (JsonProcessingException e) {
            throw new RuntimeException("消息序列化失败", e);
        }
    }

    // ===================== 批量消息 =====================
    @Override
    public void add(String conversationId, List<Message> messages) {
        messages.forEach(msg -> add(conversationId, msg));
    }

    // ===================== 清空 =====================
    @Override
    public void clear(String conversationId) {
        redisTemplate.delete(KEY_PREFIX + conversationId);
    }

    // ===================== 【核心:手动反序列化】 =====================
    private Message deserializeMessage(String json) {
        try {
            // 先读成 Map,手动判断类型
            Map<String, Object> map = objectMapper.readValue(json, Map.class);
            String type = (String) map.get("messageType");
            String content = (String) map.get("text");

            return switch (MessageType.valueOf(type)) {
                case USER -> new UserMessage(content);
                case ASSISTANT -> new AssistantMessage(content);
                case SYSTEM -> new SystemMessage(content);
                default -> throw new IllegalArgumentException("不支持的消息类型");
            };
        } catch (Exception e) {
            throw new RuntimeException("消息反序列化失败", e);
        }
    }

    // ===================== 限制最大条数 =====================
    private void trimToMaxSize(String key) {
        Long size = redisTemplate.opsForList().size(key);
        if (size != null && size > MAX_MESSAGES) {
            redisTemplate.opsForList().trim(key, size - MAX_MESSAGES, -1);
        }
    }
}

6.4 RedisChatService类

package com.example.ai.service;

import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.stereotype.Service;

@Service("redisChatService")
public class RedisChatService extends AbstractAiChatService {

    @Resource(name = "redisChatMemory")
    private ChatMemory redisChatMemory;

    public RedisChatService(ChatClient chatClient) {
        super(chatClient);
    }

    @Override
    protected ChatMemory getChatMemory() {
        return redisChatMemory;
    }
}

6.5 测试类

@GetMapping("/redis")
public String chatRedis(
        @RequestParam String conversationId,
        @RequestParam String message) {
    return redisChatService.chat(conversationId, message);
}

参考资料:

Logo

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

更多推荐