乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • Spring AI 学习路径
  • 第1章 Spring AI 概述与核心理念

    • 章节导读
    • Spring AI 概述与可移植 API
    • 核心模块与依赖关系
  • 第2章 快速入门与第一个AI应用

    • 章节导读
    • 环境准备与配置
    • 第一个 AI 对话应用
  • 第3章 聊天模型与ChatClient

    • 章节导读
    • ChatClient 详解
    • ChatModel 底层抽象
    • 多轮对话与 ChatMemory
  • 第4章 提示词管理与模板

    • 章节导读
    • Prompt 与 Message 体系
    • 提示词模板与动态构建
  • 第5章 输出解析与结构化响应

    • 章节导读
    • 输出解析器与 BeanOutputConverter
  • 第6章 嵌入模型与向量存储

    • 章节导读
    • EmbeddingModel 与文本向量化
    • ETL 数据注入流水线
    • 向量存储抽象与配置
  • 第7章 检索增强生成(RAG)

    • 章节导读
    • RAG 核心机制与流程
    • QuestionAnswerAdvisor 详解
    • RAG 实战案例
  • 第8章 函数调用(Function Calling)

    • 章节导读
    • Tool 注解与函数声明
    • 函数调用实战
  • 第9章 多模态

    • 章节导读
    • 图像输入与视觉模型
    • 图像生成
  • 第10章 Advisor拦截器链

    • 章节导读
    • Advisor 链与内置拦截器
  • 第11章 MCP 协议与跨语言工具集成

    • 章节导读
    • MCP 协议深入
  • 第12章 可观测性测试与最佳实践

    • 章节导读
    • 可观测性与指标监控
    • 测试策略与 Mock 实践
    • 最佳实践与面试考点汇总

小崔挠头:“我昨天问课程助手的对话,今天它全忘了。”白歌说:“用 ChatMemory,把对话历史持久化。InMemory 只适合开发,生产环境用 JDBC 或 Cassandra。”

多轮对话与 ChatMemory

定义与作用

ChatMemory 是 Spring AI 中管理对话历史的组件。它将每轮对话的用户消息和 AI 回复存储起来,在后续请求中自动注入到 Prompt,让 AI "记住"上下文。

没有 ChatMemory 时,每次请求都是独立的——AI 不知道上一轮说了什么。有了 ChatMemory,AI 能像真人一样进行连贯的多轮对话。

核心原理:ChatMemoryAdvisor 的消息注入流程

图释:ChatMemoryAdvisor 在每次请求前从 ChatMemory 读取历史消息注入 Prompt,在收到响应后把新消息写入 ChatMemory。整个流程对应用代码透明。

ChatMemory 接口

public interface ChatMemory {
    void add(String conversationId, List<Message> messages);
    List<Message> get(String conversationId, int lastN);
    void clear(String conversationId);
}
  • add:追加消息到指定会话
  • get:获取最近 N 条消息(控制上下文长度)
  • clear:清除会话历史

ChatMemory 实现对比

实现持久化适用场景依赖
InMemoryChatMemory否(内存)开发/测试/演示无
JdbcChatMemory是(数据库)中小规模生产spring-jdbc + 数据库
CassandraChatMemory是(分布式)大规模生产spring-data-cassandra

完整示例一:带 ChatMemory 的课程问答机器人

场景说明

孔蓝要求课程问答支持多轮对话——学生可以先问"离散数学有哪些前置课程",再追问"这两门课分别多少学分"。

关键代码

@Configuration
public class ChatMemoryConfig {

    @Bean
    public ChatMemory chatMemory() {
        // 开发环境用 InMemory,生产环境替换为 JdbcChatMemory
        return new InMemoryChatMemory();
    }

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder,
                                  ChatMemory chatMemory) {
        return builder
                .defaultSystem("""
                    你是飞翔科技大学的课程顾问。你了解所有专业的课程设置。
                    回答时引用具体的课程编号和学分。
                    """)
                .defaultAdvisors(new ChatMemoryAdvisor(chatMemory))
                .build();
    }
}
@RestController
public class CourseAdvisorController {

    private final ChatClient chatClient;

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

    // conversationId 通过请求参数传递,用于区分不同学生的会话
    @GetMapping("/course-advisor")
    public String ask(@RequestParam String question,
                      @RequestParam(defaultValue = "default") String conversationId) {
        return chatClient.prompt()
                .user(question)
                .advisors(advisor -> advisor
                        .param("chat_memory_conversation_id", conversationId))
                .call()
                .content();
    }

    @GetMapping("/course-advisor/clear")
    public String clear(@RequestParam(defaultValue = "default") String conversationId) {
        // 需要注入 ChatMemory 来手动清除
        return "会话 " + conversationId + " 已清除(需注入 ChatMemory 调用 clear)";
    }
}

运行结果

第一轮:GET /course-advisor?question=离散数学有哪些前置课程&conversationId=stu001
→ "离散数学(CS201)的前置课程是《高等数学A》(MATH101,5学分)和
   《线性代数》(MATH102,4学分)。"

第二轮:GET /course-advisor?question=这两门课分别多少学分&conversationId=stu001
→ "根据之前的回答,《高等数学A》(MATH101)5学分,
   《线性代数》(MATH102)4学分。"
   ↑ 不需要重新说"离散数学的前置"

第三轮:GET /course-advisor?question=这两门课分别多少学分&conversationId=stu002
→ "请问你指的是哪两门课程?" 
   ↑ 不同 conversationId,没有历史记录

完整示例二:限制对话历史长度

场景说明

上下文窗口有限,需要只保留最近 N 轮对话。ChatMemoryAdvisor 支持通过 defaultConversationHistoryLimit 控制。

关键代码

@Bean
public ChatClient chatClient(ChatClient.Builder builder,
                              ChatMemory chatMemory) {
    return builder
            .defaultAdvisors(
                new ChatMemoryAdvisor(
                    chatMemory,
                    "default",                           // 默认 conversation ID
                    10                                   // 只保留最近 10 条消息
                )
            )
            .build();
}

易错场景与面试考点

易错场景一:ChatMemory 不持久化导致对话丢失

// ❌ 错误:生产环境使用 InMemoryChatMemory
@Bean
public ChatMemory chatMemory() {
    return new InMemoryChatMemory();
    // 应用重启 → 所有学生的对话历史丢失,体验极差
}

问题分析

InMemoryChatMemory 存储在 JVM 堆内存中,应用重启或节点故障时数据丢失。虽然开发和演示阶段方便,但用户会话数据丢失会影响体验。

// ✅ 正确:生产环境使用 JdbcChatMemory
@Bean
public ChatMemory chatMemory(DataSource dataSource) {
    return new JdbcChatMemory(dataSource);
    // 对话历史持久化到数据库表 chat_memory
}

JdbcChatMemory 需要的表结构(自动创建):

CREATE TABLE IF NOT EXISTS chat_memory (
    conversation_id VARCHAR(100),
    message_index INT,
    message_type VARCHAR(20),
    message_content TEXT,
    PRIMARY KEY (conversation_id, message_index)
);

易错场景二:conversationId 未传导致所有用户共享历史

// ❌ 错误:所有请求共用一个 conversationId
@GetMapping("/ask")
public String ask(@RequestParam String question) {
    return chatClient.prompt()
            .user(question)
            .call()
            .content();
    // 所有学生共享对话历史!张三问"我叫什么"会得到李四的名字
}

问题分析

ChatMemoryAdvisor 默认使用 "default" 作为 conversationId。所有不传 conversationId 的请求共享同一会话。

// ✅ 正确:每个学生用独立 conversationId
@GetMapping("/ask")
public String ask(@RequestParam String question,
                  @RequestParam String studentId) {
    return chatClient.prompt()
            .user(question)
            .advisors(advisor -> advisor
                    .param("chat_memory_conversation_id", studentId))
            .call()
            .content();
}

面试高频题

Q1:ChatMemory 和 ChatMemoryAdvisor 是什么关系?

ChatMemory 是存储组件(存/取消息),ChatMemoryAdvisor 是 Advisor(拦截器),在请求前从 ChatMemory 读取历史注入 Prompt,在响应后将新消息写回。两者配合实现多轮对话的自动上下文管理。

Q2:上下文窗口有限时,ChatMemory 如何避免 Token 超限?

通过 ChatMemoryAdvisor 的 defaultConversationHistoryLimit 参数限制注入的历史消息数量(如只取最近 10 条)。在大型应用中可结合 Token 计数器动态裁剪历史。

Q3:生产环境该如何选择 ChatMemory 实现?

中小规模(单实例):JdbcChatMemory,利用现有数据库。大规模(分布式多实例):CassandraChatMemory,支持水平扩展和高可用。开发/测试:InMemoryChatMemory。

本章小结

  • ChatMemory 是对话历史管理的核心接口,三种实现覆盖开发到生产
  • ChatMemoryAdvisor 在 Advisor 链中自动注入历史消息,对应用代码透明
  • conversationId 用于隔离不同用户的会话,生产环境务必正确传递
  • defaultConversationHistoryLimit 控制上下文长度,防止 Token 超限
  • InMemory 仅适合开发,生产用 JdbcChatMemory 或 CassandraChatMemory
上一页
ChatModel 底层抽象