Agent 记忆管理 (一定要做会话隔离)-(长期记忆、记忆压缩、记忆检索策略)
🎯 为什么需要记忆管理?
核心问题:LLM 的上下文窗口是有限的(比如 128K),但对话可以无限长。
用户:你好 Agent:你好! 用户:今天天气不错 Agent:是啊... 用户:(聊了 1000 轮之后) 用户:我刚才说我叫什么名字? Agent:❌ 对不起,前面的内容超出上下文窗口了...
记忆管理要解决的三件事:
- 记什么 —— 不是所有对话都值得记住
- 怎么记 —— 压缩、摘要、向量化
- 怎么取 —— 需要时能快速检索出来
📦 记忆的三层架构
┌─────────────────────────────────────────────────────────┐ │ 长期记忆 (Long-term) │ │ ───────────────────────────────────────────────────── │ │ • 向量数据库 (Qdrant/Chroma/Pinecone) │ │ • 存储:用户偏好、重要事实、历史经验 │ │ • 检索方式:语义相似度搜索 │ └─────────────────────────────────────────────────────────┘ ↕ 选择性写入/读取 ┌─────────────────────────────────────────────────────────┐ │ 工作记忆 (Working) │ │ ───────────────────────────────────────────────────── │ │ • 当前对话的滑动窗口 (最近 N 轮) │ │ • 存储:当前任务的上下文 │ │ • 检索方式:FIFO + 重要性评分 │ └─────────────────────────────────────────────────────────┘ ↕ 压缩/摘要 ┌─────────────────────────────────────────────────────────┐ │ 短期记忆 (Short-term) │ │ ───────────────────────────────────────────────────── │ │ • LLM 的 Context Window │ │ • 存储:Prompt + 最近几轮对话 │ │ • 检索方式:全量注入 Prompt │ └─────────────────────────────────────────────────────────┘
🔧 记忆压缩的 4 种策略
策略 1:滑动窗口 (Sliding Window)
最简单,只保留最近 N 轮对话。
优点:实现简单
缺点:可能丢掉重要信息(比如用户第 1 轮说的名字)
策略 2:摘要压缩 (Summarization)
用 LLM 把旧对话压缩成摘要。
原始对话(1000 tokens): 用户:我叫张三,在北京工作,喜欢打篮球... Agent:好的张三... 用户:我明天要去看电影... Agent:... ↓ 压缩后(100 tokens): 摘要:用户名叫张三,在北京工作,喜欢篮球。 当前任务:计划明天看电影。
优点:保留关键信息,大幅节省 Token
缺点:需要额外调用 LLM,有成本
策略 3:重要性评分 (Importance Scoring)
给每条消息打分,优先保留重要的。
优点:智能保留重要信息
缺点:评分规则需要调优
策略 4:向量检索 (Vector Retrieval) ⭐ 最强大
把历史对话向量化存储,需要时检索相关片段。
优点:
- 可以存储海量历史(不受 Context 限制)
- 检索精准(语义相似度)
- 支持跨会话记忆
缺点:
- 需要向量数据库
- 需要 Embedding 模型(有成本)
🎯 记忆检索的 3 种策略
策略 1:按需检索 (On-Demand Retrieval)
Agent 主动判断什么时候需要查记忆。
// 在 Prompt 中告诉 Agent 可以查记忆 var prompt = """ 你是智能助手。如果用户提到"之前"、"那个"、"我记得"等词, 请调用 memory_search 工具查询历史记录。 可用工具:memory_search(query: string) """;
策略 2:自动注入 (Auto-Inject)
每轮对话前,自动检索相关记忆注入 Prompt。
// 每轮对话前执行 var relevantMemories = await memoryStore.SearchAsync( query: currentUserMessage, limit: 5 ); var prompt = $""" 相关记忆: {string.Join("\n", relevantMemories)} 当前对话: {conversationHistory} 用户:{currentUserMessage} """;
策略 3:混合策略 (Hybrid) ⭐ 推荐
工作记忆用滑动窗口 + 摘要,长期记忆用向量检索。
每轮对话流程: 1. 保留最近 10 轮对话(工作记忆) 2. 用 LLM 把 10-50 轮压缩成摘要 3. 如果用户提到"之前"、"那个"等词 → 向量检索长期记忆 4. 把 (1)+(2)+(3) 组合成最终 Prompt
📊 实战建议
| 场景 | 推荐方案 |
|---|---|
| 简单聊天机器人 | 滑动窗口 (20 轮) |
| 个人助手(记住用户偏好) | 滑动窗口 + 向量检索 |
| 企业客服(多会话) | 摘要压缩 + 向量检索 |
| 复杂任务 Agent | 混合策略(三层架构) |
代码实战【未做会话隔离,仅做演示,实际项目必须要会话隔离】
main.cs
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Embeddings; using Qdrant.Client; using System.Text.Json; namespace ConsoleApp13 { internal class Program { static async Task Main(string[] args) { // 1. 构建配置 var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); // 2. 配置日志 var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information)); // 3. 创建 Kernel(配置 DashScope) var builder = Kernel.CreateBuilder(); builder.AddOpenAIChatCompletion( modelId: "qwen-plus", apiKey: config["DashScope:ApiKey"]!, endpoint: new Uri(config["DashScope:Endpoint"]!)); var kernel = builder.Build(); var embeddingService = new EmbeddingService(config["DashScope:ApiKey"]!); // 4. 创建 Qdrant 客户端 var qdrantClient = new QdrantClient( host: config["Qdrant:Host"]!, port: Convert.ToInt32(config["Qdrant:Port"]!), apiKey: config["Qdrant:QdrantApiKey"]!); // 5. 创建混合记忆服务 var memoryService = new HybridMemoryService( kernel, embeddingService, // ← 直接传 qdrantClient, loggerFactory.CreateLogger<HybridMemoryService>()); // 6. 初始化 await memoryService.InitializeAsync(); Console.WriteLine("✅ 记忆服务初始化完成"); // 7. 模拟对话流程 Console.WriteLine("\n🎯 开始对话演示...\n"); // 第 1 轮:用户自我介绍 await ProcessUserMessage(memoryService, "你好,我叫张三,在北京工作,喜欢打篮球"); // 第 2 轮:记住偏好 await ProcessUserMessage(memoryService, "我平时喜欢吃川菜,尤其是麻婆豆腐"); // 第 3 轮:记住任务 await ProcessUserMessage(memoryService, "对了,我明天要去看电影,记得提醒我"); // 第 4 轮:测试记忆检索 await ProcessUserMessage(memoryService, "我之前说我叫什么名字来着?"); // 第 5 轮:测试记忆检索 await ProcessUserMessage(memoryService, "那个项目怎么样了?"); Console.WriteLine("\n✅ 演示完成"); } static async Task ProcessUserMessage(HybridMemoryService memoryService, string userMessage) { Console.WriteLine($"👤 用户:{userMessage}"); // 1. 保存到工作记忆 memoryService.AddToWorkingMemory("用户", userMessage); // 2. 检测是否需要保存到长期记忆 if (userMessage.Contains("我叫") || userMessage.Contains("我喜欢") || userMessage.Contains("记得")) { //包含关键字,存入向量数据库 await memoryService.SaveLongTermMemoryAsync( userMessage, MemoryType.Fact, importance: 8); } // 3. 构建包含记忆的上下文 var context = await memoryService.BuildContextWithMemoriesAsync(userMessage); // 4. 调用 LLM 生成回复(这里简化处理) var response = $"[AI 回复] 基于上下文,我记住了你的信息。"; Console.WriteLine($"🤖 AI: {response}\n"); // 5. 保存 AI 回复到工作记忆 - 临时线程安全队列/字典 memoryService.AddToWorkingMemory("AI", response); // 6. 检查是否需要压缩记忆 "到达 50 条就执行",压缩其中40条,保留10条,等待下一次50条在启动压缩 await memoryService.TryCompressMemoryAsync(); } } /// <summary> /// 自定义 DashScope Embedding 服务 /// </summary> public class EmbeddingService { private readonly string _apiKey; private readonly HttpClient _httpClient = new(); public EmbeddingService(string apiKey) { _apiKey = apiKey; } public async Task<float[]> GenerateEmbeddingAsync(string text) { var endpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"; var requestBody = new { model = "text-embedding-v2", input = text }; var json = JsonSerializer.Serialize(requestBody); var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); _httpClient.DefaultRequestHeaders.Clear(); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}"); var response = await _httpClient.PostAsync(endpoint, content); var responseJson = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new Exception($"DashScope API 错误:{response.StatusCode}\n{responseJson}"); } using var doc = JsonDocument.Parse(responseJson); var embedding = doc.RootElement.GetProperty("data")[0].GetProperty("embedding"); var vector = new float[embedding.GetArrayLength()]; int idx = 0; foreach (var item in embedding.EnumerateArray()) { vector[idx++] = item.GetSingle(); } return vector; } } }
services
using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Embeddings; using Qdrant.Client; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; namespace ConsoleApp13 { /// <summary> /// 混合记忆服务 - 实现三层记忆架构 /// </summary> public class HybridMemoryService : IDisposable { private readonly Kernel _kernel; private readonly EmbeddingService _embeddingService; // ← 直接用你的类,不依赖接口 private readonly QdrantClient _qdrantClient; private readonly ILogger<HybridMemoryService> _logger; // 工作记忆:滑动窗口(最近 N 轮对话) private readonly ConcurrentQueue<ChatMessage> _workingMemory = new(); private readonly int _workingMemoryLimit = 20; // 记忆集合名称 private const string LongTermCollection = "long_term_memories"; private const int EmbeddingDimensions = 1536; // 根据使用的 Embedding 模型调整 // 摘要压缩配置 private readonly int _summaryThreshold = 50; // 超过 50 轮触发压缩 private readonly int _summaryKeepCount = 10; // 压缩后保留最近 10 轮 public HybridMemoryService( Kernel kernel, EmbeddingService embeddingService, QdrantClient qdrantClient, ILogger<HybridMemoryService> logger) { _kernel = kernel; _embeddingService = embeddingService; _qdrantClient = qdrantClient; _logger = logger; } #region 初始化 /// <summary> /// 初始化 Qdrant 集合 /// </summary> public async Task InitializeAsync() { try { // 检查集合是否存在,不存在则创建 var collections = await _qdrantClient.ListCollectionsAsync(); if (!collections.Any(c => c == LongTermCollection)) { await _qdrantClient.CreateCollectionAsync( LongTermCollection, new Qdrant.Client.Grpc.VectorParams { Size = EmbeddingDimensions, Distance = Qdrant.Client.Grpc.Distance.Cosine }); _logger.LogInformation("创建 Qdrant 集合:{Collection}", LongTermCollection); } } catch (Exception ex) { _logger.LogError(ex, "初始化 Qdrant 集合失败"); throw; } } #endregion #region 工作记忆管理(滑动窗口) /// <summary> /// 添加消息到工作记忆 /// </summary> public void AddToWorkingMemory(string role, string content) { _workingMemory.Enqueue(new ChatMessage { Role = role, Content = content, Timestamp = DateTime.UtcNow }); // 超出限制时移除最旧的 while (_workingMemory.Count > _workingMemoryLimit) { _workingMemory.TryDequeue(out _); } } /// <summary> /// 获取工作记忆(最近 N 轮对话) /// </summary> public List<ChatMessage> GetWorkingMemory(int? limit = null) { return _workingMemory .TakeLast(limit ?? _workingMemoryLimit) .ToList(); } /// <summary> /// 清空工作记忆 /// </summary> public void ClearWorkingMemory() { while (_workingMemory.TryDequeue(out _)) { } } #endregion #region 记忆压缩(摘要生成) /// <summary> /// 压缩旧对话生成摘要 /// </summary> public async Task<string> CompressOldMessagesAsync(List<ChatMessage> oldMessages) { var prompt = @"你是一个记忆压缩助手。请将以下对话压缩成 200 字以内的摘要。 要求: 1. 保留用户的关键信息(姓名、偏好、重要事实) 2. 保留当前进行中的任务 3. 去掉寒暄和无关细节 4. 用第三人称描述 对话内容: {{conversation}} 摘要:"; var conversationText = string.Join("\n", oldMessages.Select(m => $"{m.Role}: {m.Content}")); var args = new KernelArguments { ["conversation"] = conversationText }; var summarizer = _kernel.CreateFunctionFromPrompt(prompt); var summary = await summarizer.InvokeAsync(_kernel, args); return summary.ToString(); } /// <summary> /// 检查并执行记忆压缩 /// </summary> public async Task<bool> TryCompressMemoryAsync() { if (_workingMemory.Count < _summaryThreshold) { return false; // 未达到压缩阈值 } // 取出需要压缩的旧消息 var messagesToCompress = _workingMemory .Take(_workingMemory.Count - _summaryKeepCount) .ToList(); if (!messagesToCompress.Any()) { return false; } // 生成摘要 var summary = await CompressOldMessagesAsync(messagesToCompress); // 将摘要存入长期记忆 await SaveLongTermMemoryAsync( summary, MemoryType.Summary, importance: 8); // 摘要重要性较高 // 清空旧消息,保留最近 N 轮 var recentMessages = _workingMemory .TakeLast(_summaryKeepCount) .ToList(); ClearWorkingMemory(); foreach (var msg in recentMessages) { _workingMemory.Enqueue(msg); } _logger.LogInformation("完成记忆压缩,生成摘要:{Summary}", summary[..Math.Min(50, summary.Length)]); return true; } #endregion #region 长期记忆(向量检索) /// <summary> /// 保存长期记忆 /// </summary> public async Task SaveLongTermMemoryAsync( string text, MemoryType type, int importance = 5, string? metadata = null) { try { // 生成向量 var embedding = await _embeddingService.GenerateEmbeddingAsync(text); var memoryItem = new MemoryItem { Text = text, Type = type, Importance = importance, Metadata = metadata }; // 保存到 Qdrant await _qdrantClient.UpsertAsync( LongTermCollection, new[] { new Qdrant.Client.Grpc.PointStruct { Id = new Qdrant.Client.Grpc.PointId { Uuid = memoryItem.Id }, Vectors = new Qdrant.Client.Grpc.Vectors { Vector = embedding }, Payload = { ["text"] = memoryItem.Text, ["type"] = memoryItem.Type.ToString(), ["importance"] = memoryItem.Importance, ["created_at"] = memoryItem.CreatedAt, ["metadata"] = memoryItem.Metadata ?? "" } } }); _logger.LogInformation("保存长期记忆:{Text}", text[..Math.Min(30, text.Length)]); } catch (Exception ex) { _logger.LogError(ex, "保存长期记忆失败"); throw; } } /// <summary> /// 检索相关记忆 /// </summary> public async Task<List<MemoryItem>> SearchMemoriesAsync( string query, ulong limit = 5, double minRelevanceScore = 0.7, MemoryType? typeFilter = null) { try { // 生成查询向量 var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query); // 在 Qdrant 中搜索 var searchResults = await _qdrantClient.SearchAsync( LongTermCollection, queryEmbedding.ToArray(), limit: limit, scoreThreshold: (float)minRelevanceScore); var memories = new List<MemoryItem>(); foreach (var result in searchResults) { var payload = result.Payload; var memory = new MemoryItem { Id = result.Id.Uuid, Text = payload["text"]?.StringValue ?? "", Type = payload.ContainsKey("type") ? Enum.Parse<MemoryType>(payload["type"].StringValue) : MemoryType.Fact, Importance = payload.ContainsKey("importance") ? (int)payload["importance"].IntegerValue : 5, CreatedAt = payload.ContainsKey("created_at") ? payload["created_at"].IntegerValue : 0, Metadata = payload["metadata"]?.StringValue }; memories.Add(memory); } // 更新访问时间 foreach (var memory in memories) { memory.LastAccessedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); memory.AccessCount++; } _logger.LogInformation("检索到 {Count} 条相关记忆", memories.Count); return memories; } catch (Exception ex) { _logger.LogError(ex, "检索记忆失败"); return new List<MemoryItem>(); } } /// <summary> /// 删除记忆 /// </summary> public async Task DeleteMemoryAsync(string memoryId) { await _qdrantClient.DeleteAsync( LongTermCollection, new[] { new Qdrant.Client.Grpc.PointId { Uuid = memoryId } }); } #endregion #region 混合检索(构建最终 Prompt) /// <summary> /// 构建包含记忆的完整上下文 /// </summary> public async Task<string> BuildContextWithMemoriesAsync( string userMessage, bool enableLongTermSearch = true) { var contextBuilder = new StringBuilder(); // 1. 添加工作记忆(最近对话) contextBuilder.AppendLine("【当前对话】"); foreach (var msg in GetWorkingMemory()) { contextBuilder.AppendLine($"{msg.Role}: {msg.Content}"); } // 2. 如果需要,检索长期记忆 if (enableLongTermSearch) { var keywords = ExtractSearchKeywords(userMessage); if (!string.IsNullOrEmpty(keywords)) { var memories = await SearchMemoriesAsync(keywords, limit: 3); if (memories.Any()) { contextBuilder.AppendLine("\n【相关记忆】"); foreach (var memory in memories) { contextBuilder.AppendLine($"• {memory.Text}"); } } } } // 3. 添加当前用户消息 contextBuilder.AppendLine($"\n用户:{userMessage}"); return contextBuilder.ToString(); } /// <summary> /// 从用户消息中提取搜索关键词 /// </summary> private string ExtractSearchKeywords(string userMessage) { // 检测是否需要检索记忆的关键词 var triggerWords = new[] { "之前", "那个", "我记得", "上次", "以前", "说过" }; if (triggerWords.Any(w => userMessage.Contains(w))) { // 简单提取:去掉触发词,保留核心内容 return userMessage; } return string.Empty; } #endregion public void Dispose() { _qdrantClient?.Dispose(); } } /// <summary> /// 对话消息 /// </summary> public class ChatMessage { public string Role { get; set; } = string.Empty; public string Content { get; set; } = string.Empty; public DateTime Timestamp { get; set; } } }
Models
using System; using System.Collections.Generic; using System.Text; namespace ConsoleApp13 { /// <summary> /// 记忆项 - 存储在 Qdrant 中的基本单元 /// </summary> public class MemoryItem { public string Id { get; set; } = Guid.NewGuid().ToString(); /// <summary> /// 记忆文本内容 /// </summary> public string Text { get; set; } = string.Empty; /// <summary> /// 记忆类型:Fact(事实) | Task(任务) | Preference(偏好) | Summary(摘要) /// </summary> public MemoryType Type { get; set; } /// <summary> /// 重要性评分 (0-10) /// </summary> public int Importance { get; set; } = 5; /// <summary> /// 创建时间戳 /// </summary> public long CreatedAt { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); /// <summary> /// 最后访问时间戳(用于热度衰减) /// </summary> public long LastAccessedAt { get; set; } /// <summary> /// 访问次数(用于热度计算) /// </summary> public int AccessCount { get; set; } /// <summary> /// 元数据(JSON 字符串,存储额外信息) /// </summary> public string? Metadata { get; set; } } public enum MemoryType { Fact = 0, // 用户事实(名字、地点等) Task = 1, // 任务信息 Preference = 2, // 用户偏好 Summary = 3, // 对话摘要 Experience = 4 // 经验/教训 } }
配置文件
{ "DashScope": { "ApiKey": "sk-axxxc9xxxxxxxxxxxxxxxxxxxxxxxxxxxxx1d", "Endpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1" }, "Qdrant": { "Host": "x.y.m.c", "Port": 6334, "QdrantApiKey": "cxxxxxxxxxxxxxxxxxxxg", "VectorSize": 1536 }, "Memory": { "WorkingMemoryLimit": 20, "SummaryThreshold": 50, "SummaryKeepCount": 10 } }
项目引用
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.SemanticKernel" Version="1.78.0" /> <PackageReference Include="Qdrant.Client" Version="1.18.1" /> <PackageReference Include="Sdcb.DashScope" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="System.Threading.RateLimiting" Version="8.0.0" /> <PackageReference Include="Polly" Version="8.4.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" /> </ItemGroup> <ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup> </Project>

浙公网安备 33010602011771号