Spring AI 1.x 系列【27】Chat Memory API:让 LLM 拥有上下文记忆能力

文章目录

  • [1. 基础知识](#1. 基础知识)
    • [1.1 人脑记忆](#1.1 人脑记忆)
    • [1.2 大模型没有记忆](#1.2 大模型没有记忆)
    • [1.3 记忆工程](#1.3 记忆工程)
    • [1.4 模型上下文](#1.4 模型上下文)
    • [1.5 上下文窗口](#1.5 上下文窗口)
  • [2. Spring AI Chat Memory](#2. Spring AI Chat Memory)
    • [2.1 ChatMemory](#2.1 ChatMemory)
    • [2.2 MessageWindowChatMemory](#2.2 MessageWindowChatMemory)
    • [2.3 ChatMemoryRepository](#2.3 ChatMemoryRepository)
      • [2.3.1 InMemoryChatMemoryRepository](#2.3.1 InMemoryChatMemoryRepository)
      • [2.3.2 外部数据库存储实现](#2.3.2 外部数据库存储实现)
  • [3. ChatMemoryAdvisor](#3. ChatMemoryAdvisor)
    • [3.1 BaseChatMemoryAdvisor](#3.1 BaseChatMemoryAdvisor)
    • [3.1 MessageChatMemoryAdvisor](#3.1 MessageChatMemoryAdvisor)
    • [3.2 PromptChatMemoryAdvisor](#3.2 PromptChatMemoryAdvisor)

1. 基础知识

1.1 人脑记忆

认知心理学视角对记忆的经典定义,将记忆看作大脑对信息的加工过程,核心分为三步:

  • 编码:接收并转换外界信息,形成大脑可处理的形式
  • 储存:将编码后的信息长期保留
  • 提取:需要时把储存的信息调取、再现

按保存时间划分的记忆类型:

  • 瞬时记忆:刺激停止后,信息在感觉通道内的短暂保留,内容经注意后,可进入短时记忆。存放在对应的初级感觉皮层,枕叶(视觉皮层)、颞叶(听觉皮层)、顶叶(躯体感觉皮层)。
  • 短时记忆 :保持时间约 1 分钟内的记忆,无复述时,18 秒后回忆正确率大幅下降,约 1 分钟内衰退消失,经复述可进入长时记忆,存放在前额叶皮层(PFC)。
  • 长时记忆:由 海马体 把短时记忆整理、固化成长时记忆,分散存放在大脑皮层各处。

1.2 大模型没有记忆

大模型的底层是 Transformer 架构,在设计上是‌无状态(Stateless)‌的,每次推理仅基于当前输入生成响应,不保留历史对话或用户上下文。

示例,先告诉大模型你是谁?再次发起提问:

java 复制代码
        String content1 = zhiPuAiChatClient.prompt("我叫张三,今年30岁,养了一只叫"可乐"的柯基犬,家住上海。")
                .call()
                .content();
        String content2 = zhiPuAiChatClient.prompt("你知道我叫什么名字吗?")
                .call()
                .content();
        System.out.println(content1);
        System.out.println(content2);

从输出结果可以看出,上一句才告诉它你是谁,下一次对话它就不记得了:

1.3 记忆工程

为了解决大模型原生 "无状态、记不住事 " 的缺陷,可以在模型外部搭建一套完整的记忆管理系统,通过工程手段模拟人类记忆的逻辑,让 AI 能记得过去的对话、知识和偏好。

工程层面的记忆机制有:

  • 全量记忆:每轮对话都完整保留,全部发送给模型信息最完整,但极易触发上下文长度限制,成本高,仅适合极短对话。
  • 滑动窗口记忆 :只保留最近 N 轮对话,更早的内容自动丢弃实现简单、开销小,像 "滑动窗口" 一样只关注近期内容,适合日常短对话。
  • 相关性过滤记忆:给每条对话按重要性 / 相关性打分,只保留高分内容,清除低分记忆能筛选关键信息,减少冗余,需要额外的重要性评估逻辑。
  • 摘要 / 压缩记忆:将旧对话内容自动生成摘要,用摘要替代原始文本送入模型。

人类认知层面的记忆机制:

  • 向量数据库记忆:把对话内容转成向量(语义嵌入)存入向量库,需要时按语义检索相关历史适合超长时间对话,只加载和当前话题相关的历史,避免上下文溢出。
  • 知识图谱记忆 :将对话中的实体、属性、关系结构化成语谱,后续可做图上检索和推理更接近人类 "知识组织" 方式,适合需要逻辑关联、知识沉淀的场景。
  • 分层记忆:同时维护「短期滑动窗口」和「长期语义记忆」,关键内容自动 提升到长期存储结合短期即时记忆和长期知识记忆,平衡时效性和持久性。
  • 模拟 OSSwap记忆管理:类比计算机内存,将旧对话存放到外部存储,必要时再载入上下文,更偏向工程化的内存交换思路,最大化利用有限上下文窗口。

1.4 模型上下文

模型上下文(Model Context)是大模型在一次推理计算中,能看到、能利用的全部输入文本信息。

简单来说,给模型的所有输入(当前提问 + 历史对话 + 任务指令 + 参考文档等),拼在一起就是模型上下文,模型只能基于这些内容生成回复。

通常包含几类信息:

  • 当前用户输入:你这一轮说的话、提的问题。
  • 历史对话片段:之前几轮的用户提问 + 模型回复。
  • 任务 / 系统指令:提前告诉模型的角色、规则。
  • 外部参考信息:通过 RAG / 记忆系统检索到的文档、知识片段。

1.5 上下文窗口

上下文窗口(Context Window)是指模型在一次交互中能够处理的总 Token 数量,它包括了输入文本和生成文本的所有 Token。上下文窗口决定了模型能记住多少历史信息。如果输入和期望输出的总长度超过了模型的上下文窗口,模型将无法处理。

目前,主流大模型的上下文窗口已从早期的几千 Token 扩展到数十万甚至数百万Token,使其能够处理整本书、长篇报告或极长的对话。

例如,qwen3.5-plus 模型:

  • 模型的总上下文窗口上限为 1 M
  • 普通模式下,你能发给模型的输入文本上限约 99.1token
  • 普通模式下,模型一次能生成的回答文本上限,约 6.4 token

2. Spring AI Chat Memory

Spring AI 提供了对话记忆(Chat Memory)功能,支持你在与大型语言模型的多次交互中存储和检索信息。

对话记忆与对话历史的区别:

  • 对话记忆:大型语言模型在整段对话中,为维持上下文感知而保留并使用的信息。
  • 对话历史:完整的对话记录,包含用户与模型之间交互的所有消息。

Spring AI 采用 "抽象层 + 实现层" 的设计,解耦业务逻辑与存储细节,核心接口有两个:

  • ChatMemory:顶层抽象接口,定义了对话记忆的核心操作(添加消息、查询消息、清空消息),负责管理记忆策略(比如保留哪些消息、删除哪些消息)。
  • ChatMemoryRepository:底层存储抽象接口,负责消息的持久化 / 读取,屏蔽具体存储介质(内存、数据库等)的差异。

2.1 ChatMemory

ChatMemorySpring AI 定义的对话记忆核心接口,定义了 AI 对话记忆存储的统一规范,是实现多轮对话、上下文记忆的核心契约。

基础能力:

  • 会话隔离 :支持通过 conversationId 区分不同用户 / 场景的对话,实现多会话隔离管理
  • 消息存储:支持单条 / 批量添加对话消息,内置参数非空校验
  • 消息读取:查询指定会话的完整历史消息
  • 记忆清空:主动清空指定会话的对话历史,释放资源

定义了两个常量:

  • DEFAULT_CONVERSATION_ID:默认会话ID(单用户/简单场景用)。
  • CONVERSATION_ID:会话ID的统一标识key(用于上下文/元数据中标记会话ID)。

定义了多个方法:

  • add:给「指定会话 ID」添加单条、多条对话消息
  • get:根据会话 ID,获取该会话下的所有记忆消息
  • clear:清空指定会话 ID 的所有记忆消息

接口源码:

java 复制代码
public interface ChatMemory {

	/**
	 * 默认会话ID
	 * 当未自定义会话标识时,使用该默认ID存储对话历史
	 */
	String DEFAULT_CONVERSATION_ID = "default";

	/**
	 * 从上下文中获取对话记忆会话ID的固定键名
	 * 用于在上下文、请求参数中标识会话ID
	 */
	String CONVERSATION_ID = "chat_memory_conversation_id";

	/**
	 * 向指定会话中添加单条对话消息
	 * @param conversationId 会话唯一标识,不能为空
	 * @param message 待存储的对话消息,不能为空
	 */
	default void add(String conversationId, Message message) {
		Assert.hasText(conversationId, "conversationId cannot be null or empty");
		Assert.notNull(message, "message cannot be null");
		this.add(conversationId, List.of(message));
	}

	/**
	 * 向指定会话中批量添加多条对话消息
	 * @param conversationId 会话唯一标识,不能为空
	 * @param messages 待批量存储的对话消息集合
	 */
	void add(String conversationId, List<Message> messages);

	/**
	 * 获取指定会话的全部对话历史消息
	 * @param conversationId 会话唯一标识
	 * @return 该会话下按顺序存储的消息列表,无消息时返回空集合
	 */
	List<Message> get(String conversationId);

	/**
	 * 清空指定会话的所有对话历史消息
	 * @param conversationId 待清空的会话唯一标识
	 */
	void clear(String conversationId);

}

2.2 MessageWindowChatMemory

MessageWindowChatMemoryChatMemory 接口的具体实现类,核心作用是实现「滑动窗口记忆策略」,即只保留最近 N 条对话消息(超出则自动删除最早的),是短期记忆的典型实现。

核心属性:

  • DEFAULT_MAX_MESSAGES:默认的窗口大小,默认只保留最近 20 条消息,超过则自动删除最早的
  • ChatMemoryRepository:实际的消息存储

源码如下:

java 复制代码
/**
 * 基于消息窗口的聊天记忆实现类
 * 维护固定大小的对话消息窗口,当消息数量超过上限时自动淘汰旧消息
 * <p>
 * 特殊处理系统消息(SystemMessage):
 * 1. 新增系统消息时,会删除内存中所有旧的系统消息
 * 2. 消息裁剪时,优先保留系统消息,淘汰其他类型消息
 *
 * @author Thomas Vitale
 * @author Ilayaperumal Gopinathan
 * @since 1.0.0
 */
public final class MessageWindowChatMemory implements ChatMemory {

	/**
	 * 默认最大消息数量:20条
	 */
	private static final int DEFAULT_MAX_MESSAGES = 20;

	/**
	 * 聊天记忆存储仓库(负责消息的持久化/内存存储)
	 */
	private final ChatMemoryRepository chatMemoryRepository;

	/**
	 * 消息窗口最大容量
	 */
	private final int maxMessages;

	/**
	 * 私有构造方法
	 * @param chatMemoryRepository 消息存储仓库
	 * @param maxMessages 消息最大数量
	 */
	private MessageWindowChatMemory(ChatMemoryRepository chatMemoryRepository, int maxMessages) {
		Assert.notNull(chatMemoryRepository, "chatMemoryRepository cannot be null");
		Assert.isTrue(maxMessages > 0, "maxMessages must be greater than 0");
		this.chatMemoryRepository = chatMemoryRepository;
		this.maxMessages = maxMessages;
	}

	/**
	 * 向指定会话批量添加消息
	 * 会自动处理系统消息、裁剪消息窗口
	 * @param conversationId 会话ID
	 * @param messages 待添加的消息列表
	 */
	@Override
	public void add(String conversationId, List<Message> messages) {
		Assert.hasText(conversationId, "conversationId cannot be null or empty");
		Assert.notNull(messages, "messages cannot be null");
		Assert.noNullElements(messages, "messages cannot contain null elements");

		// 获取会话中已存储的历史消息
		List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId);
		// 处理消息(系统消息去重 + 窗口裁剪)
		List<Message> processedMessages = process(memoryMessages, messages);
		// 保存处理后的消息
		this.chatMemoryRepository.saveAll(conversationId, processedMessages);
	}

	/**
	 * 获取指定会话的所有聊天消息
	 * @param conversationId 会话ID
	 * @return 消息列表
	 */
	@Override
	public List<Message> get(String conversationId) {
		Assert.hasText(conversationId, "conversationId cannot be null or empty");
		return this.chatMemoryRepository.findByConversationId(conversationId);
	}

	/**
	 * 清空指定会话的所有消息
	 * @param conversationId 会话ID
	 */
	@Override
	public void clear(String conversationId) {
		Assert.hasText(conversationId, "conversationId cannot be null or empty");
		this.chatMemoryRepository.deleteByConversationId(conversationId);
	}

	/**
	 * 核心消息处理逻辑
	 * 1. 处理系统消息:新增系统消息时,删除旧系统消息
	 * 2. 消息裁剪:超过最大数量时,优先保留系统消息,淘汰旧消息
	 * @param memoryMessages 历史消息
	 * @param newMessages 新消息
	 * @return 处理后的最终消息列表
	 */
	private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) {
		List<Message> processedMessages = new ArrayList<>();

		Set<Message> memoryMessagesSet = new HashSet<>(memoryMessages);
		// 判断是否新增了新的系统消息
		boolean hasNewSystemMessage = newMessages.stream()
			.filter(SystemMessage.class::isInstance)
			.anyMatch(message -> !memoryMessagesSet.contains(message));

		// 保留历史消息:如果有新系统消息,则移除历史中的所有系统消息
		memoryMessages.stream()
			.filter(message -> !(hasNewSystemMessage && message instanceof SystemMessage))
			.forEach(processedMessages::add);

		// 添加新消息
		processedMessages.addAll(newMessages);

		// 未超过最大容量,直接返回
		if (processedMessages.size() <= this.maxMessages) {
			return processedMessages;
		}

		// 计算需要淘汰的消息数量
		int messagesToRemove = processedMessages.size() - this.maxMessages;

		List<Message> trimmedMessages = new ArrayList<>();
		int removed = 0;
		// 裁剪消息:优先保留系统消息,淘汰普通消息
		for (Message message : processedMessages) {
			if (message instanceof SystemMessage || removed >= messagesToRemove) {
				trimmedMessages.add(message);
			}
			else {
				removed++;
			}
		}

		return trimmedMessages;
	}

	/**
	 * 获取构建器对象
	 * @return Builder
	 */
	public static Builder builder() {
		return new Builder();
	}

	/**
	 * 建造者模式:构建 MessageWindowChatMemory 实例
	 */
	public static final class Builder {

		private ChatMemoryRepository chatMemoryRepository;

		private int maxMessages = DEFAULT_MAX_MESSAGES;

		private Builder() {
		}

		/**
		 * 设置自定义消息存储仓库
		 */
		public Builder chatMemoryRepository(ChatMemoryRepository chatMemoryRepository) {
			this.chatMemoryRepository = chatMemoryRepository;
			return this;
		}

		/**
		 * 设置消息窗口最大容量
		 */
		public Builder maxMessages(int maxMessages) {
			this.maxMessages = maxMessages;
			return this;
		}

		/**
		 * 构建实例:未指定仓库时,默认使用内存存储
		 */
		public MessageWindowChatMemory build() {
			if (this.chatMemoryRepository == null) {
				this.chatMemoryRepository = new InMemoryChatMemoryRepository();
			}
			return new MessageWindowChatMemory(this.chatMemoryRepository, this.maxMessages);
		}

	}

}

2.3 ChatMemoryRepository

ChatMemoryRepository 是对话记忆底层存储接口,负责聊天消息的持久化/内存存储操作,是聊天记忆模块的底层存储抽象层,解耦聊天记忆业务逻辑与具体存储实现(内存、Redis、数据库等)。

定义的相关方法:

  • findConversationIds:查询所有会话 ID
  • findByConversationId:按会话 ID 查询消息
  • saveAll:批量保存消息
  • deleteByConversationId:按会话 ID 删除消息

源码如下:

java 复制代码
public interface ChatMemoryRepository {

	/**
	 * 查询所有已存在的会话ID
	 * @return 所有会话的唯一标识列表
	 */
	List<String> findConversationIds();

	/**
	 * 根据会话ID查询对应的全部聊天消息
	 * @param conversationId 会话唯一标识
	 * @return 该会话下的消息列表,无消息时返回空集合
	 */
	List<Message> findByConversationId(String conversationId);

	/**
	 * 覆盖式保存指定会话的所有消息
	 * 会删除该会话下原有的全部消息,替换为新的消息列表
	 * @param conversationId 会话唯一标识
	 * @param messages 待保存的新消息列表
	 */
	void saveAll(String conversationId, List<Message> messages);

	/**
	 * 根据会话ID删除该会话的所有聊天消息
	 * @param conversationId 待删除的会话唯一标识
	 */
	void deleteByConversationId(String conversationId);

}

2.3.1 InMemoryChatMemoryRepository

ChatMemoryRepository 接口的内存版具体实现类,所有会话消息存在 JVM 内存的 ConcurrentHashMap 中,适合测试、开发环境或简单的单实例短会话场景。

源码如下:

java 复制代码
public final class InMemoryChatMemoryRepository implements ChatMemoryRepository {
    Map<String, List<Message>> chatMemoryStore = new ConcurrentHashMap();

    public InMemoryChatMemoryRepository() {
    }

2.3.2 外部数据库存储实现

Spring AI 也提供了多种外部数据库存储实现:

  • JdbcChatMemoryRepository:使用 JDBC 将消息存储在关系数据库中。它开箱即用支持多个数据库,适合需要持续存储聊天内存的应用。
  • CassandraChatMemoryRepository:使用 Apache Cassandra 来存储消息。它适合需要持续存储聊天内存的应用,尤其是在可用性、耐用性、扩展性以及利用寿命时间(TTL)功能时。
  • Neo4jChatMemoryRepository:利用 Neo4j 将聊天消息作为节点和关系存储在属性图数据库中。它适合希望利用 Neo4j 图功能实现聊天内存持久化的应用程序。
  • CosmosDBChatMemoryRepository:使用 Azure Cosmos DB NoSQL API 来存储消息。它适用于需要全球分布式、高度可扩展的文档数据库以实现聊天内存持久化的应用。仓库使用对话 ID 作为分区键,以确保数据的高效分发和快速检索。
  • MongoDBChatMemoryRepository:使用 MongoDB 来存储消息。它适用于需要灵活、面向文档的数据库以实现聊天内存持久化的应用。

在使用前,需要引入对应的依赖。比如 JDBC 存储:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>

3. ChatMemoryAdvisor

Spring AI 提供了若干内置的增强器(Advisor),你可根据自身需求,通过这些增强器配置 ChatClient 的记忆行为。

注意:当前版本中,执行工具调用时与大型语言模型交互产生的中间消息不会被存储到记忆中。这是当前实现的一处限制,后续版本会对此问题进行优化。

3.1 BaseChatMemoryAdvisor

所有聊天记忆增强器的基础接口,它继承自 BaseAdvisor(所有增强器的根接口),仅提供一个默认方法,用于统一获取对话 ID

java 复制代码
public interface BaseChatMemoryAdvisor extends BaseAdvisor {

 default String getConversationId(Map<String, Object> context, String defaultConversationId) {
    // 1. 参数合法性校验
    Assert.notNull(context, "context cannot be null");
    Assert.noNullElements(context.keySet().toArray(), "context cannot contain null keys");
    Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
    
    // 2. 优先从上下文获取对话ID,无则使用默认值
    return context.containsKey(ChatMemory.CONVERSATION_ID) 
            ? context.get(ChatMemory.CONVERSATION_ID).toString()
            : defaultConversationId;
}
}

Spring AI 提供了多种实现:

  • MessageChatMemoryAdvisor
  • PromptChatMemoryAdvisor
  • VectorStoreChatMemoryAdvisor

3.1 MessageChatMemoryAdvisor

该增强器基于传入的 ChatMemory 实现类管理对话记忆。每次交互时,它会从记忆中检索对话历史,并将其以消息集合的形式加载到提示词中。

所有变量都是 private final,对象创建后不可修改,保证线程安全:

java 复制代码
private final ChatMemory chatMemory;      // 对话记忆存储接口(内存/Redis/数据库等)
private final String defaultConversationId; // 默认对话ID
private final int order;                 // 顾问执行优先级(数字越小越先执行)
private final Scheduler scheduler;       // Reactor 调度器(处理流式异步请求)

构造器私有化,禁止直接 new,必须通过建造者模式创建:

java 复制代码
private MessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int order, Scheduler scheduler) {
    // 强制非空校验,杜绝空指针
    Assert.notNull(chatMemory, "chatMemory cannot be null");
    Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
    Assert.notNull(scheduler, "scheduler cannot be null");
    // 初始化变量
    this.chatMemory = chatMemory;
    this.defaultConversationId = defaultConversationId;
    this.order = order;
    this.scheduler = scheduler;
}

before() 前置处理会在用户消息发送给 AI 之前执行,是实现「加载历史对话」的核心方法:

java 复制代码
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
    // 1. 获取对话ID(复用父接口方法:优先上下文,兜底默认值)
    String conversationId = getConversationId(chatClientRequest.context(), this.defaultConversationId);

    // 2. 从ChatMemory中读取当前对话的历史消息
    List<Message> memoryMessages = this.chatMemory.get(conversationId);

    // 3. 合并:历史消息 + 当前用户输入的消息
    List<Message> processedMessages = new ArrayList<>(memoryMessages);
    processedMessages.addAll(chatClientRequest.prompt().getInstructions());

    // 4. 关键规则:系统消息必须放在最前面(LLM强制要求)
    for (int i = 0; i < processedMessages.size(); i++) {
        if (processedMessages.get(i) instanceof SystemMessage) {
            Message systemMessage = processedMessages.remove(i);
            processedMessages.add(0, systemMessage);
            break;
        }
    }

    // 5. 构建新请求:替换为「带历史记忆」的消息列表
    ChatClientRequest processedChatClientRequest = chatClientRequest.mutate()
        .prompt(chatClientRequest.prompt().mutate().messages(processedMessages).build())
        .build();

    // 6. 保存当前用户消息到记忆(为下一轮对话准备)
    Message userMessage = processedChatClientRequest.prompt().getLastUserOrToolResponseMessage();
    this.chatMemory.add(conversationId, userMessage);

    return processedChatClientRequest;
}

after() 后置处理会在 AI 返回响应后执行,是实现「保存 AI 回复」的核心方法:

java 复制代码
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
    List<Message> assistantMessages = new ArrayList<>();
    // 提取AI返回的助手消息
    if (chatClientResponse.chatResponse() != null) {
        assistantMessages = chatClientResponse.chatResponse()
            .getResults()
            .stream()
            .map(g -> (Message) g.getOutput())
            .toList();
    }
    // 保存AI回复到对话记忆
    this.chatMemory.add(this.getConversationId(chatClientResponse.context(), this.defaultConversationId),
            assistantMessages);
    return chatClientResponse;
}

adviseStream() 用于支持流式响应时的聚合流式分片:

java 复制代码
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
    Scheduler scheduler = this.getScheduler();

    return Mono.just(chatClientRequest)
        .publishOn(scheduler)  // 异步调度
        .map(request -> this.before(request, streamAdvisorChain)) // 执行前置逻辑
        .flatMapMany(streamAdvisorChain::nextStream) // 执行流式请求
        // 聚合流式分片 → 完整响应 → 执行after保存记忆
        .transform(flux -> new ChatClientMessageAggregator().aggregateChatClientResponse(flux,
                response -> this.after(response, streamAdvisorChain)));
}

Spring AI 推荐的优雅创建方式,必填项 + 默认值,使用极简:

java 复制代码
public static Builder builder(ChatMemory chatMemory) {
    return new Builder(chatMemory);
}

public static final class Builder {
    // 默认值:框架内置常量
    private String conversationId = ChatMemory.DEFAULT_CONVERSATION_ID;
    private int order = Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER;
    private Scheduler scheduler = BaseAdvisor.DEFAULT_SCHEDULER;

    // 链式配置
    public Builder conversationId(String conversationId) {...}
    public Builder order(int order) {...}
    public Builder scheduler(Scheduler scheduler) {...}

    // 构建实例
    public MessageChatMemoryAdvisor build() {...}
}

3.2 PromptChatMemoryAdvisor

该增强器基于传入的 ChatMemory 实现类管理对话记忆。每次交互时,它会从记忆中检索对话历史,并将其以纯文本形式追加到系统提示词中。

相比 MessageChatMemoryAdvisor,新增了提示词模板变量:

java 复制代码
private final PromptTemplate systemPromptTemplate; // 自定义系统提示词模板
private final String defaultConversationId;        // 默认对话ID
private final int order;                          // 执行优先级
private final Scheduler scheduler;                 // 响应式调度器
private final ChatMemory chatMemory;               // 对话存储接口

类中定义了默认的系统提示词模板,是实现记忆嵌入的核心:

java 复制代码
private static final PromptTemplate DEFAULT_SYSTEM_PROMPT_TEMPLATE = new PromptTemplate("""
        {instructions}

        Use the conversation memory from the MEMORY section to provide accurate answers.

        ---------------------
        MEMORY:
        {memory}
        ---------------------

        """);

其中:

  • {instructions}:原系统提示词指令
  • {memory}:格式化后的对话历史

before() 前置处理不再拼接消息,而是改造系统提示词:

java 复制代码
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
    // 1. 获取对话ID(复用父接口)
    String conversationId = getConversationId(chatClientRequest.context(), this.defaultConversationId);
    // 2. 读取历史对话
    List<Message> memoryMessages = this.chatMemory.get(conversationId);

    // 3. 格式化历史:只保留用户/助手消息,转为「类型:内容」字符串
    String memory = memoryMessages.stream()
        .filter(m -> m.getMessageType() == MessageType.USER || m.getMessageType() == MessageType.ASSISTANT)
        .map(m -> m.getMessageType() + ":" + m.getText())
        .collect(Collectors.joining(System.lineSeparator()));

    // 4. 渲染提示词:原指令 + 记忆文本
    SystemMessage systemMessage = chatClientRequest.prompt().getSystemMessage();
    String augmentedSystemText = this.systemPromptTemplate
        .render(Map.of("instructions", systemMessage.getText(), "memory", memory));

    // 5. 构建新请求:替换为「带记忆的系统提示词」
    ChatClientRequest processedChatClientRequest = chatClientRequest.mutate()
        .prompt(chatClientRequest.prompt().augmentSystemMessage(augmentedSystemText))
        .build();

    // 6. 保存当前用户消息到记忆
    Message userMessage = processedChatClientRequest.prompt().getLastUserOrToolResponseMessage();
    this.chatMemory.add(conversationId, userMessage);

    return processedChatClientRequest;
}

after() 后置处理和 MessageChatMemoryAdvisor 一致,但空安全更完善、调试日志更详细:

java 复制代码
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
    // Optional 链式空安全处理,提取AI助手消息
    List<Message> assistantMessages = Optional.ofNullable(chatClientResponse)
        .map(ChatClientResponse::chatResponse)
        .filter(response -> response.getResults() != null && !response.getResults().isEmpty())
        .map(response -> response.getResults()
            .stream()
            .map(g -> (Message) g.getOutput())
            .collect(Collectors.toList()))
        .orElse(List.of());

    // 非空则保存记忆,并打印调试日志
    if (!assistantMessages.isEmpty()) {
        this.chatMemory.add(getConversationId(chatClientResponse.context(), defaultConversationId), assistantMessages);
        logger.debug("保存AI响应到记忆");
    }
    return chatClientResponse;
}

adviseStream()MessageChatMemoryAdvisor 代码完全一致,保证同步 / 流式双模式兼容:

java 复制代码
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
    return Mono.just(chatClientRequest)
        .publishOn(getScheduler())
        .map(request -> before(request, streamAdvisorChain))
        .flatMapMany(streamAdvisorChain::nextStream)
        .transform(flux -> new ChatClientMessageAggregator().aggregateChatClientResponse(flux, this::after));
}

Builder 方法中,支持开发者自定义记忆嵌入的提示词格式:

java 复制代码
public static final class Builder {
    private PromptTemplate systemPromptTemplate = DEFAULT_SYSTEM_PROMPT_TEMPLATE; // 可覆盖
    private String conversationId = ChatMemory.DEFAULT_CONVERSATION_ID;
    // ... 其他默认值

    // 核心扩展方法:自定义系统提示词模板
    public Builder systemPromptTemplate(PromptTemplate systemPromptTemplate) {
        this.systemPromptTemplate = systemPromptTemplate;
        return this;
    }
}
相关推荐
kimi-2222 小时前
如何让大语言模型稳定输出 JSON 的三层防御体系
人工智能·语言模型·json
渔民小镇2 小时前
一次编写到处对接 —— 为 Godot/Unity/React 生成统一交互接口
java·分布式·游戏·unity·godot
weixin_156241575762 小时前
基于YOLO深度学习的运动品牌检测与识别系统
人工智能·深度学习·yolo·识别·模型、
路ZP2 小时前
放大镜下拉框
java·数据库·sql
兴趣使然黄小黄2 小时前
【AI-agent】Claude code+Minimax 2.7环境搭建
人工智能·ai编程
物联网软硬件开发-轨物科技2 小时前
【行业动态】AI发展历程通俗速览
人工智能
实在智能RPA2 小时前
Agent 在招投标场景能解决哪些问题?——2026年招投标数智化转型深度解析
人工智能·ai
愈努力俞幸运2 小时前
docker入门,容器,镜像
java·分布式·docker
摇曳的精灵2 小时前
Spring boot注解实现信息脱敏
java·spring boot·后端·注解脱敏·信息脱敏