work hard work smart

专注于AI+Java后端开发。 不断总结,举一反三。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

摘要:多轮对话是大模型应用的核心需求。本文讲解 Spring AI 如何通过 ChatMemory 和 Advisor 机制实现对话短期记忆,并支持 JDBC 持久化。内容包含:短期记忆概念、Advisor 原理、内存/JDBC 两种实现、生产环境配置。

image

一、什么是短期记忆?

短期记忆(Short-term Memory) 是指 AI 在单次对话会话中记住上下文的能力。

为什么需要短期记忆?

大模型本身是无状态的,每次请求都是独立的。如果没有短期记忆:

用户:我想去北京玩
AI:北京有很多景点,推荐故宫、长城...

用户:预算3000够吗?
AI:❓ 什么预算?(不记得前面说的是北京旅游)

有了短期记忆:

用户:我想去北京玩
AI:北京有很多景点,推荐故宫、长城...

用户:预算3000够吗?
AI:✅ 3000元预算去北京玩3-4天是够的...(记得是北京旅游)

短期记忆 vs 长期记忆

类型 作用域 存储内容 生命周期
短期记忆 单次会话 对话历史 会话结束或清空
长期记忆 跨会话 用户偏好、关键信息 永久保存

本文聚焦短期记忆的实现。


二、什么是 Advisor?

Advisor(顾问/拦截器) 是 Spring AI ChatClient 的核心扩展机制,类似于 Spring 的拦截器或 AOP。

Advisor 的作用

Advisor 可以在请求发送前后进行拦截,实现各种增强功能:

graph LR A[用户请求] --> B[Advisor 1] B --> C[Advisor 2] C --> D[ChatModel] D --> E[Advisor 2] E --> F[Advisor 1] F --> G[返回响应]

常用 Advisor

Advisor 功能 场景
MessageChatMemoryAdvisor 自动管理对话历史 多轮对话
SimpleLoggerAdvisor 打印请求/响应日志 开发调试
VectorStoreAdvisor RAG 向量检索 知识库问答

MessageChatMemoryAdvisor 工作原理

graph TD A[用户消息] --> B[MessageChatMemoryAdvisor] B --> C[从 ChatMemory 获取历史] C --> D[合并历史 + 当前消息] D --> E[发送给 ChatModel] E --> F[AI 响应] F --> G[保存到 ChatMemory] G --> H[返回给用户]

关键点

  1. 请求前:自动加载历史对话
  2. 请求后:自动保存新对话
  3. 开发者:无需手动管理消息列表

三、问题场景:无记忆的对话

假设你想让 AI 帮你策划产品发布会:

你:我们下个月要发布一款智能手表
AI:好的,智能手表发布会需要考虑场地、流程、媒体邀请...

你:预算大概50万,场地选在哪里合适?
AI:???(什么预算?)

问题:每次对话 AI 都像失忆了一样!

原因:直接使用 ChatModel 需要手动维护消息列表:

@GetMapping("/planProductLaunch")
public String planProductLaunch(String message) {
    List<Message> messages = new ArrayList<>();
    messages.add(new SystemMessage("你是一个资深产品策划专家"));
    messages.add(new UserMessage("我们下个月要发布一款智能手表"));
    ChatResponse response = dashScopeChatModel.call(new Prompt(messages));
    
    // 第二轮需要手动添加历史
    messages.add(new AssistantMessage(response.getResult().getOutput().getText()));
    messages.add(new UserMessage("预算大概50万,场地选在哪里合适?"));
    // ... 代码冗长,无法跨请求保持上下文
    return dashScopeChatModel.call(new Prompt(messages)).getResult().getOutput().getText();
}

缺点

  • ❌ 需要手动维护 List<Message>
  • ❌ 无法跨请求保持上下文
  • ❌ 代码冗长,容易出错

四、解决方案:使用 ChatMemory + Advisor

4.1 需要的依赖包

<!-- Spring AI 核心 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-spring-boot-starter</artifactId>
</dependency>

<!-- 通义千问模型 -->
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter</artifactId>
</dependency>

<!-- JDBC 持久化(生产环境) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

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

核心类说明

  • org.springframework.ai.chat.client.ChatClient:对话客户端
  • org.springframework.ai.chat.memory.ChatMemory:记忆接口
  • org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor:记忆拦截器
  • org.springframework.ai.chat.model.ChatModel:模型接口

4.2 实现代码

@RestController
@RequestMapping("/memory")
public class ChatMemoryController implements InitializingBean {

    @Autowired
    private ChatModel dashScopeChatModel;
    
    @Autowired
    private ChatMemory chatMemory;
    
    private ChatClient chatClient;
    
    @Override
    public void afterPropertiesSet() throws Exception {
        this.chatClient = ChatClient.builder(dashScopeChatModel)
                .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(chatMemory).build(),
                    new SimpleLoggerAdvisor()
                )
                .defaultOptions(
                    DashScopeChatOptions.builder()
                        .withTopP(0.7)
                        .build()
                )
                .build();
    }
    
    @GetMapping("/continuousChat")
    public Flux<String> continuousChat(String message, String chatId, HttpServletResponse httpServletResponse) {
        httpServletResponse.setCharacterEncoding("UTF-8");
        return chatClient
                .prompt()
                .user(message)
                .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
                .stream()
                .content();
    }
}

关键点

  • MessageChatMemoryAdvisor:自动处理历史消息的加载和保存
  • chatId:会话唯一标识,同一个 chatId 共享历史
  • InitializingBean:确保 ChatClient 在应用启动时创建,全局复用

4.3 测试效果

# 第一轮对话
curl "http://localhost:8080/memory/continuousChat?message=我们下个月要发布一款智能手表&chatId=launch-001"

# 第二轮对话 - AI 记得前面说过发布会
curl "http://localhost:8080/memory/continuousChat?message=预算大概50万,场地选在哪里合适?&chatId=launch-001"

# 第三轮对话 - AI 依然记得上下文
curl "http://localhost:8080/memory/continuousChat?message=希望能邀请科技博主来做现场体验&chatId=launch-001"

效果:AI 现在能够记住整个对话历史,给出连贯的回复!


五、ChatMemory 的两种实现

5.1 内存存储(开发测试)

@Configuration
public class ChatMemoryConfiguration {

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

特点

  • ✅ 配置简单,无需数据库
  • ❌ 应用重启后数据丢失
  • ✅ 适合开发和测试环境

5.2 JDBC 持久化(生产环境)

① 创建数据库表

CREATE TABLE `spring_ai_chat_memory` (
  `conversation_id` varchar(36) NOT NULL,
  `content` text NOT NULL,
  `type` enum('USER','ASSISTANT','SYSTEM','TOOL') NOT NULL,
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`,`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

② 配置文件

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_ai?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  ai:
    chat:
      memory:
        repository:
          jdbc:
            enabled: true
            platform: mysql
            initialize-schema: always

③ 配置类

@Configuration
public class JdbcChatMemoryConfiguration {

    @Bean
    public ChatMemory jdbcChatMemory(JdbcChatMemoryRepository jdbcChatMemoryRepository) {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(jdbcChatMemoryRepository)
                .maxMessages(20)
                .build();
    }
}

④ 控制器实现

@RestController
@RequestMapping("/jdbc/memory")
public class JdbcChatMemoryController implements InitializingBean {

    @Autowired
    private ChatModel dashScopeChatModel;

    @Autowired
    private ChatMemory jdbcChatMemory;

    private ChatClient chatClient;

    @GetMapping("/persistentChat")
    public Flux<String> persistentChat(String message, String chatId, HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        return chatClient
                .prompt()
                .user(message)
                .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
                .stream()
                .content();
    }

    @Override
    public void afterPropertiesSet() {
        this.chatClient = ChatClient.builder(dashScopeChatModel)
                .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(jdbcChatMemory).build(), 
                    new SimpleLoggerAdvisor()
                )
                .defaultOptions(DashScopeChatOptions.builder().withTopP(0.7).build())
                .build();
    }
}

关键差异:注入的是 jdbcChatMemory Bean,其他代码与内存版本完全一致!

⑤ 测试持久化效果

# 第一次对话
curl "http://localhost:8000/jdbc/memory/persistentChat?message=我们要发布一款智能手表&chatId=product-001"

# 第二次对话(同一会话)
curl "http://localhost:8000/jdbc/memory/persistentChat?message=预算50万,场地怎么选?&chatId=product-001"

# 重启应用后继续对话
curl "http://localhost:8000/jdbc/memory/persistentChat?message=希望能邀请科技博主&chatId=product-001"

效果:AI 依然记得之前的对话,因为数据已持久化到数据库!

5.3 两种实现对比

特性 内存存储 JDBC 存储
持久化 ❌ 重启丢失 ✅ 永久保存
性能 ⚡ 极快 ⚡ 快(有索引)
配置 简单 需要数据库
适用场景 开发测试 生产环境

六、最佳实践

6.1 合理设置消息窗口大小

MessageWindowChatMemory.builder()
    .maxMessages(20)
    .build();

建议:一般场景 10-20 条,长对话 30-50 条,注意模型的 token 限制。

6.2 数据库优化(JDBC)

连接池配置

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000

定期清理过期数据

DELETE FROM spring_ai_chat_memory 
WHERE timestamp < DATE_SUB(NOW(), INTERVAL 30 DAY);

6.3 会话 ID 管理

@GetMapping("/initiateSession")
public String initiateSession() {
    return UUID.randomUUID().toString();
}

七、常见问题

Q1:ChatMemory 会无限增长吗?

不会。使用 MessageWindowChatMemory 可以限制消息数量,超过限制后最早的消息会被自动删除。

Q2:如何区分不同用户的对话?

通过不同的 chatId

chatClient.prompt()
    .user(message)
    .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, "user-A"))
    .stream();

Q3:应用重启后记忆会丢失吗?

  • 内存存储:❌ 会丢失
  • JDBC 存储:✅ 不会丢失(持久化到数据库)

Q4:如何清除对话记忆?

chatMemory.clear(chatId);

八、总结

Spring AI 的 ChatMemory + Advisor 机制为多轮对话提供了优雅的解决方案:

核心价值

  1. 自动化:无需手动管理消息列表
  2. 隔离性:不同会话通过 chatId 隔离
  3. 可控性:支持消息数量限制、持久化存储
  4. 透明性:切换存储实现时,业务代码无需修改

快速上手

// 1. 注入 ChatMemory
@Autowired
private ChatMemory chatMemory;

// 2. 配置 ChatClient
chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
    .build();

// 3. 调用时传入 chatId
chatClient.prompt()
    .user(message)
    .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
    .stream()
    .content();

通过 ChatMemory,我们可以轻松实现有"记忆力"的 AI 对话,让交互更加自然流畅!


参考资源