跳到主要内容

对话记忆

大语言模型是无状态的,无法记住之前的对话内容。对话记忆(Chat Memory)解决了这个问题,让 AI 能够保持上下文连贯性。本章介绍 Spring AI 中的对话记忆功能。

为什么需要对话记忆?

┌─────────────────────────────────────────────────────────────┐
│ 无状态 vs 有状态 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 无状态对话(默认): │
│ 用户: "我叫张三" │
│ AI: "你好张三!" │
│ 用户: "我叫什么名字?" │
│ AI: "抱歉,我不知道您的名字。" ❌ │
│ │
│ 有状态对话(使用记忆): │
│ 用户: "我叫张三" │
│ AI: "你好张三!" │
│ 用户: "我叫什么名字?" │
│ AI: "您说您叫张三。" ✅ │
│ │
└─────────────────────────────────────────────────────────────┘

ChatMemory 接口

Spring AI 提供了 ChatMemory 抽象接口:

public interface ChatMemory {

// 添加消息到会话
void add(String conversationId, List<Message> messages);

// 获取会话的消息列表
List<Message> get(String conversationId, int lastN);

// 清除会话历史
void clear(String conversationId);
}

内置实现

Spring AI 提供了多种 ChatMemory 实现:

实现说明适用场景
InMemoryChatMemoryRepository内存存储开发测试
JdbcChatMemoryRepository关系数据库生产环境
CassandraChatMemoryRepositoryCassandra大规模分布式
Neo4jChatMemoryRepositoryNeo4j 图数据库复杂关系查询
MongoChatMemoryRepositoryMongoDB文档存储
CosmosDBChatMemoryRepositoryAzure Cosmos DB云原生应用

快速开始

使用默认配置

Spring AI 自动配置了一个基于内存的 ChatMemory

@Service
public class ChatService {

@Autowired
private ChatMemory chatMemory;

public String chat(String conversationId, String userMessage) {
// 添加用户消息
chatMemory.add(conversationId, List.of(new UserMessage(userMessage)));

// 获取完整的对话历史
List<Message> history = chatMemory.get(conversationId);

// 调用模型...
return "response";
}
}

使用 MessageChatMemoryAdvisor

推荐的方式是使用 Advisor 来自动管理对话记忆:

@Configuration
public class ChatMemoryConfig {

@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20) // 保留最近20条消息
.build();
}

@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
return builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
}

控制器示例

@RestController
@RequestMapping("/api/chat")
public class ChatController {

private final ChatClient chatClient;

public ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
.defaultSystem("你是一个有帮助的助手。")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}

@PostMapping
public String chat(@RequestBody ChatRequest request) {
return chatClient.prompt()
.user(request.message())
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, request.conversationId())
)
.call()
.content();
}

@DeleteMapping("/{conversationId}")
public void clearHistory(@PathVariable String conversationId) {
// 清除对话历史
chatMemory.clear(conversationId);
}
}

record ChatRequest(String conversationId, String message) {}

存储实现

1. 内存存储

适合开发和测试:

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

2. JDBC 存储

适合生产环境,支持多种数据库:

添加依赖:

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>

配置:

spring:
datasource:
url: jdbc:postgresql://localhost:5432/chat_memory
username: postgres
password: postgres
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always

使用:

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

支持的数据库:

数据库方言类
PostgreSQLPostgresChatMemoryRepositoryDialect
MySQL / MariaDBMySqlChatMemoryRepositoryDialect
SQL ServerSqlServerChatMemoryRepositoryDialect
OracleOracleChatMemoryRepositoryDialect
HSQLDBHsqldbChatMemoryRepositoryDialect

3. MongoDB 存储

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-mongodb</artifactId>
</dependency>
spring:
data:
mongodb:
uri: mongodb://localhost:27017/chat_memory
ai:
chat:
memory:
repository:
mongodb:
initialize-schema: true
ttl: 604800 # 7天过期(秒)

4. Redis 存储

@Bean
public ChatMemory chatMemory(RedisTemplate<String, Object> redisTemplate) {
// 使用 Redis 存储
RedisChatMemoryRepository repository = RedisChatMemoryRepository.builder()
.redisTemplate(redisTemplate)
.ttl(Duration.ofHours(24))
.build();

return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20)
.build();
}

Advisor 类型

Spring AI 提供了多种记忆 Advisor:

1. MessageChatMemoryAdvisor

将对话历史作为消息列表添加到提示词中:

ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();

2. PromptChatMemoryAdvisor

将对话历史以文本形式追加到系统提示词中:

ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
PromptChatMemoryAdvisor.builder(chatMemory)
.promptTemplate(customTemplate) // 可选自定义模板
.build()
)
.build();

3. VectorStoreChatMemoryAdvisor

使用向量存储来检索相关的历史对话:

ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
VectorStoreChatMemoryAdvisor.builder(vectorStore)
.topK(5) // 检索最相关的5条历史
.build()
)
.build();

高级配置

自定义消息窗口大小

@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(30) // 保留最近30条消息
.build();
}

自定义模板

String customTemplate = """
以下是对话的历史记录:

{memory}

请根据以上历史继续对话。
""";

PromptTemplate template = new PromptTemplate(customTemplate);

PromptChatMemoryAdvisor advisor = PromptChatMemoryAdvisor.builder(chatMemory)
.promptTemplate(template)
.build();

会话隔离

@Service
public class MultiUserService {

private final ChatClient chatClient;
private final ChatMemory chatMemory;

public String chat(String userId, String sessionId, String message) {
// 使用复合键隔离不同用户的会话
String conversationId = userId + ":" + sessionId;

return chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, conversationId)
)
.call()
.content();
}

public void clearUserHistory(String userId) {
// 清除用户所有会话
// 需要根据存储实现来实现清除逻辑
}
}

实际应用示例

1. 多轮对话服务

@Service
public class ConversationService {

private final ChatClient chatClient;
private final ChatMemory chatMemory;

public ConversationService(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatMemory = chatMemory;
this.chatClient = builder
.defaultSystem("""
你是一个专业的客服助手。
请记住用户之前提供的信息,保持对话连贯。
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory)
.build()
)
.build();
}

public ConversationResponse chat(String sessionId, String message) {
String response = chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, sessionId)
)
.call()
.content();

return new ConversationResponse(
response,
getMessageCount(sessionId)
);
}

public void resetSession(String sessionId) {
chatMemory.clear(sessionId);
}

public int getMessageCount(String sessionId) {
return chatMemory.get(sessionId).size();
}

record ConversationResponse(String response, int messageCount) {}
}

2. 用户偏好记忆

@Service
public class UserPreferenceService {

private final ChatClient chatClient;
private final ChatMemory chatMemory;

public UserPreferenceService(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatMemory = chatMemory;
this.chatClient = builder
.defaultSystem("""
你是一个个性化助手。
记住用户的偏好设置,如语言、风格等。
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}

public String chatWithPreference(String userId, String message) {
return chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, "user:" + userId)
)
.call()
.content();
}

public void setPreference(String userId, String key, String value) {
// 将偏好设置存储为系统消息
String preferenceMessage = String.format("用户偏好设置:%s = %s", key, value);
chatMemory.add("user:" + userId, List.of(new SystemMessage(preferenceMessage)));
}
}

3. 会话管理 API

@RestController
@RequestMapping("/api/conversations")
public class ConversationController {

private final ConversationService conversationService;
private final ChatMemory chatMemory;

@PostMapping("/{sessionId}/message")
public ConversationResponse sendMessage(
@PathVariable String sessionId,
@RequestBody MessageRequest request) {
return conversationService.chat(sessionId, request.message());
}

@GetMapping("/{sessionId}/history")
public List<MessageInfo> getHistory(@PathVariable String sessionId) {
return chatMemory.get(sessionId).stream()
.map(msg -> new MessageInfo(
msg.getMessageType().name(),
msg.getContent(),
msg.getMetadata()
))
.toList();
}

@DeleteMapping("/{sessionId}")
public void clearSession(@PathVariable String sessionId) {
chatMemory.clear(sessionId);
}

@GetMapping("/{sessionId}/stats")
public SessionStats getStats(@PathVariable String sessionId) {
List<Message> messages = chatMemory.get(sessionId);
return new SessionStats(
messages.size(),
messages.stream()
.filter(m -> m.getMessageType() == MessageType.USER)
.count(),
messages.stream()
.filter(m -> m.getMessageType() == MessageType.ASSISTANT)
.count()
);
}
}

record MessageRequest(String message) {}
record MessageInfo(String type, String content, Map<String, Object> metadata) {}
record SessionStats(long totalMessages, long userMessages, long assistantMessages) {}

4. 流式对话

@GetMapping(value = "/{sessionId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(
@PathVariable String sessionId,
@RequestParam String message) {

return chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, sessionId)
)
.stream()
.content();
}

记忆策略

1. 滑动窗口

保留最近的 N 条消息:

MessageWindowChatMemory.builder()
.maxMessages(20) // 保留最近20条
.build();

2. 基于 Token 限制

根据 Token 数量限制:

// 可以通过计算消息的 Token 数来决定保留多少
// 目前 Spring AI 主要使用消息数量限制

3. 重要性过滤

保留重要的消息:

// 可以自定义实现,根据消息重要性过滤
public class ImportanceFilteringChatMemory implements ChatMemory {
// 实现基于重要性的过滤逻辑
}

最佳实践

1. 合理设置窗口大小

// 简单对话:10-20 条消息
MessageWindowChatMemory.builder().maxMessages(15).build();

// 复杂任务:20-50 条消息
MessageWindowChatMemory.builder().maxMessages(30).build();

// 长对话场景:考虑使用 VectorStoreChatMemoryAdvisor

2. 会话 ID 设计

// 用户级别
String userSession = "user:" + userId;

// 会话级别
String conversationSession = "user:" + userId + ":conv:" + conversationId;

// 租户级别
String tenantSession = "tenant:" + tenantId + ":user:" + userId;

3. 定期清理

@Scheduled(cron = "0 0 3 * * ?")  // 每天凌晨3点
public void cleanupOldSessions() {
// 清理过期的会话
// 具体实现取决于使用的存储
}

小结

本章我们学习了:

  1. 对话记忆概念:解决 LLM 无状态问题
  2. ChatMemory 接口:统一的记忆抽象
  3. 存储实现:内存、JDBC、MongoDB 等
  4. Advisor 类型:消息、提示词、向量存储
  5. 实际应用:多轮对话、用户偏好、会话管理
  6. 最佳实践:窗口大小、会话设计、定期清理

练习

  1. 实现一个支持多用户的聊天服务
  2. 使用 JDBC 存储创建持久化对话历史
  3. 实现会话导出和恢复功能

参考资源