【大模型对话中长期记忆与短期记忆】
摘要:大模型对话系统的“记忆”设计是实现智能、连贯对话的关键挑战。记忆系统分为短期记忆和长期记忆两部分。短期记忆通过上下文窗口管理器维护对话连贯性,控制输入长度;长期记忆则通过外部存储实现个性化、知识持久化和跨会话关联。实现上,短期记忆采用队列管理最新消息,长期记忆涉及记忆写入器和读取器,支持结构化存储和语义搜索。文章提供了Java简化实现,展示核心数据模型、短期记忆管理和长期记忆存储的模块化架构
大模型对话中“记忆”系统的设计与实现可以说是构建真正智能、连贯、个性化的对话系统的核心挑战之一。
我们将从概念、作用、实现策略,再到具体的代码实现,进行层层深入的剖析。
1. 记忆的概念与作用分析
在人类对话中,记忆是自然而然的。我们记得刚才说过的话(短期记忆),也记得对方的姓名、喜好和之前的讨论主题(长期记忆)。大模型本身是“无状态”的,每次调用都像一张白纸。因此,我们必须人为地为它设计记忆系统。
短期记忆
-
作用:
- 维持对话连贯性:记住当前对话轮次中的上下文,确保模型能理解指代(如“它”、“那个方法”)、跟上话题的转折。
- 执行复杂任务的基础:在链式思考或工具调用中,上一步的结果是下一步的输入,这些都存储在短期记忆中。
- 控制对话长度:防止输入上下文无限增长,通过滑动窗口或总结来保持核心信息。
-
本质:通常直接体现在模型的输入上下文中。即,我们将最近的几轮对话(User-Assistant对)直接拼接起来,送给模型。
长期记忆
-
作用:
- 个性化:记住用户的偏好、身份信息(如“我对花生过敏”、“我是Java开发者”),使对话体验独一无二。
- 知识持久化:存储从对话中提取的结构化事实(如“用户的项目是XX系统,使用SpringBoot”),避免用户重复陈述。
- 跨会话关联:在用户多次访问中保持信息的连续性,实现真正的“认识你”的效果。
- 减少冗余:无需在每次对话中重新介绍背景信息。
-
本质:是一个独立于模型对话之外的、可读写的外部存储系统(如数据库、向量库)。它不是直接拼接到上下文里,而是在需要时被“回忆”并注入到当前上下文中。
2. 实现策略与架构
一个完整的记忆系统是短期与长期记忆的有机结合。
短期记忆的实现
实现相对直接,主要是一个“上下文窗口管理器”。
- 数据结构:使用一个双向队列或列表来保存对话消息
List<Message>。 - 消息格式:每条消息通常包含角色(
user,assistant,system)和内容。 - 管理策略:
- 固定窗口:保留最新的N条消息或N个Token。当超过限制时,丢弃最老的消息。
- 滑动窗口与总结:一种更高级的策略。当窗口快满时,用一个单独的LLM调用将窗口中部不那么重要的对话总结成一条摘要,只保留最新的和最重要的消息。这能在有限上下文内保留更多“精髓”。
长期记忆的实现
这是一个更复杂的系统,通常包含两个核心组件:记忆写入器 和 记忆读取器。
-
记忆的存储与写入
- 触发时机:何时创建一条长期记忆?
- 显式指令:用户说“请记住我喜欢蓝色”。
- 隐式提取:模型在对话中自动识别出关键事实(如用户的工作、项目名),并决定存入记忆。
- 存储格式:
- 结构化:存入关系型数据库。表结构如
(user_id, key, value, timestamp)。例如(”user123″, “favorite_color”, “blue”, “2023-10-27”)。适合存储明确的键值对信息。 - 非结构化/向量化:将对话片段或提取的事实转换成向量嵌入,存入向量数据库(如Milvus, Pinecone)。这使得我们可以进行语义搜索。当用户问“我之前跟你提过什么关于颜色的事?”,即使他没说“蓝色”,系统也能通过向量相似度找到相关记忆。
- 结构化:存入关系型数据库。表结构如
- 触发时机:何时创建一条长期记忆?
-
记忆的读取与召回
- 触发时机:在每次构造对话上下文之前。
- 召回策略:
- 基于查询:分析用户当前的问题,生成一个搜索查询去长期记忆库中查找。
- 全量/最近记忆:在对话开始时,直接加载用户最近或最重要的几条记忆,作为系统背景信息。
- 注入方式:将召回的记忆,以
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打下坚实的基础,往期的习惯及历史内容会成为未来对话提供建设性参考和智能性。
更多推荐


所有评论(0)