简介
上篇文章:Spring Ai–快速入门1:对话实战 – 自学精灵 实现了简单对话功能,但不支持上下文。
对话记忆的核心是:上下文的存储(发送请求时会附带)。本文实现上下文记忆和会话隔离。
会话记忆
代码
详述
SpringAI 对话记忆核心类是ChatMemory。
- ChatMemory提供的方法:添加消息到对话、从对话中检索消息、清除对话历史。
- 目前有一个内置实现:MessageWindowChatMemory,可配置最大消息数量(默认20),当消息数量超过时,较旧的消息会被移除。
- Spring AI 用 ChatMemoryRepository 来读写上下文,其默认实现是:用InMemoryChatMemoryRepository(存储在内存中)。
代码
下边自定义ChatMemory的Bean,并将其绑定到ChatClient的增强器。(实际上,Spring Ai有默认的ChatMemory的Bean,其实现就是InMemoryChatMemoryRepository,最大消息数是20,这里不自定义Bean也可以,本处写出来主要是为了了解如何自定义配置。)
package com.knife.example.ai.chat.config;
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.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 AiCustomConfiguration {
/**
* 聊天对话记忆的存储
*/
@Bean
public ChatMemory chatMemory() {
// 当前版本下 MessageWindowChatMemory 是 ChatMemory 的唯一内置实现类
// 并且构造器私有化,只提供Builder模式来创建实例(这点与之前的版本不一样)
return MessageWindowChatMemory.builder()
// 对话存储的repository存储库层的实现方式。默认是 Spring 提供的 InMemoryChatMemoryRepository
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20) // 最大消息数
.build();
}
/**
* 会话客户端
*/
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
ChatMemory chatMemory) {
return chatClientBuilder
.defaultSystem("你是一个博学的智能聊天助手,请根据用户提问回答!")
.defaultAdvisors(
new SimpleLoggerAdvisor(), // 日志记录器
MessageChatMemoryAdvisor.builder(chatMemory).build() // 添加聊天记录记忆的环绕增强器
)
.build();
}
}
测试
访问:http://localhost:8080/doc.html
如下图所示,可发现:有上下文记忆功能了!



后台日志(可以发现:请求会带着之前的会话问题和结果)
第一次:

第二次:

第三次:

详细日志(这里只贴出请求日志)
2025-12-07T11:45:49.079+08:00 DEBUG 19168 --- [1_chat] [nio-8080-exec-2] o.s.a.c.c.advisor.SimpleLoggerAdvisor : request: ChatClientRequest[prompt=Prompt{messages=[SystemMessage{textContent='你是一个博学的智能聊天助手,请根据用户提问回答!', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='一加一等于几?', metadata={messageType=USER}, messageType=USER}], modelOptions=OpenAiChatOptions: {"streamUsage":false,"model":"qwen-plus","temperature":0.8,"top_p":0.7}}, context={}]
2025-12-07T11:46:24.627+08:00 DEBUG 19168 --- [1_chat] [nio-8080-exec-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor : request: ChatClientRequest[prompt=Prompt{messages=[UserMessage{content='一加一等于几?', metadata={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=一加一等于二。, metadata={role=ASSISTANT, messageType=ASSISTANT, refusal=, finishReason=STOP, annotations=[{}], index=0, id=chatcmpl-6e39b8f1-2bc8-437b-8c04-8d03e8cb2e46}], SystemMessage{textContent='你是一个博学的智能聊天助手,请根据用户提问回答!', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='再加一呢?', metadata={messageType=USER}, messageType=USER}], modelOptions=OpenAiChatOptions: {"streamUsage":false,"model":"qwen-plus","temperature":0.8,"top_p":0.7}}, context={}]
2025-12-07T11:48:28.010+08:00 DEBUG 19168 --- [1_chat] [nio-8080-exec-4] o.s.a.c.c.advisor.SimpleLoggerAdvisor : request: ChatClientRequest[prompt=Prompt{messages=[UserMessage{content='一加一等于几?', metadata={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=一加一等于二。, metadata={role=ASSISTANT, messageType=ASSISTANT, refusal=, finishReason=STOP, annotations=[{}], index=0, id=chatcmpl-6e39b8f1-2bc8-437b-8c04-8d03e8cb2e46}], UserMessage{content='再加一呢?', metadata={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=如果在一加一(即2)的基础上再加一,那么结果就是3。, metadata={role=ASSISTANT, messageType=ASSISTANT, refusal=, finishReason=STOP, annotations=[{}], index=0, id=chatcmpl-f604d8f7-60f0-42b3-b1f4-ede325ad52fb}], SystemMessage{textContent='你是一个博学的智能聊天助手,请根据用户提问回答!', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='再次加一呢?', metadata={messageType=USER}, messageType=USER}], modelOptions=OpenAiChatOptions: {"streamUsage":false,"model":"qwen-plus","temperature":0.8,"top_p":0.7}}, context={}]
会话隔离
现在已经实现记忆,但是还有问题:所有会话共享一个记忆,没有进行隔离!
会话隔离的方法是:在请求发送前,调用 advisor() 方法配置一个 Consumer:
代码
package com.knife.example.ai.chat.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@Slf4j
@Tag(name = "对话")
@RequestMapping("chat")
@RestController
public class ChatController {
@Autowired
private ChatClient chatClient;
@Operation(summary = "直接输出")
@GetMapping("direct")
public String direct(@Parameter(description = "消息") String message,
@Parameter(description = "会话ID") String chatId) {
return chatClient.prompt(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
.call()
.content();
}
/**
* 可以用apifox测试
*/
@Operation(summary = "流式输出")
@GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@Parameter(description = "消息") String message,
@Parameter(description = "会话ID") String chatId) {
return chatClient.prompt(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
.stream()
.content();
}
}
测试1:使用同一会话ID
访问:http://localhost:8080/doc.html



后台日志(可以发现,请求时会带上会话id)
第一次:

第二次:

第三次:

测试2:更换会话ID
更换会话ID后可以发现,已经是单独的上下文了,实现了隔离。

注意事项
本处为了简单,直接在前端指定了会话ID,实际项目里需要进行优化:由后端控制会话ID的生成。
详细方案如下:
- 第一次请求时:前端传给后端空的会话ID,后端去创建会话ID并请求AI,并将会话ID返回给前端。
- 第二次及之后的请求:前端传给后端的会话ID是后端第一次返回的会话ID。
- 前端要有“新建会话”按钮,点击后新建会话,然后重复上边第1,2步(从传给后端空的会话ID开始)
默认的内存存储方案分析
ChatMemory
上边ChatMemory主要是为了了解如何自定义配置。实际上,Spring Ai有默认的ChatMemory的Bean:InMemoryChatMemoryRepository,源码如下:

MessageWindowChatMemory
MessageWindowChatMemory,是一个消息窗口组件,可配置最大消息数量(默认20),当消息数量超过时,较旧的消息会被移除。

上边各个组件的关系:
ChatMemory(接口)
定义了聊天记忆的基本行为,如:记住消息、获取消息等。是最高层次的抽象。
MessageWindowChatMemory(实现类)
实现 ChatMemory接口,并引入了“消息窗口”的概念,限制记忆的消息数量。
ChatMemoryRepository(接口)
定义了对聊天记忆的 CRUD (增删改查) 。是低层次的存储抽象。
InMemoryChatMemoryRepository(实现类)
实现 ChatMemoryRepository接口,将聊天记忆存储在内存中。

请先 !