Spring AI 对话记忆
本文介绍如何使用 Spring AI 的对话记忆实现多轮对话:按会话 ID 持久化历史消息,每次请求时把该会话的历史一并发给模型,让回复能联系上下文。本文使用 MessageChatMemoryAdvisor + JDBC 存储,并通过 Cookie 区分不同会话。
示例代码库
您可以在 GitHub 仓库 中找到本文的示例代码。
为什么需要对话记忆
单轮对话每次只发当前用户输入,模型看不到之前的问答,无法实现「接着上一句说」、追问、澄清等。对话记忆的作用是:
- 按会话隔离:同一会话内的多轮消息归为一组(用 conversationId 区分)。
- 持久化:历史消息写入数据库(本示例用 PostgreSQL),重启后仍可继续同一会话。
- 自动注入:每次调用时,Advisor 根据 conversationId 取出历史,拼进本次 Prompt,再发给模型。
本示例使用 DeepSeek(OpenAI 兼容 API)和 PostgreSQL 存储记忆;前端提供简单聊天页面,通过 Cookie 携带会话 ID。
项目结构概览
- ChatMemoryApplication:Spring Boot 入口。
- ChatController:
POST /api/chat,接收用户输入,按 Cookie 中的会话 ID 调用带记忆的 ChatClient,并回写 Cookie。 - IndexController:
GET /,返回聊天页面(Thymeleaf 模板index.html)。 - ChatMemory:由
spring-ai-starter-model-chat-memory-repository-jdbc自动配置,基于 JDBC 的ChatMemoryRepository实现。 - docker-compose.yaml:提供 PostgreSQL,供 JDBC 记忆存储使用。
核心依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
- openai:调用 DeepSeek(或其它 OpenAI 兼容模型)。
- chat-memory-repository-jdbc:提供
ChatMemoryBean 和 JDBC 持久化,并依赖数据源。 - postgresql:JDBC 驱动。
- docker-compose:可选,用于启动本地 Postgres。
后端:ChatClient + MessageChatMemoryAdvisor
在构造 ChatClient 时注册 MessageChatMemoryAdvisor,并注入 ChatMemory(由 JDBC starter 提供)。每次请求时通过 Advisor 参数传入当前会话 ID,Advisor 会按该 ID 读写历史并拼入 Prompt。
@RestController
@RequestMapping("/")
class ChatController {
private final ChatClient chatClient;
ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
new SimpleLoggerAdvisor()
)
.build();
}
@PostMapping("/api/chat")
ResponseEntity<Output> chat(@RequestBody @Valid Input input,
@CookieValue(name = "X-CONV-ID", required = false) String convId) {
String conversationId = convId == null ? UUID.randomUUID().toString() : convId;
var response = this.chatClient.prompt()
.user(input.prompt())
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call().content();
ResponseCookie cookie = ResponseCookie.from("X-CONV-ID", conversationId)
.path("/")
.maxAge(3600)
.build();
Output output = new Output(response);
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(output);
}
record Input(@NotBlank String prompt) {}
record Output(String content) {}
}
要点:
- MessageChatMemoryAdvisor.builder(chatMemory).build():在默认 Advisor 链中加入对话记忆,会根据
ChatMemory.CONVERSATION_ID参数读写该会话的历史。 - advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)):本次请求使用
conversationId;若无 Cookie 则用新 UUID,并在响应里通过 Set-Cookie 下发给浏览器,后续请求自动带上同一会话 ID。 - SimpleLoggerAdvisor:可选,便于在日志中查看请求/响应。
这样,同一 Cookie 下的多轮请求都会带上对应会话的历史,模型即可基于上下文回复。
前端:会话 ID 与 Cookie
聊天页面(templates/index.html)使用 jQuery 调用 POST /api/chat,请求体为 { "prompt": "用户输入" }。浏览器会自动携带 Cookie,因此:
- 首次访问无
X-CONV-ID,服务端生成新 conversationId 并写入 Cookie。 - 之后同一浏览器内的请求都会带上该 Cookie,服务端用同一 conversationId 读写记忆,实现多轮对话。
前端只需正常发 POST,无需自己维护会话 ID。
配置
application.properties 示例:
spring.application.name=07-chat-memory
spring.ai.openai.base-url=https://api.deepseek.com
spring.ai.openai.api-key=${DEEPSEEK_API_KEY}
spring.ai.openai.chat.options.model=deepseek-reasoner
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
# 启动时自动创建 Chat Memory 所需表结构
spring.ai.chat.memory.repository.jdbc.initialize-schema=always
docker-compose.yaml(本地 Postgres):
services:
postgres:
image: postgres:18
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- "5432:5432"
使用 Docker Compose 时,Spring Boot 会按服务自动配置数据源,无需再写 spring.datasource.*。
运行与验证
环境变量:设置 DeepSeek API Key。
export DEEPSEEK_API_KEY=<your-api-key>启动 Postgres(若使用 docker-compose):
cd 07-chat-memory && docker compose up -d启动应用:
mvn spring-boot:run打开聊天页面:浏览器访问
http://localhost:8080/,在页面中连续多轮提问,模型应能结合上文回答。API 测试(同一会话需在请求中带相同 Cookie,或先请求一次拿到 Set-Cookie 再复用):
# 首次请求,从响应头中拿到 Set-Cookie 中的 X-CONV-ID,后续请求可携带该 Cookie curl -s -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"prompt":"我叫小明,请记住。"}' -c cookies.txt -b cookies.txt -D - curl -s -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"prompt":"我叫什么名字?"}' -b cookies.txt查看数据库。可以看到数据库里面创建一个
spring_ai_chat_memory表:
小结
- 对话记忆:通过 ChatMemory + MessageChatMemoryAdvisor,按会话 ID 持久化并注入历史消息,实现多轮上下文对话。
- 会话划分:本示例用 Cookie
X-CONV-ID区分会话,无 Cookie 时生成新 UUID 并回写 Cookie。 - 存储:
spring-ai-starter-model-chat-memory-repository-jdbc使用 PostgreSQL 存储历史,表结构由initialize-schema=always自动创建,创建的表名为spring_ai_chat_memory。 - 前端:Thymeleaf 提供聊天页,正常调用
POST /api/chat即可,Cookie 由浏览器自动维护。
在 Spring AI 中接入对话记忆,只需引入 JDBC 记忆 starter、配置数据源、在 ChatClient 中加上 MessageChatMemoryAdvisor 并在每次请求中传入 conversationId 即可。
