Spring AI Alibaba零基础速成(5) ---- Memory(记忆)

大模型默认只能单轮对话,每次对话完成后就会丢失当前对话记忆,我们之前了解过可以通过AssistantMessage把大模型回复结果存储起来下次提问时在发送给大模型,不过使用过于麻烦和受限,Spring AI 和Spring AI Alibaba都实现了更好实现记忆化的方式:

1. 内存存储

java 复制代码
@Configuration
public class SaaLLMConfig {
    private final String QWEN_MODEL = "qwen-plus";
    @Bean
    public DashScopeApi dashScopeApi() {
        return DashScopeApi.builder().apiKey(System.getenv("AliQWenAPIKey")).build();
    }
    @Bean
    public ChatModel qwenChatModel(DashScopeApi dashScopeApi) {
        return DashScopeChatModel
                .builder()
                .dashScopeApi(dashScopeApi)
                .defaultOptions(DashScopeChatOptions.builder().model(QWEN_MODEL).build())
                .build();
    }
    @Bean
    public ChatClient qwenChatClient(@Qualifier("qwenChatModel") ChatModel qwenChatModel) {

        MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory.builder()
                .maxMessages(10)
                .chatMemoryRepository(new InMemoryChatMemoryRepository())
                .build();

        return ChatClient
                .builder(qwenChatModel)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build())
                .build();
    }
}
  • MessageWindowChatMemory:维护固定容量的消息窗口(默认 20 条)。当消息超限时,自动移除较早的对话消息(始终保留系统消息)。
  • InMemoryChatMemoryRepository: 基于 ConcurrentHashMap 实现内存存储。默认情况下,若未配置其他 Repository,Spring AI 将自动配置 InMemoryChatMemoryRepository 类型的 ChatMemoryRepository Bean供直接使用。

2. 源码解析

关键在于MessageChatMemoryAdvisor这个类,可以看到我们在配置ChatClient时配置了一个该对象,这个就是生效的关键,虽然名字叫 Advisor ,但它和 Spring AOP 一点关系都没有 。它是 Spring AI 自己实现的一套独立拦截器机制。

java 复制代码
	@Override
	public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
		String conversationId = getConversationId(chatClientRequest.context(), this.defaultConversationId);

		// 1. Retrieve the chat memory for the current conversation.
		List<Message> memoryMessages = this.chatMemory.get(conversationId);

		// 2. Advise the request messages list.
		List<Message> processedMessages = new ArrayList<>(memoryMessages);
		processedMessages.addAll(chatClientRequest.prompt().getInstructions());

		// 3. Create a new request with the advised messages.
		ChatClientRequest processedChatClientRequest = chatClientRequest.mutate()
			.prompt(chatClientRequest.prompt().mutate().messages(processedMessages).build())
			.build();

		// 4. Add the new user message to the conversation memory.
		UserMessage userMessage = processedChatClientRequest.prompt().getUserMessage();
		this.chatMemory.add(conversationId, userMessage);

		return processedChatClientRequest;
	}

	@Override
	public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
		List<Message> assistantMessages = new ArrayList<>();
		if (chatClientResponse.chatResponse() != null) {
			assistantMessages = chatClientResponse.chatResponse()
				.getResults()
				.stream()
				.map(g -> (Message) g.getOutput())
				.toList();
		}
		this.chatMemory.add(this.getConversationId(chatClientResponse.context(), this.defaultConversationId),
				assistantMessages);
		return chatClientResponse;
	}

在该类源码中我们可以看到关键的两个方法,before和after

  • before:在发送请求前执行先从memoryMessages中获取历史对话,再把本次对话的用户问题也存入chatMemory中。
  • after:在大模型返回后执行,把大模型返回内容存储到chatMemory中

3. Redis存储

配置redis:

XML 复制代码
spring:
  data:
    redis:
      host: localhost
      port: 6379
      database: 0

要使用Redis需要引入依赖:

XML 复制代码
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Alibaba Spring AI Redis 记忆模块 -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
        </dependency>

可以看到这个依赖适配了Jedis和RedissonRedis以及springBoot的Lettuce

java 复制代码
@Configuration
public class SaaLLMConfig {
    private final String DEEPSEEK_MODEL = "deepseek-v3.2";

    @Bean
    public ChatModel deepseekChatModel() {
        return DashScopeChatModel
                .builder()
                .dashScopeApi(DashScopeApi.builder().apiKey(System.getenv("AliQWenAPIKey")).build())
                .defaultOptions(DashScopeChatOptions.builder().model(DEEPSEEK_MODEL).build())
                .build();
    }

    @Bean
    public ChatClient deepseekChatClient(@Qualifier("deepseekChatModel") ChatModel deepseekChatModel) {
        LettuceRedisChatMemoryRepository redisCMR = LettuceRedisChatMemoryRepository.builder().build();

        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
                .maxMessages(10)
                .chatMemoryRepository(redisCMR)
                .build();

        return ChatClient
                .builder(deepseekChatModel)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}

同时,一个大模型通常会服务多个用户,对于不同用户的对话应该分开存储,我们可以通过设置CONVERSATION_ID来实现:

java 复制代码
@RestController
public class ChatMemoryController {
    @Resource(name = "deepseekChatClient")
    ChatClient deepseekChatClient;

    @GetMapping("/chatmemory/chat2")
    public Flux<String> chat2(String userId, String question) {
        return deepseekChatClient
                .prompt()
                .user(question)
                .advisors(
                        advisorSpec -> advisorSpec.param(CONVERSATION_ID, userId)
                )
                .stream()
                .content();
    }
}

通过.advisors方法设置,如果不设置会使用默认的,这样所有用户的对话是共享的:

可以看到是按id分别存储的

4. 关系型数据库存储

导入依赖:

XML 复制代码
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Spring JDBC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--Spring AI JDBC 聊天记忆启动器-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
        </dependency>

增加配置:

XML 复制代码
spring:
  ai:
    chat:
      #开启Schema初始化,会自动创建表用
      memory:
        repository:
          jdbc:
            initialize-schema: always
  # 数据库连接
  datasource:
    url: jdbc:mysql://localhost:3306/ai_chat?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

  # JDBC 自动建表
  sql:
    init:
      mode: always

配置ChatClient:

java 复制代码
@Configuration
public class SaaLLMConfig {
    private final String DEEPSEEK_MODEL = "deepseek-v3.2";
    private final String QWEN_MODEL = "qwen-plus";
    private final String GLM5 = "glm-5";

    @Bean
    public DashScopeApi dashScopeApi() {
        return DashScopeApi.builder().apiKey(System.getenv("AliQWenAPIKey")).build();
    }
    @Bean
    public ChatModel glmChatModel() {
        return DashScopeChatModel
                .builder()
                .dashScopeApi(DashScopeApi.builder().apiKey(System.getenv("AliQWenAPIKey")).build())
                .defaultOptions(DashScopeChatOptions.builder().model(GLM5).build())
                .build();
    }
    @Bean
    public ChatClient glmChatClient(@Qualifier("glmChatModel") ChatModel glmChatModel, JdbcChatMemoryRepository jdbcChatMemoryRepository) {
        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
                .maxMessages(10)
                .chatMemoryRepository(jdbcChatMemoryRepository)
                .build();

        return ChatClient
                .builder(glmChatModel)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

Spring AI 为 JdbcChatMemoryRepository 提供自动配置,可直接注入。

java 复制代码
@RestController
public class ChatMemoryController {
    @Resource(name = "glmChatClient")
    ChatClient glmChatClient;

    @GetMapping("/chatmemory/chat3")
    public Flux<String> chat3(String userId, String question) {
        return glmChatClient
                .prompt()
                .user(question)
                .advisors(
                        advisorSpec -> advisorSpec.param(CONVERSATION_ID, userId)
                )
                .stream()
                .content();
    }
}
相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
小羊没烦恼!3 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
回眸&啤酒鸭3 天前
【回眸】Minicart 电商购物车核心功能落地指南
人工智能
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
一隅论数智3 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
AI的探索之旅3 天前
97 个 OpenCV 实例(三十):双目立体,从标定到点云
人工智能·opencv·计算机视觉
AlbertZein3 天前
Step-5-Preview 上手实测:3D 游戏、金融分析、网页设计一次跑完
人工智能·aigc
俊昭喜喜里3 天前
java中的继承和多态的区别
java
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#