ai
  • outline
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 1.面试题目
  • 2. 参考答案
    • 2.1 Spring AI实现AI多轮对话功能
      • 2.1.1 核心原理:记忆能力与上下文连贯
      • 2.1.2 关键组件与实现流程
      • 2.1.3 实现示例
    • 2.2 对话记忆持久化问题及解决方案
      • 2.2.1 默认实现及局限性
      • 2.2.2 Spring AI提供的持久化方案
    • 2.3 自定义实现高效可靠的对话记忆持久化
      • 2.3.1 技术选型:使用高性能的Kryo序列化库
      • 2.3.2 自定义实现方案
      • 2.3.3 高级优化:支持多种存储后端
      • 2.3.4 配置与使用
    • 2.4 性能优化与监控
      • 2.4.1 性能优化策略
      • 2.4.2 监控与指标
    • 2.5 总结

1.面试题目 #

请详细阐述在Spring AI框架下,如何实现AI多轮对话功能?并针对对话记忆的持久化问题,分析其默认实现方式的局限性,以及Spring AI提供了哪些解决方案。如果您需要构建一个高效且可靠的对话记忆持久化机制,您会如何自定义实现?请说明具体的技术选型和理由。

2. 参考答案 #

2.1 Spring AI实现AI多轮对话功能 #

2.1.1 核心原理:记忆能力与上下文连贯 #

多轮对话功能的关键在于让AI具备"记忆能力",即能够记住与用户之前的对话内容并保持上下文连贯。在Spring AI框架中,这主要通过对话记忆(Chat Memory)和顾问(Advisor)特性来实现。

2.1.2 关键组件与实现流程 #

1. ChatClient: 作为核心组件,用于构建功能更丰富、更灵活的AI对话。

2. Advisors: ChatClient支持使用Advisors。Advisors可以理解为一系列可插拔的拦截器,它们在调用AI前后执行额外操作。

3. MessageChatMemoryAdvisor: 这是实现多轮对话的关键Advisor。

  • 作用: 它负责从对话记忆中检索历史对话,并将这些历史对话作为消息集合添加到当前的提示词(Prompt)中。
  • 效果: 通过这种方式,实现了让AI模型能够"记住"之前的交流,从而维持对话的上下文。

4. ChatMemory 接口: MessageChatMemoryAdvisor依赖于ChatMemory接口的实现来存取对话历史。ChatMemory接口中定义了保存消息、查询消息和清空历史的方法。

2.1.3 实现示例 #

@Configuration
@EnableConfigurationProperties(SpringAiProperties.class)
public class MultiTurnChatConfig {

    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }

    @Bean
    public MessageChatMemoryAdvisor memoryAdvisor(ChatMemory chatMemory) {
        return new MessageChatMemoryAdvisor(chatMemory);
    }

    @Bean
    public ChatClient chatClient(ChatModel chatModel, MessageChatMemoryAdvisor memoryAdvisor) {
        return ChatClient.builder(chatModel)
            .defaultAdvisors(memoryAdvisor)
            .build();
    }
}

@Service
public class MultiTurnChatService {

    private final ChatClient chatClient;
    private final ChatMemory chatMemory;

    public MultiTurnChatService(ChatClient chatClient, ChatMemory chatMemory) {
        this.chatClient = chatClient;
        this.chatMemory = chatMemory;
    }

    public String chat(String userMessage, String conversationId) {
        // 1. 添加用户消息到记忆
        chatMemory.add(conversationId, new UserMessage(userMessage));

        // 2. 生成AI响应
        String response = chatClient.prompt()
            .user(userMessage)
            .call()
            .content();

        // 3. 添加AI响应到记忆
        chatMemory.add(conversationId, new AssistantMessage(response));

        return response;
    }

    public void clearMemory(String conversationId) {
        chatMemory.clear(conversationId);
    }

    public List<Message> getConversationHistory(String conversationId) {
        return chatMemory.get(conversationId);
    }
}

2.2 对话记忆持久化问题及解决方案 #

2.2.1 默认实现及局限性 #

Spring AI默认情况下会使用InMemoryChatMemory实现ChatMemory接口。

特点: 对话记忆仅存在于应用程序的内存中。

局限性: 一旦服务重启,所有存储在内存中的对话记忆就会丢失,无法实现长期记忆。为了解决这个问题,需要将对话记忆进行持久化。

// 默认的内存实现
@Component
public class DefaultMemoryService {

    private final InMemoryChatMemory chatMemory;

    public DefaultMemoryService() {
        this.chatMemory = new InMemoryChatMemory();
    }

    // 问题:服务重启后记忆丢失
    public void addMessage(String conversationId, Message message) {
        chatMemory.add(conversationId, message);
    }
}

2.2.2 Spring AI提供的持久化方案 #

Spring AI提供了一些内置的持久化方案:

1. JdbcChatMemory: 可以将对话保存在关系型数据库中,提供了一种基于数据库的持久化方式。

@Configuration
public class DatabaseMemoryConfig {

    @Bean
    public ChatMemory jdbcChatMemory(DataSource dataSource) {
        return new JdbcChatMemory(dataSource);
    }
}

// 使用示例
@Service
public class DatabaseMemoryService {

    private final ChatMemory chatMemory;

    public DatabaseMemoryService(ChatMemory chatMemory) {
        this.chatMemory = chatMemory;
    }

    public void persistConversation(String conversationId, List<Message> messages) {
        for (Message message : messages) {
            chatMemory.add(conversationId, message);
        }
    }
}

2. RedisChatMemory: 基于Redis的分布式记忆存储。

@Configuration
public class RedisMemoryConfig {

    @Bean
    public ChatMemory redisChatMemory(RedisTemplate<String, Object> redisTemplate) {
        return new RedisChatMemory(redisTemplate);
    }
}

2.3 自定义实现高效可靠的对话记忆持久化 #

在实际项目中,考虑到spring-ai-starter-model-chat-memory-jdbc可能存在依赖版本较少且缺乏相关介绍的情况,或者有特定的性能、存储需求时,可以选择自定义实现ChatMemory接口。

2.3.1 技术选型:使用高性能的Kryo序列化库 #

选择理由:

  1. 复杂对象结构: Message接口在Spring AI中可能有多种实现,导致其结构不统一。
  2. 序列化挑战: 这些Message对象可能没有无参构造函数,也可能没有实现Serializable接口。
  3. 兼容性问题: 在这种复杂情况下,普通的JSON序列化(如Jackson或Gson)难以有效处理,容易出现兼容性问题或需要大量定制。
  4. 性能优势: Kryo是一个高性能、高效的Java序列化框架,它能够处理复杂的对象图,并且通常比Java自带的序列化或JSON序列化更小、更快。

2.3.2 自定义实现方案 #

// 1. 自定义ChatMemory实现
@Component
public class FileBasedChatMemory implements ChatMemory {

    private final Kryo kryo;
    private final String storagePath;
    private final Map<String, List<Message>> memoryCache;

    public FileBasedChatMemory(@Value("${chat.memory.storage.path:/tmp/chat-memory}") String storagePath) {
        this.storagePath = storagePath;
        this.memoryCache = new ConcurrentHashMap<>();
        this.kryo = new Kryo();

        // 配置Kryo
        configureKryo();

        // 确保存储目录存在
        createStorageDirectory();
    }

    private void configureKryo() {
        // 注册Message相关类
        kryo.register(UserMessage.class);
        kryo.register(AssistantMessage.class);
        kryo.register(SystemMessage.class);
        kryo.register(ArrayList.class);
        kryo.register(ConcurrentHashMap.class);

        // 设置序列化策略
        kryo.setDefaultSerializer(DefaultSerializers.KryoSerializableSerializer.class);
    }

    @Override
    public void add(String conversationId, Message message) {
        List<Message> messages = memoryCache.computeIfAbsent(conversationId, k -> new ArrayList<>());
        messages.add(message);

        // 异步持久化
        persistToFile(conversationId, messages);
    }

    @Override
    public List<Message> get(String conversationId) {
        // 先从缓存获取
        List<Message> cachedMessages = memoryCache.get(conversationId);
        if (cachedMessages != null) {
            return new ArrayList<>(cachedMessages);
        }

        // 从文件加载
        List<Message> messages = loadFromFile(conversationId);
        if (messages != null) {
            memoryCache.put(conversationId, messages);
        }

        return messages != null ? new ArrayList<>(messages) : new ArrayList<>();
    }

    @Override
    public void clear(String conversationId) {
        memoryCache.remove(conversationId);

        // 删除文件
        File file = getConversationFile(conversationId);
        if (file.exists()) {
            file.delete();
        }
    }

    private void persistToFile(String conversationId, List<Message> messages) {
        try {
            File file = getConversationFile(conversationId);

            try (FileOutputStream fos = new FileOutputStream(file);
                 OutputStream os = new BufferedOutputStream(fos)) {

                kryo.writeObject(os, messages);
                os.flush();
            }
        } catch (Exception e) {
            log.error("Failed to persist conversation {} to file", conversationId, e);
        }
    }

    private List<Message> loadFromFile(String conversationId) {
        try {
            File file = getConversationFile(conversationId);
            if (!file.exists()) {
                return null;
            }

            try (FileInputStream fis = new FileInputStream(file);
                 InputStream is = new BufferedInputStream(fis)) {

                return kryo.readObject(is, ArrayList.class);
            }
        } catch (Exception e) {
            log.error("Failed to load conversation {} from file", conversationId, e);
            return null;
        }
    }

    private File getConversationFile(String conversationId) {
        return new File(storagePath, conversationId + ".kryo");
    }

    private void createStorageDirectory() {
        File dir = new File(storagePath);
        if (!dir.exists()) {
            dir.mkdirs();
        }
    }
}

2.3.3 高级优化:支持多种存储后端 #

// 2. 抽象存储接口
public interface ChatMemoryStorage {
    void save(String conversationId, List<Message> messages);
    List<Message> load(String conversationId);
    void delete(String conversationId);
    boolean exists(String conversationId);
}

// 3. 文件存储实现
@Component
public class FileChatMemoryStorage implements ChatMemoryStorage {

    private final Kryo kryo;
    private final String storagePath;

    public FileChatMemoryStorage(@Value("${chat.memory.file.path:/tmp/chat-memory}") String storagePath) {
        this.storagePath = storagePath;
        this.kryo = new Kryo();
        configureKryo();
    }

    @Override
    public void save(String conversationId, List<Message> messages) {
        // 实现文件保存逻辑
    }

    @Override
    public List<Message> load(String conversationId) {
        // 实现文件加载逻辑
        return null;
    }

    @Override
    public void delete(String conversationId) {
        // 实现文件删除逻辑
    }

    @Override
    public boolean exists(String conversationId) {
        // 实现文件存在检查逻辑
        return false;
    }
}

// 4. 数据库存储实现
@Component
public class DatabaseChatMemoryStorage implements ChatMemoryStorage {

    private final JdbcTemplate jdbcTemplate;

    public DatabaseChatMemoryStorage(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void save(String conversationId, List<Message> messages) {
        // 实现数据库保存逻辑
        String sql = "INSERT INTO chat_messages (conversation_id, message_type, content, timestamp) VALUES (?, ?, ?, ?)";

        for (Message message : messages) {
            jdbcTemplate.update(sql, 
                conversationId, 
                message.getClass().getSimpleName(),
                message.getContent(),
                System.currentTimeMillis()
            );
        }
    }

    @Override
    public List<Message> load(String conversationId) {
        // 实现数据库加载逻辑
        String sql = "SELECT message_type, content FROM chat_messages WHERE conversation_id = ? ORDER BY timestamp";

        return jdbcTemplate.query(sql, 
            new Object[]{conversationId},
            (rs, rowNum) -> {
                String messageType = rs.getString("message_type");
                String content = rs.getString("content");

                return switch (messageType) {
                    case "UserMessage" -> new UserMessage(content);
                    case "AssistantMessage" -> new AssistantMessage(content);
                    case "SystemMessage" -> new SystemMessage(content);
                    default -> new UserMessage(content);
                };
            }
        );
    }

    @Override
    public void delete(String conversationId) {
        String sql = "DELETE FROM chat_messages WHERE conversation_id = ?";
        jdbcTemplate.update(sql, conversationId);
    }

    @Override
    public boolean exists(String conversationId) {
        String sql = "SELECT COUNT(*) FROM chat_messages WHERE conversation_id = ?";
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class, conversationId);
        return count != null && count > 0;
    }
}

// 5. 统一的ChatMemory实现
@Component
public class UnifiedChatMemory implements ChatMemory {

    private final ChatMemoryStorage storage;
    private final Map<String, List<Message>> memoryCache;
    private final int maxCacheSize;

    public UnifiedChatMemory(ChatMemoryStorage storage, 
                           @Value("${chat.memory.cache.size:1000}") int maxCacheSize) {
        this.storage = storage;
        this.memoryCache = new ConcurrentHashMap<>();
        this.maxCacheSize = maxCacheSize;
    }

    @Override
    public void add(String conversationId, Message message) {
        List<Message> messages = memoryCache.computeIfAbsent(conversationId, k -> new ArrayList<>());
        messages.add(message);

        // 异步持久化
        CompletableFuture.runAsync(() -> {
            try {
                storage.save(conversationId, messages);
            } catch (Exception e) {
                log.error("Failed to persist conversation {}", conversationId, e);
            }
        });

        // 缓存大小控制
        if (memoryCache.size() > maxCacheSize) {
            evictOldestConversation();
        }
    }

    @Override
    public List<Message> get(String conversationId) {
        // 先从缓存获取
        List<Message> cachedMessages = memoryCache.get(conversationId);
        if (cachedMessages != null) {
            return new ArrayList<>(cachedMessages);
        }

        // 从存储加载
        List<Message> messages = storage.load(conversationId);
        if (messages != null) {
            memoryCache.put(conversationId, messages);
        }

        return messages != null ? new ArrayList<>(messages) : new ArrayList<>();
    }

    @Override
    public void clear(String conversationId) {
        memoryCache.remove(conversationId);
        storage.delete(conversationId);
    }

    private void evictOldestConversation() {
        // 简单的LRU实现
        String oldestKey = memoryCache.keySet().iterator().next();
        memoryCache.remove(oldestKey);
    }
}

2.3.4 配置与使用 #

// 6. 配置类
@Configuration
@EnableConfigurationProperties(SpringAiProperties.class)
public class CustomMemoryConfig {

    @Bean
    @Primary
    public ChatMemory customChatMemory(@Qualifier("fileChatMemoryStorage") ChatMemoryStorage storage) {
        return new UnifiedChatMemory(storage);
    }

    @Bean
    public ChatMemoryStorage fileChatMemoryStorage(@Value("${chat.memory.file.path:/tmp/chat-memory}") String storagePath) {
        return new FileChatMemoryStorage(storagePath);
    }

    @Bean
    public ChatMemoryStorage databaseChatMemoryStorage(JdbcTemplate jdbcTemplate) {
        return new DatabaseChatMemoryStorage(jdbcTemplate);
    }
}

// 7. 使用示例
@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final MultiTurnChatService chatService;

    public ChatController(MultiTurnChatService chatService) {
        this.chatService = chatService;
    }

    @PostMapping("/conversation/{conversationId}")
    public ResponseEntity<ChatResponse> chat(
            @PathVariable String conversationId,
            @RequestBody ChatRequest request) {

        try {
            String response = chatService.chat(request.getMessage(), conversationId);
            return ResponseEntity.ok(new ChatResponse(response));
        } catch (Exception e) {
            return ResponseEntity.status(500)
                .body(new ChatResponse("聊天服务暂时不可用"));
        }
    }

    @DeleteMapping("/conversation/{conversationId}")
    public ResponseEntity<Void> clearConversation(@PathVariable String conversationId) {
        chatService.clearMemory(conversationId);
        return ResponseEntity.ok().build();
    }

    @GetMapping("/conversation/{conversationId}/history")
    public ResponseEntity<List<Message>> getConversationHistory(@PathVariable String conversationId) {
        List<Message> history = chatService.getConversationHistory(conversationId);
        return ResponseEntity.ok(history);
    }
}

2.4 性能优化与监控 #

2.4.1 性能优化策略 #

// 8. 性能优化
@Component
public class OptimizedChatMemory implements ChatMemory {

    private final ChatMemoryStorage storage;
    private final Cache<String, List<Message>> cache;
    private final ExecutorService persistenceExecutor;

    public OptimizedChatMemory(ChatMemoryStorage storage) {
        this.storage = storage;
        this.cache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofHours(1))
            .build();
        this.persistenceExecutor = Executors.newFixedThreadPool(4);
    }

    @Override
    public void add(String conversationId, Message message) {
        List<Message> messages = cache.get(conversationId, k -> new ArrayList<>());
        messages.add(message);

        // 异步批量持久化
        persistenceExecutor.submit(() -> {
            try {
                storage.save(conversationId, messages);
            } catch (Exception e) {
                log.error("Failed to persist conversation {}", conversationId, e);
            }
        });
    }

    @Override
    public List<Message> get(String conversationId) {
        return cache.get(conversationId, k -> {
            List<Message> messages = storage.load(conversationId);
            return messages != null ? messages : new ArrayList<>();
        });
    }

    @Override
    public void clear(String conversationId) {
        cache.invalidate(conversationId);
        storage.delete(conversationId);
    }
}

2.4.2 监控与指标 #

// 9. 监控组件
@Component
public class ChatMemoryMonitor {

    private final MeterRegistry meterRegistry;
    private final Counter saveCounter;
    private final Counter loadCounter;
    private final Timer saveTimer;
    private final Timer loadTimer;

    public ChatMemoryMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.saveCounter = Counter.builder("chat.memory.save.count")
            .description("Chat memory save operations")
            .register(meterRegistry);
        this.loadCounter = Counter.builder("chat.memory.load.count")
            .description("Chat memory load operations")
            .register(meterRegistry);
        this.saveTimer = Timer.builder("chat.memory.save.duration")
            .description("Chat memory save duration")
            .register(meterRegistry);
        this.loadTimer = Timer.builder("chat.memory.load.duration")
            .description("Chat memory load duration")
            .register(meterRegistry);
    }

    public void recordSave(String conversationId, long duration) {
        saveCounter.increment(Tags.of("conversation_id", conversationId));
        saveTimer.record(duration, TimeUnit.MILLISECONDS);
    }

    public void recordLoad(String conversationId, long duration) {
        loadCounter.increment(Tags.of("conversation_id", conversationId));
        loadTimer.record(duration, TimeUnit.MILLISECONDS);
    }
}

2.5 总结 #

通过以上多层次的解决方案,Spring AI可以实现高效且可靠的对话记忆持久化:

  1. 默认方案: 使用InMemoryChatMemory进行快速原型开发
  2. 内置方案: 使用JdbcChatMemory或RedisChatMemory进行简单持久化
  3. 自定义方案: 使用Kryo序列化实现高性能文件存储
  4. 统一方案: 通过抽象接口支持多种存储后端
  5. 优化方案: 通过缓存、异步处理、监控等机制提升性能

这种设计既保证了系统的灵活性,又确保了对话记忆的可靠持久化,为构建高质量的AI对话应用提供了坚实的基础。

访问验证

请输入访问令牌

Token不正确,请重新输入