大模型对话中“记忆”系统的设计与实现可以说是构建真正智能、连贯、个性化的对话系统的核心挑战之一。

我们将从概念、作用、实现策略,再到具体的代码实现,进行层层深入的剖析。

1. 记忆的概念与作用分析

在人类对话中,记忆是自然而然的。我们记得刚才说过的话(短期记忆),也记得对方的姓名、喜好和之前的讨论主题(长期记忆)。大模型本身是“无状态”的,每次调用都像一张白纸。因此,我们必须人为地为它设计记忆系统。

短期记忆
  • 作用

    1. 维持对话连贯性:记住当前对话轮次中的上下文,确保模型能理解指代(如“它”、“那个方法”)、跟上话题的转折。
    2. 执行复杂任务的基础:在链式思考或工具调用中,上一步的结果是下一步的输入,这些都存储在短期记忆中。
    3. 控制对话长度:防止输入上下文无限增长,通过滑动窗口或总结来保持核心信息。
  • 本质:通常直接体现在模型的输入上下文中。即,我们将最近的几轮对话(User-Assistant对)直接拼接起来,送给模型。

长期记忆
  • 作用

    1. 个性化:记住用户的偏好、身份信息(如“我对花生过敏”、“我是Java开发者”),使对话体验独一无二。
    2. 知识持久化:存储从对话中提取的结构化事实(如“用户的项目是XX系统,使用SpringBoot”),避免用户重复陈述。
    3. 跨会话关联:在用户多次访问中保持信息的连续性,实现真正的“认识你”的效果。
    4. 减少冗余:无需在每次对话中重新介绍背景信息。
  • 本质:是一个独立于模型对话之外的、可读写的外部存储系统(如数据库、向量库)。它不是直接拼接到上下文里,而是在需要时被“回忆”并注入到当前上下文中。


2. 实现策略与架构

一个完整的记忆系统是短期与长期记忆的有机结合。

短期记忆的实现

实现相对直接,主要是一个“上下文窗口管理器”。

  1. 数据结构:使用一个双向队列或列表来保存对话消息 List<Message>
  2. 消息格式:每条消息通常包含角色(user, assistant, system)和内容。
  3. 管理策略
    • 固定窗口:保留最新的N条消息或N个Token。当超过限制时,丢弃最老的消息。
    • 滑动窗口与总结:一种更高级的策略。当窗口快满时,用一个单独的LLM调用将窗口中部不那么重要的对话总结成一条摘要,只保留最新的和最重要的消息。这能在有限上下文内保留更多“精髓”。
长期记忆的实现

这是一个更复杂的系统,通常包含两个核心组件:记忆写入器记忆读取器

  1. 记忆的存储与写入

    • 触发时机:何时创建一条长期记忆?
      • 显式指令:用户说“请记住我喜欢蓝色”。
      • 隐式提取:模型在对话中自动识别出关键事实(如用户的工作、项目名),并决定存入记忆。
    • 存储格式
      • 结构化:存入关系型数据库。表结构如 (user_id, key, value, timestamp)。例如 (”user123″, “favorite_color”, “blue”, “2023-10-27”)。适合存储明确的键值对信息。
      • 非结构化/向量化:将对话片段或提取的事实转换成向量嵌入,存入向量数据库(如Milvus, Pinecone)。这使得我们可以进行语义搜索。当用户问“我之前跟你提过什么关于颜色的事?”,即使他没说“蓝色”,系统也能通过向量相似度找到相关记忆。
  2. 记忆的读取与召回

    • 触发时机:在每次构造对话上下文之前
    • 召回策略
      • 基于查询:分析用户当前的问题,生成一个搜索查询去长期记忆库中查找。
      • 全量/最近记忆:在对话开始时,直接加载用户最近或最重要的几条记忆,作为系统背景信息。
    • 注入方式:将召回的记忆,以system消息或特殊格式的user消息,拼接到短期记忆上下文的最前面。

3. 关键部分Java源码实现

下面我们用一个简化的、模块化的Java实现来展示核心逻辑。我们将使用内存存储和简单的策略,但架构易于扩展为使用Redis、MySQL或向量数据库。

1. 定义核心数据模型
// 表示一条对话消息
public class Message {
    public enum Role {
        USER, ASSISTANT, SYSTEM
    }
    private Role role;
    private String content;
    // ... Constructors, Getters, Setters
}

// 表示一条长期记忆
public class Memory {
    private String userId;
    private String key; // e.g., "dietary_restriction"
    private String value; // e.g., "allergic to peanuts"
    private Instant timestamp;
    // ... Constructors, Getters, Setters
}
2. 短期记忆管理器的实现
import java.util.*;

public class ShortTermMemoryManager {
    private final int maxTokens; // 或 maxMessages
    private final LinkedList<Message> conversationHistory;
    private final Tokenizer tokenizer; // 假设有一个Token计算工具

    public ShortTermMemoryManager(int maxTokens) {
        this.maxTokens = maxTokens;
        this.conversationHistory = new LinkedList<>();
    }

    public void addMessage(Message message) {
        conversationHistory.add(message);
        truncateHistory();
    }

    public List<Message> getContext() {
        return new ArrayList<>(conversationHistory);
    }

    private void truncateHistory() {
        int totalTokens = calculateTotalTokens(conversationHistory);
        while (totalTokens > maxTokens && !conversationHistory.isEmpty()) {
            // 移除最老的一条消息(通常是USER-ASSISTANT一对中的一条,需要更精细的策略)
            Message removed = conversationHistory.removeFirst();
            totalTokens = calculateTotalTokens(conversationHistory);
            System.out.println("警告:上下文过长,移除最早的消息: " + removed.getContent().substring(0, Math.min(20, removed.getContent().length())));
        }
    }

    private int calculateTotalTokens(List<Message> messages) {
        // 简化实现,实际中需要使用与模型对应的Tokenizer
        return messages.stream().mapToInt(m -> m.getContent().split("\\s+").length).sum();
    }
}
3. 长期记忆管理器的实现

这里我们实现一个基于内存Map的简单版本,并模拟向量搜索。

import java.util.*;
import java.util.stream.Collectors;

public class LongTermMemoryManager {
    // In-Memory存储。实际应用中替换为DB。
    private Map<String, List<Memory>> userMemories = new HashMap<>();
    // 模拟一个向量存储。Key: userId, Value: (memoryText, embeddingVector)
    private Map<String, List<Pair<String, float[]>>> vectorMemoryStore = new HashMap<>();

    // --- 结构化记忆 CRUD ---
    public void saveMemory(String userId, String key, String value) {
        Memory memory = new Memory(userId, key, value, Instant.now());
        userMemories.computeIfAbsent(userId, k -> new ArrayList<>()).add(memory);
        // 同时可以存入向量库,以便语义搜索
        saveToVectorStore(userId, key + ": " + value);
    }

    public List<Memory> getMemoryByKey(String userId, String key) {
        List<Memory> memories = userMemories.getOrDefault(userId, new ArrayList<>());
        return memories.stream()
                .filter(m -> m.getKey().equalsIgnoreCase(key))
                .sorted(Comparator.comparing(Memory::getTimestamp).reversed())
                .collect(Collectors.toList());
    }

    // --- 基于向量的语义搜索 ---
    private void saveToVectorStore(String userId, String memoryText) {
        float[] embedding = generateEmbedding(memoryText); // 调用Embedding模型生成向量
        vectorMemoryStore.computeIfAbsent(userId, k -> new ArrayList<>()).add(new Pair<>(memoryText, embedding));
    }

    public List<String> searchMemoriesBySemantics(String userId, String query, int topK) {
        List<Pair<String, float[]>> userVectors = vectorMemoryStore.getOrDefault(userId, new ArrayList<>());
        if (userVectors.isEmpty()) return new ArrayList<>();

        float[] queryEmbedding = generateEmbedding(query);

        // 计算余弦相似度并排序
        return userVectors.stream()
                .map(pair -> new AbstractMap.SimpleEntry<>(pair.getKey(), cosineSimilarity(queryEmbedding, pair.getValue())))
                .sorted((e1, e2) -> Float.compare(e2.getValue(), e1.getValue())) // 降序
                .limit(topK)
                .map(AbstractMap.SimpleEntry::getKey)
                .collect(Collectors.toList());
    }

    // --- 模拟Embedding生成和相似度计算 ---
    private float[] generateEmbedding(String text) {
        // 模拟:真实环境中调用OpenAI / SentenceTransformer等API
        // 这里返回一个随机向量用于演示
        float[] embedding = new float[384]; // 假设维度384
        Random rand = new Random();
        for (int i = 0; i < embedding.length; i++) {
            embedding[i] = rand.nextFloat();
        }
        return embedding;
    }

    private float cosineSimilarity(float[] vecA, float[] vecB) {
        // 简化实现,省略了归一化等步骤
        float dotProduct = 0.0f;
        float normA = 0.0f;
        float normB = 0.0f;
        for (int i = 0; i < vecA.length; i++) {
            dotProduct += vecA[i] * vecB[i];
            normA += vecA[i] * vecA[i];
            normB += vecB[i] * vecB[i];
        }
        return (float) (dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)));
    }

    // 辅助类
    private static class Pair<K, V> {
        private K key;
        private V value;
        // ... Constructors, Getters
    }
}
4. 记忆控制器的整合

这是整个系统的“大脑”,负责协调短期和长期记忆。

public class MemoryAwareDialogueController {
    private ShortTermMemoryManager shortTermMemory;
    private LongTermMemoryManager longTermMemory;
    private LLMClient llmClient; // 假设的大模型客户端

    public MemoryAwareDialogueController(String userId) {
        this.shortTermMemory = new ShortTermMemoryManager(2048); // 假设2K Token限制
        this.longTermMemory = new LongTermMemoryManager();
        // 初始化时,可以加载一些长期记忆作为背景
        loadRelevantLongTermMemories(userId, "Initial context");
    }

    public String processUserInput(String userId, String userInput) {
        // 1. 在调用模型前,先根据用户输入召回相关的长期记忆
        List<String> relevantMemories = longTermMemory.searchMemoriesBySemantics(userId, userInput, 3);

        // 2. 将长期记忆作为系统上下文注入
        String memoryContext = "Here is some relevant information about the user from previous conversations:\n"
                + String.join("\n", relevantMemories);
        Message memoryMessage = new Message(Message.Role.SYSTEM, memoryContext);
        
        // 3. 将用户当前输入转为消息
        Message userMessage = new Message(Message.Role.USER, userInput);

        // 4. 获取当前的短期记忆上下文
        List<Message> context = shortTermMemory.getContext();

        // 5. 构建最终的模型输入: [长期记忆] + [短期记忆] + [最新用户输入]
        List<Message> fullContext = new ArrayList<>();
        fullContext.add(memoryMessage); // 注入长期记忆
        fullContext.addAll(context);     // 添加上文
        fullContext.add(userMessage);    // 添加最新输入

        // 6. 调用大模型
        String assistantResponse = llmClient.generateResponse(fullContext);

        // 7. 更新短期记忆
        shortTermMemory.addMessage(userMessage);
        shortTermMemory.addMessage(new Message(Message.Role.ASSISTANT, assistantResponse));

        // 8. (可选)从对话中提取关键信息并存入长期记忆
        // 例如,如果用户说“我的名字是Alice”,可以在这里调用一个信息提取函数
        // extractAndSaveMemory(userId, userInput, assistantResponse);

        return assistantResponse;
    }

    private void loadRelevantLongTermMemories(String userId, String query) {
        // 在对话开始时预加载一些记忆
        // 例如,加载最近创建的5条记忆,或者关于“偏好”的记忆
        List<Memory> recentPrefs = longTermMemory.getMemoryByKey(userId, "preference");
        // ... 可以将其注入初始上下文
    }

    private void extractAndSaveMemory(String userId, String userInput, String assistantResponse) {
        // 使用一个LLM来分析和提取用户输入中值得长期记忆的事实。
        // 这是一个简化示例,逻辑需要根据实际情况设计。
        if (userInput.toLowerCase().contains("my name is")) {
            String name = userInput.substring(userInput.toLowerCase().indexOf("my name is") + "my name is".length()).trim();
            longTermMemory.saveMemory(userId, "user_name", name);
        }
        if (userInput.toLowerCase().contains("i like") || userInput.toLowerCase().contains("i love")) {
            // ... 提取爱好
        }
    }
}

总结与展望

通过上述分析和代码,我们可以看到:

  • 短期记忆是对话的“工作台”,通过管理上下文窗口实现,技术相对成熟。
  • 长期记忆是对话的“知识库”,其实现更复杂,涉及记忆的识别、存储、检索和融合。向量数据库的引入是实现强大语义搜索的关键。
  • 两者的协同:一个优秀的对话系统,在每次交互中,都会动态地将相关的长期记忆“注入”到短期记忆的上下文中,从而让模型在拥有丰富背景信息的情况下进行回复。

未来更高级的实现可能包括

  • 记忆摘要与压缩:自动将多轮对话或大量记忆总结成简洁的要点。
  • 记忆权重与衰减:根据使用频率和新近度对记忆排序,不重要的记忆逐渐“遗忘”。
  • 主动记忆:模型主动询问以获取缺失的关键信息(如“为了更好地帮助您,可以告诉我您的职业吗?”)。

构建具有真正记忆能力的智能对话Agent打下坚实的基础,往期的习惯及历史内容会成为未来对话提供建设性参考和智能性。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐