让AI拥有记忆-对话上下文的两种实现

发布时间:2026/7/7 7:48:39
让AI拥有记忆-对话上下文的两种实现 大模型的失忆症先看一个场景你: 我叫张三 AI: 你好张三 你: 我叫什么名字 AI: 抱歉我不知道你的名字请告诉我。这是因为第二次请求时AI只收到了我叫什么名字这一条消息之前的对话已经丢失了。解决方案很直接把历史对话一起发给模型。但怎么发就有讲究了。方式一简单记忆——全量拼接历史最直接的思路把所有历史消息都塞进每次请求的messages里。ListChatMessage chatMessages [ new ChatMessage(ChatRole.System, 你是一个有用的AI助手请用中文回答用户的问题。) ]; while (true) { Console.Write(\n你: ); var userInput Console.ReadLine(); // 将用户消息加入历史 chatMessages.Add(new ChatMessage(ChatRole.User, userInput)); StringBuilder sb new StringBuilder(); await foreach (var update in client.GetStreamingResponseAsync(chatMessages)) { foreach (var item in update.Contents) { if (item is TextContent text) { Console.Write(text.Text); sb.Append(text.Text); } } } }注意这里的关键chatMessages是一个持续累积的列表每次对话的User消息和Assistant回复都会被追加进去下一次请求时全部发送给模型。效果验证你: 我叫张三 AI: 你好张三 你: 我叫什么名字 AI: 你叫张三呀AI记住了但这种方式有明显的局限性简单记忆的问题问题说明Token消耗爆炸对话越久每次请求携带的token越多费用越高超出上下文窗口模型有最大token限制如8K、32K超过就会报错信息噪音无关的历史对话会干扰模型的回答质量想象一下如果你们聊了100轮每次请求都要带上所有历史——这就是为什么我们需要更聪明的方案。方式二向量记忆——智能检索相关上下文核心思想不是所有历史都相关只把与当前问题最相关的历史发给模型。那怎么判断相关呢答案是把语义变成向量用数学计算相似度。Embedding把文字变成向量Embedding模型的作用是将一段文本转换为一个高维浮点数组向量。语义相近的文本其向量在空间中的距离也越近。IEmbeddingGeneratorstring, Embeddingfloat embeddingGenerator new OpenAI.Embeddings.EmbeddingClient( embeddingModel, new ApiKeyCredential(apiKey), new OpenAI.OpenAIClientOptions { Endpoint new Uri(baseUrl) }) .AsIEmbeddingGenerator();我们使用embedding-3模型它会生成2048维的浮点向量。余弦相似度衡量语义距离有了向量我们需要一种方法来衡量两个向量的相似程度。余弦相似度是最常用的方法值域为[-1, 1]值越接近1表示语义越相似。.NET提供了高性能的TensorPrimitives.CosineSimilarity方法float similarity TensorPrimitives.CosineSimilarity( new ReadOnlySpanfloat(record.Embedding), querySpan);这里使用ReadOnlySpanfloat可以获得接近零拷贝的高性能计算。向量记忆的完整流程用户输入 ↓ Embedding模型 → 生成查询向量 ↓ 计算查询向量与所有历史记录的余弦相似度 ↓ 过滤相似度 阈值如0.3 ↓ 排序按相似度降序取TopK条如6条 ↓ 构建消息System提示 筛选后的相关历史 当前用户消息 ↓ 发送给大模型 → 流式输出回答 ↓ 将本轮对话用户消息AI回复的向量和文本存入记忆核心实现选择相关历史record ChatMessageRecord(ChatMessage Message, float[] Embedding); ListChatMessageRecord SelectRelevantMessages( ListChatMessageRecord allMessages, float[] queryVector, int topK, float threshold) { if (allMessages.Count 0) return []; ReadOnlySpanfloat querySpan queryVector; // 计算每条历史记录与当前查询的余弦相似度 var scored new List(ChatMessageRecord Record, float Score)(); foreach (var record in allMessages) { float similarity TensorPrimitives.CosineSimilarity( new ReadOnlySpanfloat(record.Embedding), querySpan); if (similarity threshold) { scored.Add((record, similarity)); } } // 按相似度降序排列取TopK return scored .OrderByDescending(x x.Score) .Take(topK) .Select(x x.Record) .ToList(); }完整的向量记忆对话// 存储所有对话记录及其对应的向量 ListChatMessageRecord allMessages []; const int topK 6; // 最多选取6条最相关的历史对话 const float threshold 0.3f; // 余弦相似度阈值 // 1. 为当前用户输入生成向量 var userEmbedding await embeddingGenerator.GenerateAsync(userInput); float[] userVector userEmbedding.Vector.ToArray(); // 2. 计算与所有历史对话的余弦相似度选出最相关的 var relevantMessages SelectRelevantMessages(allMessages, userVector, topK, threshold); // 3. 构建本次发送给AI的消息列表 ListChatMessage chatMessages [ new ChatMessage(ChatRole.System, 你是一个有用的AI助手请用中文回答用户的问题。 下面是与用户当前问题相关的历史对话记录请参考这些上下文来回答。) ]; foreach (var record in relevantMessages) { chatMessages.Add(record.Message); } chatMessages.Add(new ChatMessage(ChatRole.User, userInput)); // 4. 调用AI流式输出... // 5. 将本轮对话存入历史记录包含向量 allMessages.Add(new ChatMessageRecord( new ChatMessage(ChatRole.User, userInput), userEmbedding.Vector.ToArray())); if (sb.Length 0) { var aiText sb.ToString(); var aiEmbedding await embeddingGenerator.GenerateAsync(aiText); allMessages.Add(new ChatMessageRecord( new ChatMessage(ChatRole.Assistant, aiText), aiEmbedding.Vector.ToArray())); }注意第5步AI的回复也要生成向量并存入记忆。因为下一轮对话中用户的问题可能与AI之前的回复内容相关。两种模式对比维度简单记忆向量记忆实现复杂度⭐ 极低⭐⭐⭐ 中等Token消耗随对话轮数线性增长稳定每次最多TopK条记忆质量全量保留但有噪音精准检索语义相关上下文窗口容易超限几乎不会超限适用场景简短对话10轮长期对话、主题切换频繁额外成本无Embedding模型调用费用向量记忆的局限虽然向量记忆解决了上下文膨胀的问题但它还有一个致命缺陷记忆存在内存中程序重启就丢失了。如果你想实现跨会话的持久化记忆——比如用户今天关闭程序明天再来还能记得之前的对话——那就需要把向量存到数据库中。这正是下一篇文章要解决的问题。小结这篇文章我们学习了大模型的无状态本质每次请求都是独立的记忆需要靠我们自己维护简单记忆模式全量拼接历史简单但有token爆炸风险