第9篇:《AI终于记住我是谁了:多轮对话的上下文记忆与压缩》

承上:上一篇我们接入了社区MCP Server,AI能读文件、操作GitHub,工具能力已经拉满了。但每次对话都像在跟一个失忆的人聊天------上一句刚说完我叫张三,下一句它就问"您好,请问怎么称呼?"今天,我们要给AI装上"海马体"。

1. 问题场景:每句话都是"初次见面"

先看看我们现在的对话:

血压上来了吗?明明上一句刚说过,转头就忘。

原因很简单 :每次调用ChatClient,都是一个独立的会话。AI看不到上一轮说了什么,它只能看到你当前这一句话。

后端老鸟的类比:这不就是HTTP的无状态问题吗? HTTP每次请求独立,我们用Session/Cookie来保持状态。AI对话也需要一个"Session"------这就是 ChatMemory(对话记忆)

2. 核心概念:ChatMemory是什么?

Spring AI的 ChatMemory 接口,本质上就是对话历史记录的管理器

less 复制代码
用户:你好AI,我是"第一行代码HW"。 → 存入记忆: [用户消息: "你好AI,我是"第一行代码HW"。"]
AI:你好AI,第一行代码HW。        → 存入记忆: [AI回复: "你好AI,第一行代码HW。"]
用户:我是谁呀?                 → 存入记忆: [用户消息: "我是谁呀?"]
AI:你是"第一行代码HW"。         → 存入记忆: [AI回复: "你是第一行代码HW"]
用户:知道我为么起个名字吗?       → 把以上4条记忆一起发给AI → AI知道上下文了!

每一轮对话的消息都会被保存,下一次调用时,Spring AI自动把历史消息和当前消息拼接在一起发给AI。

Spring AI提供了三种ChatMemory实现,对应不同的存储后端和适用场景:

实现方式 存储位置 持久化 适用场景
InMemoryChatMemory JVM堆内存 ❌ 重启丢失 开发测试、单机演示
JdbcChatMemory 关系型数据库 已有数据库的中小型项目
RedisChatMemory Redis 分布式系统、高并发生产环境

实现方式对比

维度 InMemory JDBC Redis
性能 最高(内存读写) 中(依赖DB性能) 高(内存+持久化)
持久化
分布式支持 ✅(共享DB) ✅(天然支持)
过期管理 手动编码 SQL定时清理 原生TTL支持
运维成本 利用现有DB 需Redis集群
数据量上限 受JVM堆限制 受磁盘限制 受内存+磁盘限制

下面逐一实现,并分析各自的优势和适用场景。

3. 方式一:InMemoryChatMemory(开发首选)

3.1. 核心特点

  • 存储位置ConcurrentHashMap<String, List<Message>>
  • 生命周期:跟随JVM进程,应用重启数据全丢
  • 性能:纯内存读写,微秒级延迟
  • 容量限制:无内置上限,长会话可能OOM

3.1.1. 优势

  1. 零依赖:不需要数据库、Redis,开箱即用
  2. 极速响应:纯内存操作,不产生网络IO
  3. 调试友好:断点直接看内存数据,排查问题方便
  4. 无运维成本:不需要额外中间件

3.1.2. 劣势

  1. 重启丢失:发版、宕机后所有对话记忆消失
  2. 单机限制:无法在多个实例间共享
  3. 内存泄漏风险:没有自动过期机制,需手动清理
  4. 不可扩展:受单机堆内存限制

3.1.3. 适用场景

  • 本地开发和单元测试
  • 演示环境(Demo、POC)
  • 对对话连续性要求不高的单机应用
  • 短期会话(如客服咨询后立即结束)

3.2. 代码实现

kotlin 复制代码
package com.yunxi.ai.config;

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class InMemoryChatMemoryConfig {

    @Bean
    public ChatMemory chatMemory() {
        return MessageWindowChatMemory.builder()
                // 明确使用 InMemoryChatMemoryRepository(不配置也默认是这个)
                .chatMemoryRepository(new InMemoryChatMemoryRepository())
                // 自定义最大消息数,默认为 20[citation:2]
                .maxMessages(30)
                .build();
    }
}

MessageWindowChatMemory 底层是一个 ConcurrentHashMap<会话ID, 消息列表>。重启就丢,但开发阶段够用了。

kotlin 复制代码
package com.yunxi.ai.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder, ChatMemory chatMemory) {
        this.chatClient = builder
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

    public String chat(String message) {
        return chatClient.prompt()
                .system("你是一个专业的助手,可以回答用户的问题")
                .user(message)
                .call()
                .content();
    }
}

核心代码就一行

less 复制代码
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())

MessageChatMemoryAdvisor 做了什么?

  1. 调用前 :从 chatMemory 取出历史消息,拼接到请求里
  2. 调用后 :将本次的用户消息和AI回复存入 chatMemory

Advisor模式是Spring AI的一个设计亮点,类似AOP的环绕通知。它可以在Prompt发给AI之前和收到响应之后做拦截处理。记忆管理就是通过Advisor实现的。

3.3. 测试结果

**下面我们根据日志分析交互流程:

**第一次请求

css 复制代码
request: ChatClientRequest[prompt=Prompt{messages=[  SystemMessage{...你是一个专业的助手...},  UserMessage{content='你好AI,我是"第一行代码HW"'}], ...}
  • 请求中只包含 系统提示词 + 用户消息
  • 没有历史消息(因为是第一轮对话)

第二次请求

css 复制代码
request: ChatClientRequest[prompt=Prompt{messages=[  UserMessage{content='你好AI,我是"第一行代码HW"'},      ← 第一轮的历史  AssistantMessage{textContent=你好!欢迎...},            ← 第一轮的回复  SystemMessage{...你是一个专业的助手...},  UserMessage{content='还知道我是谁吗?'}                  ← 当前提问], ...}
  • 历史消息已被自动注入!包含:
    1. 第一轮用户提问
    2. 第一轮 AI 回复
    3. 系统提示词
    4. 当前用户提问

AI 的回复

arduino 复制代码
当然记得!你是"第一行代码HW"------上一次你说过的名字。

4. 方式二:JdbcChatMemory(企业级持久化)

4.1. 核心特点

  • 存储位置:MySQL/PostgreSQL等关系型数据库
  • 持久化:数据不丢失,应用重启后对话依然可用
  • 事务支持:可利用数据库事务保证一致性
  • 查询能力:可用SQL分析对话历史、用户行为

4.1.1. 优势

  1. 利用现有基础设施:大多数项目已有数据库,零额外成本
  2. 持久化可靠:ACID事务保证,不怕断电宕机
  3. SQL查询灵活:可做对话分析、统计用户提问频率、热点问题挖掘
  4. 备份恢复简单:随数据库一起备份,运维成熟
  5. 权限管理:可复用数据库的权限体系

4.1.2. 劣势

  1. 性能瓶颈:高并发下数据库压力大,需做好索引优化
  2. 存储成本:大量对话历史占用磁盘空间,需定期清理
  3. 延迟较高:相比内存/Redis,磁盘IO延迟高一个数量级
  4. Schema依赖:需提前建表,跨项目复用需统一表结构

4.1.3. 适用场景

  • 已有MySQL/PostgreSQL的中小型项目
  • 需要对话历史审计、分析的场景
  • 不想引入新中间件的保守型架构
  • 用户对话量不大(日活<10万)的系统

4.2. 代码实现

xml 复制代码
        <!-- Spring AI JDBC 聊天记忆持久化 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
        </dependency>
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ai_test?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  ai:
    memory:
      repository:
        jdbc:
          # 表初始化策略:开发环境用 always,生产环境用 never
          initialize-schema: always
sql 复制代码
-- ai_test.SPRING_AI_CHAT_MEMORY definition

CREATE TABLE `SPRING_AI_CHAT_MEMORY` (
  `conversation_id` varchar(36) NOT NULL,
  `content` text NOT NULL,
  `type` varchar(10) NOT NULL,
  `timestamp` timestamp NOT NULL,
  CONSTRAINT `TYPE_CHECK` CHECK ((`type` in (_utf8mb4'USER',_utf8mb4'ASSISTANT',_utf8mb4'SYSTEM',_utf8mb4'TOOL')))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

4.3. 测试结果

5. 方式三:RedisChatMemory(高性能生产方案)

5.1. 核心特点

  • 存储位置:Redis(内存+持久化)
  • 数据结构List<Message>,按conversationId组织
  • 过期机制:原生TTL支持,自动清理
  • 分布式支持:多实例共享,天然适配微服务

5.1.1. 优势

  1. 高性能:内存级读写,微秒级延迟,支撑高并发
  2. 持久化:RDB快照 + AOF日志,兼顾性能和数据安全
  3. 自动过期:原生TTL,无需手动写清理任务
  4. 分布式共享:所有应用实例共享同一份记忆,用户切换节点无感知
  5. 数据结构丰富:可扩展到Sorted Set(按时间排序)、Hash(存储会话元数据)
  6. 运维成熟:哨兵模式、集群模式,高可用方案完善

5.1.2. 劣势

  1. 额外中间件:需要部署和维护Redis
  2. 内存成本:大量对话历史会占用Redis内存,需合理设置TTL
  3. 数据一致性:Redis和DB的数据一致性需额外处理(如果需要双写)
  4. 序列化开销:复杂对象序列化/反序列化有性能损耗

5.1.3. 适用场景

  • 分布式微服务架构
  • 高并发对话系统(日活>10万)
  • 需要多实例共享会话状态
  • 对对话响应速度有极致要求
  • 会话有明确生命周期(如客服对话结束后7天自动清理)

5.2. 代码实现

spring-ai更新很快,我写这篇文章的时间是:2027-07-15,所以大家在学习的时候最好看一下博客的编写时间。

注意:必须是spring AI 2.0.0

Spring AI 2.0.0 要求 Spring Boot 4.0/4.1

这一块我在开发过程中遇到很多小问题,还没整理完!!

6. 什么是 CONVERSATION_ID?

CONVERSATION_ID 是对话记忆的唯一标识符,就像数据库的主键、HTTP Session的SessionId。它决定了哪些消息属于同一个对话上下文。

makefile 复制代码
conversationId = "user_001"
    ├── 用户: "我叫张三"
    ├── AI: "好的张三"
    ├── 用户: "查订单ORD001"
    └── AI: "已发货..."

conversationId = "user_002"
    ├── 用户: "今天天气怎么样"
    └── AI: "北京今天晴,25°C"

不同的 conversationId,完全隔离的记忆空间。

核心代码:

less 复制代码
 .advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, chatId))

7. 本篇小结

这一篇我们深入对比了ChatMemory的三种实现:

实现 优势 劣势 适用场景
InMemory 零依赖、极速 重启丢失 开发测试
Jdbc 利用现有DB、可审计 性能一般 中小项目
Redis 高性能、分布式、自动过期 需额外中间件 生产高并发

现在我们的AI已经是一个"有记忆的打工人"了------能记住上下文、能调工具、能流式输出、能通过MCP接入外部生态。

但它还有一个致命短板:老板问"这个月谁业绩最好",它回答不了。 因为AI根本看不到你数据库里的销售表。

下一篇,我们要解决这个问题------Text-to-SQL,让AI直接写SQL查业务数据库。


本文与DeepSeek协作完成

相关推荐
深蓝AI3 小时前
Grok Build 实战:xAI 开源编码智能体 CLI,原生 MCP 打通工具链
aigc·ai编程
Jackson__3 小时前
AI Agent 的能力从哪里来?一文讲清后训练、上下文学习和外部能力
前端·agent·ai编程
神奇霸王龙4 小时前
Gemini CLI 中转站配置使用教程
人工智能·ai·ai作画·aigc·ai编程·gemini·goolge
KaneLogger5 小时前
花了2天写了个全平台的技能管理工具
aigc·agent·ai编程
atbigapp.com6 小时前
一次踩坑如何变成团队记忆:智忆 Capture → Review → Recall 实战
aigc·ai编程
涛声依旧god6 小时前
如何打造一个 AI Agent 自动写作并一键发布技术文章的自动化系统
人工智能·ai·自动化·ai编程
孤狼GPT7 小时前
ChatGPT、Codex与Pro:AI写代码越快,为什么需求工程反而越重要?
chatgpt·ai编程·codex·chatgpt pro·需求工程
ajassi20009 小时前
AI语音智能体架构解析(四)AI 语音终端(执行器官)
人工智能·ai·架构·ai编程
啦啦啦!9 小时前
基于AI进行GUI自动化测试
功能测试·测试工具·ai编程