Agent 记忆管理 (一定要做会话隔离)-(长期记忆、记忆压缩、记忆检索策略)

🎯 为什么需要记忆管理?

核心问题:LLM 的上下文窗口是有限的(比如 128K),但对话可以无限长。

用户:你好
Agent:你好!
用户:今天天气不错
Agent:是啊...
用户:(聊了 1000 轮之后)
用户:我刚才说我叫什么名字?
Agent:❌ 对不起,前面的内容超出上下文窗口了...

记忆管理要解决的三件事

  1. 记什么 —— 不是所有对话都值得记住
  2. 怎么记 —— 压缩、摘要、向量化
  3. 怎么取 —— 需要时能快速检索出来

📦 记忆的三层架构

┌─────────────────────────────────────────────────────────┐
│                    长期记忆 (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;
        }
    }
}
View Code

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; }
    }
}
View Code

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  // 经验/教训
    }
}
View Code

配置文件

{
  "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>

 

posted @ 2026-08-17 14:47  天才卧龙  阅读(7)  评论(0)    收藏  举报