在大模型应用开发中,让AI“记住”对话历史是实现连贯对话体验的关键。本文将带你深入理解LangChain4j会话记忆机制,并实现完整的隔离与持久化方案。


引言:为什么大模型需要“记忆”?

大语言模型本身是无状态的,每次请求都是独立的。要让AI助手在对话中记住上下文,必须将历史对话与新问题一起发送给模型。这就像与一个记忆力只有7秒的鱼对话,需要不断提醒它“我们刚才说到哪儿了”。

一、会话记忆的核心原理

1.1 大模型的“记忆”本质

大模型本身不具备记忆能力。实现会话记忆的唯一方法​ 就是将历史对话内容与新提示词一起发送。

API请求示例如下:

{
  "model": "qwen-plus",
  "messages": [
    {"role": "system", "content": "你是地理知识小能手"},
    {"role": "user", "content": "北京城市大吗?"},
    {"role": "assistant", "content": "北京是中国的首都,也是一座非常大的城市"},
    {"role": "user", "content": "人口呢?"}
  ]
}

1.2 系统架构中的记忆流程

如图清晰地展示了实现原理:

  1. 用户​ 在浏览器提问

  2. Web后端​ 从存储对象中读取历史对话

  3. 后端​ 将历史对话+新问题发送给大模型

  4. 大模型​ 基于完整上下文生成回答

  5. 后端​ 将新对话存入存储对象,返回回答给用户

这种架构确保了每次对话都在完整上下文中进行。

二、LangChain4j会话记忆基础实现

2.1 ChatMemory接口设计

LangChain4j的核心接口设计:

public interface ChatMemory {
    Object id();  // 获取唯一标识
    void add(ChatMessage message);  // 添加消息
    List<ChatMessage> messages();  // 获取所有消息
    void clear();  // 清除记忆
}

2.2 主要实现类

LangChain4j提供了两种主要的记忆窗口实现:

  1. TokenWindowChatMemory​ - 基于Token数量限制

  2. MessageWindowChatMemory​ - 基于消息条数限制

2.3 基础配置

配置方式:

@Bean
public ChatMemory chatMemory() {
    return MessageWindowChatMemory.builder()
            .maxMessages(20)
            .build();
}

@AiService(
    chatMemory = "chatMemory"
)
public interface ConsultantService {
    // 服务方法
}

三、会话记忆隔离:多用户支持

3.1 问题识别

一个关键问题:默认配置下,所有会话共享同一个记忆存储对象。这会导致:

  • 用户A的对话被用户B看到

  • 不同会话的上下文混淆

  • 安全隐患和数据混乱

3.2 隔离解决方案

完整的会话隔离实现方案:

步骤1:定义ChatMemoryProvider
@Bean
public ChatMemoryProvider chatMemoryProvider() {
    return memoryId -> MessageWindowChatMemory.builder()
            .id(memoryId)
            .maxMessages(20)
            .build();
}
步骤2:服务层参数配置
@AiService(
    chatMemoryProvider = "chatMemoryProvider"
)
public interface ConsultantService {
    @SystemMessage(fromResource = "system.txt")
    Flux<String> chat(@MemoryId String memoryId, 
                      @UserMessage String message);
}
步骤3:Controller层接收memoryId
@PostMapping("/chat")
public Flux<String> chat(@RequestParam String memoryId, 
                         @RequestParam String message) {
    return consultantService.chat(memoryId, message);
}
步骤4:前端传递memoryId

前端在请求时需要传递唯一的memoryId参数,通常可以使用用户ID、会话ID或设备ID。

四、会话记忆持久化:Redis实战

4.1 问题:服务重启记忆丢失

另一个关键问题:默认的会话记忆存储在内存中,服务重启后记忆丢失

4.2 持久化架构设计

LangChain4j通过ChatMemoryStore接口提供存储抽象:

public interface ChatMemoryStore {
    List<ChatMessage> getMessages(Object memoryId);
    void updateMessages(Object memoryId, List<ChatMessage> messages);
    void deleteMessages(Object memoryId);
}

4.3 Redis持久化完整实现

以下是完整的Redis持久化实现:

4.3.1 环境准备
# 启动Redis容器
docker run -d --name redis -p 6379:6379 redis:alpine
4.3.2 Maven依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
4.3.3 配置文件
spring:
  data:
    redis:
      host: localhost
      port: 6379
4.3.4 RedisChatMemoryStore实现

以下是完整的实现代码:

package com.qcby.consultant.repository;

import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Repository
public class RedisChatMemoryStore implements ChatMemoryStore {

    private static final String REDIS_KEY_PREFIX = "chat:memory:";
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private String getKey(Object memoryId) {
        return REDIS_KEY_PREFIX + memoryId.toString();
    }

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        try {
            String key = getKey(memoryId);
            String json = redisTemplate.opsForValue().get(key);
            
            if (json == null || json.trim().isEmpty()) {
                log.debug("No chat memory found for memoryId: {}", memoryId);
                return new ArrayList<>();
            }
            
            List<ChatMessage> messages = ChatMessageDeserializer.messagesFromJson(json);
            log.debug("Retrieved {} messages for memoryId: {}", messages.size(), memoryId);
            return messages;
        } catch (Exception e) {
            log.error("Failed to get messages from Redis for memoryId: {}", memoryId, e);
            return new ArrayList<>();
        }
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        try {
            String key = getKey(memoryId);
            String json = ChatMessageSerializer.messagesToJson(messages);
            
            // 设置1天过期时间,避免内存泄漏
            redisTemplate.opsForValue().set(key, json, Duration.ofDays(1));
            
            log.debug("Updated {} messages for memoryId: {}", messages.size(), memoryId);
        } catch (Exception e) {
            log.error("Failed to update messages in Redis for memoryId: {}", memoryId, e);
        }
    }

    @Override
    public void deleteMessages(Object memoryId) {
        try {
            String key = getKey(memoryId);
            Boolean deleted = redisTemplate.delete(key);
            
            if (Boolean.TRUE.equals(deleted)) {
                log.debug("Deleted chat memory for memoryId: {}", memoryId);
            } else {
                log.debug("No chat memory found to delete for memoryId: {}", memoryId);
            }
        } catch (Exception e) {
            log.error("Failed to delete messages from Redis for memoryId: {}", memoryId, e);
        }
    }
    
    /**
     * 批量删除过期的会话记忆(可选)
     */
    public void cleanupExpiredMemories() {
        // 可以使用Redis的过期策略自动清理,或定时扫描清理
        log.info("Cleanup expired chat memories completed");
    }
}
4.3.5 配置ChatMemoryStore
@Configuration
public class ChatMemoryConfig {

    @Bean
    public ChatMemoryStore chatMemoryStore() {
        return new RedisChatMemoryStore();
    }
    
    @Bean
    public ChatMemoryProvider chatMemoryProvider(ChatMemoryStore chatMemoryStore) {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(20)  // 可根据需求调整
                .chatMemoryStore(chatMemoryStore)
                .build();
    }
}

五、高级优化与最佳实践

5.1 性能优化

  1. 序列化优化:使用更高效的序列化方式(如MessagePack)

  2. 缓存策略:在Redis前增加本地缓存

  3. 分批加载:对于长对话,分批加载历史记录

5.2 内存管理

  1. 设置合理过期时间:根据业务场景设置Redis key的TTL

  2. 定期清理:实现清理任务删除无用会话

  3. 内存限制:监控Redis内存使用,设置淘汰策略

5.3 监控与调试

// 添加监控指标
@Bean
public MeterBinder chatMemoryMetrics(ChatMemoryStore store) {
    return registry -> Gauge.builder("chat.memory.size", 
        () -> ((RedisChatMemoryStore) store).getEstimatedSize())
        .description("Estimated size of chat memory storage")
        .register(registry);
}

通过本文的完整实现,我们解决了大模型应用中的三个核心问题:

  1. 记忆传递:通过历史对话上下文传递实现连贯对话

  2. 会话隔离:通过ChatMemoryProvider实现多用户隔离

  3. 持久化存储:通过Redis实现记忆持久化,服务重启不丢失

会话记忆是大模型应用的基础设施,良好的记忆实现能够显著提升用户体验。希望本文的实现方案能为你的AI应用开发提供有力支持。


 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐