Chat Memory 对话记忆
多轮对话是 AI 应用的基础能力,Chat Memory 解决了 LLM 无状态的本质问题,让 AI 真正"记住"对话上下文。
为什么需要 Chat Memory?
LLM 本质上是无状态的——每次调用都是独立的,它不会自动记住上一轮说了什么:
❌ 没有 Memory 的对话:
用户:我叫张三
AI:你好,张三!
用户:我叫什么名字?
AI:我不知道你的名字。(忘了!)
✅ 有 Memory 的对话:
用户:我叫张三
AI:你好,张三!
用户:我叫什么名字?
AI:你叫张三。实现原理很简单:每次请求时,将历史对话注入到 Prompt 中。
第 N 轮请求的 Prompt:
SystemMessage: "你是一个助手"
UserMessage: "我叫张三" ← 第 1 轮历史
AssistantMessage: "你好,张三!" ← 第 1 轮历史
UserMessage: "我叫什么名字?" ← 当前问题ChatMemory 接口
java
public interface ChatMemory {
// 添加消息到会话
void add(String conversationId, List<Message> messages);
// 获取会话历史
List<Message> get(String conversationId, int lastN);
// 清除会话
void clear(String conversationId);
}内置存储实现
InMemoryChatMemory(开发/测试)
java
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
// 内存存储,应用重启后丢失
// 适合:开发测试、单机部署、无持久化需求
}JDBC ChatMemory(关系型数据库)
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-jdbc-store-spring-boot-autoconfigure</artifactId>
</dependency>yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/ai_db
username: root
password: password
ai:
chat:
memory:
jdbc:
initialize-schema: always # 自动建表java
@Bean
public ChatMemory chatMemory(JdbcTemplate jdbcTemplate) {
return new JdbcChatMemory(jdbcTemplate);
// 持久化存储,重启不丢失
// 适合:生产环境、需要审计历史对话
}自动创建的表结构:
sql
CREATE TABLE spring_ai_chat_memory (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
conversation_id VARCHAR(256) NOT NULL,
content TEXT NOT NULL,
type VARCHAR(64) NOT NULL, -- USER / ASSISTANT / SYSTEM / TOOL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conversation_id (conversation_id)
);Redis ChatMemory(高性能)
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store-spring-boot-autoconfigure</artifactId>
</dependency>java
@Bean
public ChatMemory chatMemory(RedisTemplate<String, Object> redisTemplate) {
return new RedisChatMemory(
redisTemplate,
Duration.ofHours(24) // TTL:24 小时后自动过期
);
// 适合:高并发、需要 TTL 自动清理
}Advisor 集成
MessageChatMemoryAdvisor
最常用的记忆 Advisor,自动注入和保存对话历史:
java
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
ChatMemory chatMemory
) {
return builder
.defaultAdvisors(
new MessageChatMemoryAdvisor(
chatMemory,
"default-session", // 默认会话 ID
20 // 保留最近 20 条消息
)
)
.build();
}多用户会话隔离
java
@RestController
public class ChatController {
@Autowired
private ChatClient chatClient;
@GetMapping("/chat")
public String chat(
@RequestParam String message,
@RequestHeader("X-Session-Id") String sessionId // 每个用户独立会话
) {
return chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId)
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 15)
)
.call()
.content();
}
// 清除会话历史
@DeleteMapping("/chat/{sessionId}")
public void clearSession(@PathVariable String sessionId) {
chatMemory.clear(sessionId);
}
}Token 窗口管理
随着对话增长,历史消息会超出模型的上下文窗口限制,需要策略性地管理:
策略一:固定窗口(最近 N 条)
java
// 只保留最近 10 条消息
new MessageChatMemoryAdvisor(chatMemory, sessionId, 10)策略二:Token 预算控制
java
@Service
public class TokenAwareChatService {
private static final int MAX_HISTORY_TOKENS = 2000;
@Autowired
private ChatMemory chatMemory;
@Autowired
private ChatClient chatClient;
public String chat(String message, String sessionId) {
// 获取历史消息
List<Message> history = chatMemory.get(sessionId, 50);
// 按 Token 预算截断(从最新消息开始保留)
List<Message> trimmedHistory = trimByTokenBudget(history, MAX_HISTORY_TOKENS);
// 手动构建 Prompt
List<Message> messages = new ArrayList<>(trimmedHistory);
messages.add(new UserMessage(message));
return chatClient.prompt(new Prompt(messages))
.call()
.content();
}
private List<Message> trimByTokenBudget(List<Message> messages, int maxTokens) {
// 简单估算:1 token ≈ 4 字符(中文约 2 字符/token)
int totalTokens = 0;
List<Message> result = new ArrayList<>();
for (int i = messages.size() - 1; i >= 0; i--) {
int tokens = messages.get(i).getContent().length() / 3;
if (totalTokens + tokens > maxTokens) break;
totalTokens += tokens;
result.add(0, messages.get(i));
}
return result;
}
}策略三:摘要压缩
java
@Service
public class SummarizingMemoryService {
@Autowired
private ChatClient chatClient;
@Autowired
private ChatMemory chatMemory;
private static final int SUMMARIZE_THRESHOLD = 20; // 超过 20 条时压缩
public String chat(String message, String sessionId) {
List<Message> history = chatMemory.get(sessionId, 100);
if (history.size() > SUMMARIZE_THRESHOLD) {
// 将旧消息压缩为摘要
List<Message> oldMessages = history.subList(0, history.size() - 10);
String summary = summarize(oldMessages);
// 清除旧历史,用摘要替代
chatMemory.clear(sessionId);
chatMemory.add(sessionId, List.of(
new SystemMessage("以下是之前对话的摘要:\n" + summary)
));
// 保留最近 10 条
chatMemory.add(sessionId, history.subList(history.size() - 10, history.size()));
}
return chatClient.prompt()
.user(message)
.advisors(new MessageChatMemoryAdvisor(chatMemory, sessionId, 15))
.call()
.content();
}
private String summarize(List<Message> messages) {
String conversation = messages.stream()
.map(m -> m.getMessageType() + ": " + m.getContent())
.collect(Collectors.joining("\n"));
return chatClient.prompt()
.system("请将以下对话压缩为简洁的摘要,保留关键信息:")
.user(conversation)
.call()
.content();
}
}VectorStoreChatMemoryAdvisor
基于向量检索的长期记忆,适合需要跨会话记忆的场景:
java
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
VectorStore vectorStore,
ChatMemory shortTermMemory
) {
return builder
.defaultAdvisors(
// 短期记忆:最近 10 条消息
new MessageChatMemoryAdvisor(shortTermMemory, sessionId, 10),
// 长期记忆:从向量库检索相关历史
new VectorStoreChatMemoryAdvisor(
vectorStore,
sessionId,
5 // 检索最相关的 5 条历史
)
)
.build();
}实战:客服系统对话管理
java
@Service
public class CustomerServiceMemory {
@Autowired
private ChatClient chatClient;
@Autowired
private ChatMemory chatMemory;
@Autowired
private CustomerRepository customerRepo;
public String handleMessage(String customerId, String message) {
// 加载客户信息作为系统上下文
Customer customer = customerRepo.findById(customerId).orElseThrow();
return chatClient.prompt()
.system("""
你是客服助手。当前服务的客户信息:
- 姓名:%s
- 会员等级:%s
- 历史订单数:%d
请根据客户信息提供个性化服务。
""".formatted(
customer.getName(),
customer.getMemberLevel(),
customer.getOrderCount()
))
.user(message)
.advisors(advisor -> advisor
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY,
"customer-" + customerId)
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 20)
)
.call()
.content();
}
// 获取对话历史(用于审计)
public List<Message> getHistory(String customerId) {
return chatMemory.get("customer-" + customerId, 100);
}
// 结束会话(保留记录,标记为已结束)
public void closeSession(String customerId) {
String sessionId = "customer-" + customerId;
List<Message> history = chatMemory.get(sessionId, 100);
// 持久化到业务数据库
conversationLogService.save(customerId, history);
// 清除内存中的会话
chatMemory.clear(sessionId);
}
}最佳实践
记忆管理建议
- 会话 ID 设计:使用
userId + sessionId组合,支持同一用户多个会话 - 窗口大小:一般保留 10-20 条消息,平衡上下文质量和 Token 消耗
- TTL 设置:Redis 存储建议设置 TTL(如 24 小时),自动清理过期会话
- 敏感信息:对话历史可能包含敏感信息,生产环境需要加密存储
- 摘要策略:长对话(>30 轮)建议启用摘要压缩,避免 Token 超限
注意事项
InMemoryChatMemory不适合生产环境(重启丢失、多实例不共享)- 多实例部署必须使用外部存储(Redis / JDBC)
- 对话历史越长,每次请求的 Token 消耗越大,注意成本控制