所有分类
  • 所有分类
  • 未分类

Spring AI–快速入门2:上下文记忆+会话隔离

简介

上篇文章:Spring Ai–快速入门1:对话实战 – 自学精灵 实现了简单对话功能,但不支持上下文。

对话记忆的核心是:上下文的存储(发送请求时会附带)。本文实现上下文记忆和会话隔离。

会话记忆

代码

详述

SpringAI 对话记忆核心类是ChatMemory。

  1. ChatMemory提供的方法:添加消息到对话、从对话中检索消息、清除对话历史。
  2. 目前有一个内置实现:MessageWindowChatMemory,可配置最大消息数量(默认20),当消息数量超过时,较旧的消息会被移除。
  3. 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的生成。

详细方案如下:

  1. 第一次请求时:前端传给后端空的会话ID,后端去创建会话ID并请求AI,并将会话ID返回给前端。
  2. 第二次及之后的请求:前端传给后端的会话ID是后端第一次返回的会话ID。
  3. 前端要有“新建会话”按钮,点击后新建会话,然后重复上边第1,2步(从传给后端空的会话ID开始)

默认的内存存储方案分析

ChatMemory

上边ChatMemory主要是为了了解如何自定义配置。实际上,Spring Ai有默认的ChatMemory的Bean:InMemoryChatMemoryRepository,源码如下:

MessageWindowChatMemory

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

上边各个组件的关系:

ChatMemory(接口)

定义了聊天记忆的基本行为,如:记住消息、获取消息等。是最高层次的抽象

MessageWindowChatMemory(实现类)

实现 ChatMemory接口,并引入了“消息窗口”的概念,限制记忆的消息数量

ChatMemoryRepository(接口)

定义了对聊天记忆的 CRUD (增删改查) 。是低层次的存储抽象

InMemoryChatMemoryRepository(实现类)

实现 ChatMemoryRepository接口,将聊天记忆存储在内存中。

功能优化:会话记忆持久化

见:Spring AI–快速入门3:上下文记忆持久化-CSDN博客

0

评论0

请先

站点公告

🪐AI课程,已部分更新~🪐
✨持续输出~✨
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录