Skip to content

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);
    }
}

最佳实践

记忆管理建议

  1. 会话 ID 设计:使用 userId + sessionId 组合,支持同一用户多个会话
  2. 窗口大小:一般保留 10-20 条消息,平衡上下文质量和 Token 消耗
  3. TTL 设置:Redis 存储建议设置 TTL(如 24 小时),自动清理过期会话
  4. 敏感信息:对话历史可能包含敏感信息,生产环境需要加密存储
  5. 摘要策略:长对话(>30 轮)建议启用摘要压缩,避免 Token 超限

注意事项

  • InMemoryChatMemory 不适合生产环境(重启丢失、多实例不共享)
  • 多实例部署必须使用外部存储(Redis / JDBC)
  • 对话历史越长,每次请求的 Token 消耗越大,注意成本控制

相关组件

本站内容由 褚成志 整理编写,仅供学习参考