Spring AI 对话记忆

本文介绍如何使用 Spring AI 的对话记忆实现多轮对话:按会话 ID 持久化历史消息,每次请求时把该会话的历史一并发给模型,让回复能联系上下文。本文使用 MessageChatMemoryAdvisor + JDBC 存储,并通过 Cookie 区分不同会话。

示例代码库

您可以在 GitHub 仓库 中找到本文的示例代码。

为什么需要对话记忆

单轮对话每次只发当前用户输入,模型看不到之前的问答,无法实现「接着上一句说」、追问、澄清等。对话记忆的作用是:

  • 按会话隔离:同一会话内的多轮消息归为一组(用 conversationId 区分)。
  • 持久化:历史消息写入数据库(本示例用 PostgreSQL),重启后仍可继续同一会话。
  • 自动注入:每次调用时,Advisor 根据 conversationId 取出历史,拼进本次 Prompt,再发给模型。

本示例使用 DeepSeek(OpenAI 兼容 API)和 PostgreSQL 存储记忆;前端提供简单聊天页面,通过 Cookie 携带会话 ID。

项目结构概览

  • ChatMemoryApplication:Spring Boot 入口。
  • ChatControllerPOST /api/chat,接收用户输入,按 Cookie 中的会话 ID 调用带记忆的 ChatClient,并回写 Cookie。
  • IndexControllerGET /,返回聊天页面(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:提供 ChatMemory Bean 和 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) {}
}

要点:

  1. MessageChatMemoryAdvisor.builder(chatMemory).build():在默认 Advisor 链中加入对话记忆,会根据 ChatMemory.CONVERSATION_ID 参数读写该会话的历史。
  2. advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)):本次请求使用 conversationId;若无 Cookie 则用新 UUID,并在响应里通过 Set-Cookie 下发给浏览器,后续请求自动带上同一会话 ID。
  3. SimpleLoggerAdvisor:可选,便于在日志中查看请求/响应。

这样,同一 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.*

运行与验证

  1. 环境变量:设置 DeepSeek API Key。

    export DEEPSEEK_API_KEY=<your-api-key>
    
  2. 启动 Postgres(若使用 docker-compose):

    cd 07-chat-memory && docker compose up -d
    
  3. 启动应用

    mvn spring-boot:run
    
  4. 打开聊天页面:浏览器访问 http://localhost:8080/ ,在页面中连续多轮提问,模型应能结合上文回答。

  5. 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
    
  6. 查看数据库。可以看到数据库里面创建一个 spring_ai_chat_memory 表:

    spring-ai-chat-memory-postgres-table

小结

  • 对话记忆:通过 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 即可。