Spring AI多轮对话:ChatMemory原理与生产级会话管理
2026/4/30大约 4 分钟
Spring AI多轮对话:ChatMemory原理与生产级会话管理
适读人群:实现AI对话功能的Java工程师
文章价值:ChatMemory完整实现 + 会话策略对比 + 多用户隔离方案
为什么多轮对话不是"加个列表"那么简单?
初学者常犯的错误:把所有历史消息都塞进Prompt,结果:
- Token爆炸:历史越长,费用越高,速度越慢
- 上下文污染:早期无关对话影响当前回答质量
- 会话混乱:多用户消息混在一起
生产级多轮对话需要解决:会话隔离、上下文窗口管理、持久化存储、过期清理。
ChatMemory架构设计
四种内存策略对比
完整实现代码
基础会话服务
@Service
@RequiredArgsConstructor
public class ConversationService {
private final ChatClient.Builder chatClientBuilder;
private final ChatMemoryRepository memoryRepository;
// 每个会话ID对应独立的ChatClient实例(实际上可复用ChatClient)
public String chat(String sessionId, String userMessage) {
// 创建消息窗口Memory(保留最近20条消息)
ChatMemory memory = MessageWindowChatMemory.builder()
.chatMemoryRepository(memoryRepository)
.conversationId(sessionId)
.maxMessages(20)
.build();
ChatClient client = chatClientBuilder
.defaultSystem("你是一位专业的Java技术助手")
.defaultAdvisors(
new MessageChatMemoryAdvisor(memory)
)
.build();
return client.prompt()
.user(userMessage)
.call()
.content();
}
// 获取会话历史
public List<Message> getHistory(String sessionId) {
return memoryRepository.findById(sessionId)
.orElse(Collections.emptyList());
}
// 清除会话
public void clearSession(String sessionId) {
memoryRepository.deleteById(sessionId);
}
}Redis持久化Memory
@Configuration
public class RedisChatMemoryConfig {
@Bean
public ChatMemoryRepository redisChatMemoryRepository(RedisTemplate<String, Object> redisTemplate) {
return new RedisChatMemoryRepository(redisTemplate, Duration.ofDays(7)); // 7天过期
}
}
// 自定义Redis实现
@Component
public class RedisChatMemoryRepository implements ChatMemoryRepository {
private final RedisTemplate<String, Object> redisTemplate;
private final Duration ttl;
private static final String KEY_PREFIX = "chat:memory:";
@Override
public void save(String conversationId, List<Message> messages) {
String key = KEY_PREFIX + conversationId;
redisTemplate.opsForValue().set(key, messages, ttl);
}
@Override
public Optional<List<Message>> findById(String conversationId) {
String key = KEY_PREFIX + conversationId;
Object cached = redisTemplate.opsForValue().get(key);
if (cached == null) return Optional.empty();
return Optional.of((List<Message>) cached);
}
@Override
public void deleteById(String conversationId) {
redisTemplate.delete(KEY_PREFIX + conversationId);
}
}多用户会话隔离(Controller层)
@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
public class ChatController {
private final ConversationService conversationService;
@PostMapping("/message")
public ChatResponse sendMessage(
@RequestBody ChatRequest request,
@AuthenticationPrincipal UserDetails user
) {
// 会话ID = 用户ID + 业务场景 + 自定义ID
String sessionId = buildSessionId(user.getUsername(), request.getScenario(), request.getSessionId());
String response = conversationService.chat(sessionId, request.getMessage());
return ChatResponse.builder()
.message(response)
.sessionId(sessionId)
.build();
}
private String buildSessionId(String userId, String scenario, String customId) {
return String.format("%s:%s:%s", userId, scenario,
customId != null ? customId : UUID.randomUUID().toString());
}
@GetMapping("/history/{sessionId}")
public List<MessageDTO> getHistory(@PathVariable String sessionId,
@AuthenticationPrincipal UserDetails user) {
// 验证session归属
validateSessionOwnership(sessionId, user.getUsername());
return conversationService.getHistory(sessionId)
.stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
}会话摘要:突破Token限制
对于长对话场景,可以定期将历史对话压缩为摘要:
@Service
@RequiredArgsConstructor
public class SummaryMemoryService {
private final ChatClient chatClient;
private final ChatMemoryRepository memoryRepository;
private static final int SUMMARY_THRESHOLD = 30; // 超过30条时触发摘要
public String chatWithSummary(String sessionId, String userMessage) {
List<Message> history = memoryRepository.findById(sessionId)
.orElse(new ArrayList<>());
// 触发摘要压缩
if (history.size() > SUMMARY_THRESHOLD) {
String summary = summarizeHistory(history.subList(0, history.size() - 10));
// 保留摘要 + 最近10条消息
List<Message> compressed = new ArrayList<>();
compressed.add(new SystemMessage("历史对话摘要:" + summary));
compressed.addAll(history.subList(history.size() - 10, history.size()));
memoryRepository.save(sessionId, compressed);
}
// 正常对话逻辑
return conversationService.chat(sessionId, userMessage);
}
private String summarizeHistory(List<Message> messages) {
String historyText = messages.stream()
.map(m -> m.getMessageType() + ": " + m.getContent())
.collect(Collectors.joining("\n"));
return chatClient.prompt()
.user("请将以下对话历史压缩为200字以内的摘要,保留关键信息:\n" + historyText)
.call()
.content();
}
}