Spring AI 持久化对话完整指南工具调用与自定义实现前言在开发 AI 应用时对话持久化是一个非常重要的功能。它能够让应用记住用户的对话历史提供更连贯的交互体验。Spring AI 提供了强大的对话记忆管理功能本文将详细介绍如何实现对话持久化包括工具调用的对话持久化以及自定义持久化方案。一、Spring AI 对话持久化概述1.1 为什么需要对话持久化上下文连贯性让 AI 能够理解前面对话的内容用户体验优化避免用户重复提供相同信息多轮对话支持实现复杂的交互流程工具调用追踪记录 AI 调用工具的历史1.2 Spring AI 的记忆管理架构Spring AI 提供了ChatMemory接口来管理对话记忆支持多种实现方式InMemoryChatMemory内存存储适合简单场景RedisChatMemoryRedis 存储支持分布式CustomChatMemory自定义实现支持任意存储介质二、基础对话持久化实现2.1 添加依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0-M4/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-redis-store-spring-boot-starter/artifactId version1.0.0-M4/version /dependency2.2 内存存储实现Configuration public class ChatMemoryConfig { Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } }2.3 使用示例Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) { this.chatClient chatClientBuilder .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); } public String chat(String userId, String message) { return chatClient.prompt() .user(message) .advisors(a - a .param(CHAT_MEMORY_CONVERSATION_ID_KEY, userId) .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) .call() .content(); } }三、Redis 持久化实现3.1 配置 Redis 存储Configuration public class RedisChatMemoryConfig { Bean public ChatMemory redisChatMemory(RedisConnectionFactory redisConnectionFactory) { return new RedisChatMemory(redisConnectionFactory); } }3.2 Redis 配置spring.redis.hostlocalhost spring.redis.port6379 spring.ai.chat.memory.redis.ttl3600 spring.ai.chat.memory.redis.key-prefixchat:3.3 Redis 数据结构Spring AI 在 Redis 中使用 Hash 结构存储对话历史Key: chat:conversation:{userId} Fields: - messages: JSON 格式的消息列表 - timestamp: 最后更新时间四、工具调用的对话持久化4.1 工具调用记录的重要性工具调用Tool Calls是 AI 与外部系统交互的重要方式记录这些调用对于调试和问题排查理解 AI 的决策过程审计和合规要求优化工具使用效率4.2 工具调用持久化配置Configuration public class ToolCallMemoryConfig { Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), new ToolCallMemoryAdvisor() ) .build(); } }4.3 工具调用记录结构工具调用记录包含以下信息public class ToolCallRecord { private String toolName; // 工具名称 private String toolId; // 工具ID private MapString, Object arguments; // 调用参数 private Object result; // 返回结果 private long executionTime; // 执行时间 private boolean success; // 是否成功 private String errorMessage; // 错误信息 }4.4 自定义工具调用记录器Component public class CustomToolCallLogger implements ToolCallListener { private final ToolCallRepository repository; Override public void beforeToolCall(ToolCallRequest request) { log.info(准备调用工具: {}, 参数: {}, request.getToolName(), request.getArguments()); } Override public void afterToolCall(ToolCallResponse response) { ToolCallRecord record new ToolCallRecord(); record.setToolName(response.getToolName()); record.setResult(response.getResult()); record.setExecutionTime(response.getExecutionTime()); record.setSuccess(response.isSuccess()); repository.save(record); log.info(工具调用完成: {}, 耗时: {}ms, response.getToolName(), response.getExecutionTime()); } }4.5 持久化工具调用到数据库创建数据表CREATE TABLE tool_calls ( id BIGINT PRIMARY KEY AUTO_INCREMENT, conversation_id VARCHAR(255) NOT NULL, tool_name VARCHAR(100) NOT NULL, tool_id VARCHAR(100), arguments TEXT, result TEXT, execution_time BIGINT, success BOOLEAN, error_message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_conversation (conversation_id), INDEX idx_tool_name (tool_name) );Repository 实现Repository public interface ToolCallRepository extends JpaRepositoryToolCallRecord, Long { ListToolCallRecord findByConversationIdOrderByCreatedAt(String conversationId); Query(SELECT t.toolName, COUNT(*) FROM ToolCallRecord t WHERE t.createdAt :startDate GROUP BY t.toolName) ListObject[] findToolUsageStats(Param(startDate) LocalDateTime startDate); }五、自定义持久化实现5.1 实现自定义 ChatMemorypublic class CustomDatabaseChatMemory implements ChatMemory { private final ConversationMessageRepository repository; private final int maxMessages; public CustomDatabaseChatMemory(ConversationMessageRepository repository, int maxMessages) { this.repository repository; this.maxMessages maxMessages; } Override public void add(String conversationId, ListMessage messages) { for (Message message : messages) { ConversationMessage cm new ConversationMessage(); cm.setConversationId(conversationId); cm.setMessageType(message.getMessageType().name()); cm.setContent(message.getContent()); cm.setMetadata(extractMetadata(message)); repository.save(cm); } // 限制消息数量 trimMessages(conversationId); } Override public ListMessage get(String conversationId, int lastN) { ListConversationMessage records repository .findByConversationIdOrderByCreatedAtDesc(conversationId, lastN); return records.stream() .sorted(Comparator.comparing(ConversationMessage::getCreatedAt)) .map(this::toMessage) .collect(Collectors.toList()); } Override public void clear(String conversationId) { repository.deleteByConversationId(conversationId); } private void trimMessages(String conversationId) { long count repository.countByConversationId(conversationId); if (count maxMessages) { ListConversationMessage oldMessages repository .findOldestMessages(conversationId, (int)(count - maxMessages)); repository.deleteAll(oldMessages); } } }5.2 数据库表设计CREATE TABLE conversation_messages ( id BIGINT PRIMARY KEY AUTO_INCREMENT, conversation_id VARCHAR(255) NOT NULL, message_type VARCHAR(50) NOT NULL, content TEXT NOT NULL, metadata TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_conversation_created (conversation_id, created_at) );5.3 多租户持久化实现public class MultiTenantChatMemory implements ChatMemory { private final ChatMemory delegate; private final TenantProvider tenantProvider; Override public void add(String conversationId, ListMessage messages) { String tenantId tenantProvider.getCurrentTenantId(); String scopedConversationId tenantId : conversationId; delegate.add(scopedConversationId, messages); } Override public ListMessage get(String conversationId, int lastN) { String tenantId tenantProvider.getCurrentTenantId(); String scopedConversationId tenantId : conversationId; return delegate.get(scopedConversationId, lastN); } }5.4 分层存储策略public class TieredChatMemory implements ChatMemory { private final ChatMemory hotStorage; // Redis - 热数据 private final ChatMemory coldStorage; // Database - 冷数据 private final int hotSizeLimit; Override public ListMessage get(String conversationId, int lastN) { // 先从热存储获取 ListMessage messages hotStorage.get(conversationId, lastN); if (messages.size() lastN) { // 从冷存储补充 int needed lastN - messages.size(); ListMessage coldMessages coldStorage.get(conversationId, needed); messages.addAll(0, coldMessages); } return messages; } Override public void add(String conversationId, ListMessage messages) { hotStorage.add(conversationId, messages); coldStorage.add(conversationId, messages); // 定期清理热存储 if (hotStorage.get(conversationId, Integer.MAX_VALUE).size() hotSizeLimit) { trimHotStorage(conversationId); } } }六、高级特性6.1 对话总结与压缩Component public class ConversationSummarizer { private final ChatClient summarizerClient; public String summarizeConversation(String conversationId, ListMessage messages) { String prompt 请总结以下对话的核心内容保持重要信息 %s 总结要求 1. 保留关键决策和结论 2. 记录重要的工具调用结果 3. 简明扼要不超过200字 .formatted(formatMessages(messages)); return summarizerClient.prompt() .user(prompt) .call() .content(); } }6.2 对话索引与搜索Service public class ConversationSearchService { private final ElasticsearchTemplate elasticsearchTemplate; public void indexConversation(String conversationId, ListMessage messages) { ConversationDocument doc new ConversationDocument(); doc.setConversationId(conversationId); doc.setContent(messages.stream() .map(Message::getContent) .collect(Collectors.joining(\n))); doc.setTimestamp(Instant.now()); elasticsearchTemplate.save(doc); } public ListString searchConversations(String keyword, int topK) { NativeSearchQuery query NativeSearchQuery.builder() .withQuery(QueryBuilders.matchQuery(content, keyword)) .withPageable(PageRequest.of(0, topK)) .build(); return elasticsearchTemplate.search(query, ConversationDocument.class) .stream() .map(hit - hit.getContent().getConversationId()) .collect(Collectors.toList()); } }6.3 对话导出与导入Service public class ConversationExportService { public String exportConversation(String conversationId, String format) { ListMessage messages chatMemory.get(conversationId, Integer.MAX_VALUE); return switch (format.toLowerCase()) { case json - exportToJson(messages); case markdown - exportToMarkdown(messages); case txt - exportToText(messages); default - throw new IllegalArgumentException(Unsupported format: format); }; } private String exportToMarkdown(ListMessage messages) { StringBuilder sb new StringBuilder(); sb.append(# 对话记录\n\n); for (Message msg : messages) { String role msg.getMessageType() MessageType.USER ? 用户 : AI; sb.append(## %s\n\n%s\n\n, role, msg.getContent()); } return sb.toString(); } public void importConversation(String conversationId, String jsonData) { ListMessage messages parseMessagesFromJson(jsonData); chatMemory.add(conversationId, messages); } }七、最佳实践7.1 性能优化批量操作使用批量插入减少数据库调用异步持久化使用消息队列异步处理持久化缓存策略对热点对话使用内存缓存分页加载避免一次性加载过多历史消息7.2 数据安全敏感信息过滤持久化前过滤敏感数据数据加密对存储的对话内容加密访问控制实现基于角色的数据访问控制数据脱敏导出时对敏感信息脱敏处理7.3 监控与告警Component public class ChatMemoryMonitor { Scheduled(fixedRate 60000) public void monitorMemoryUsage() { long totalMessages repository.count(); long activeConversations repository.countActiveConversations(); if (totalMessages WARNING_THRESHOLD) { alertService.sendAlert(对话消息数量超过阈值: totalMessages); } metrics.gauge(chat.memory.total.messages, totalMessages); metrics.gauge(chat.memory.active.conversations, activeConversations); } }八、总结Spring AI 提供了灵活而强大的对话持久化机制多种存储方案支持内存、Redis、数据库等多种存储方式工具调用追踪完整记录 AI 的工具调用历史高度可定制可以轻松实现自定义的持久化策略企业级特性支持多租户、分层存储、数据安全等企业需求通过合理使用这些功能可以构建出功能完善、性能优秀的 AI 对话应用。参考资源Spring AI 官方文档Spring AI GitHub 仓库Chat Memory 设计模式