智能体的长期记忆如何实现有效持久化?
摘要:记忆与持久化:智能体的长期记忆 前言 在前几篇文章中,我们已经掌握了如何创建能说会道的智能体、如何让智能体使用工具、如何管理多轮对话的状态。但是有一个关键问题还没有解决:每次对话结束后,智能体就像失去了记忆一样,下次用户再来,它什么都不记得
记忆与持久化:智能体的长期记忆
前言
在前几篇文章中,我们已经掌握了如何创建能说会道的智能体、如何让智能体使用工具、如何管理多轮对话的状态。但是有一个关键问题还没有解决:每次对话结束后,智能体就像失去了记忆一样,下次用户再来,它什么都不记得了。
想象一下这样的场景:
第一次对话:
用户:"我的邮箱是 john@example.com"
助手:"好的,我已经记录下来了。"
第二次对话(第二天):
用户:"我的邮箱是什么?"
助手:"抱歉,我不知道。"
这种"失忆症"让智能体无法提供真正个性化的服务。本文将深入探讨如何在Agent Framework中实现智能的长期记忆系统,让智能体能够"记住"用户的历史交互,提供更加贴心的服务。
一、记忆系统的核心概念
1.1 记忆的三层架构
一个完整的记忆系统通常包含三层:
第一层:短期记忆(Short-term Memory)
也称为工作记忆,存储当前对话的上下文信息。这种记忆的生命周期与对话相同,对话结束后通常会被清除。在Agent Framework中,这对应于ConversationContext中的消息历史。
第二层:长期记忆(Long-term Memory)
存储跨会话的用户信息,包括用户偏好、历史交互记录、重要事项等。这种记忆会被持久化存储,可以在多个会话间共享。
第三层:向量记忆(Vector Memory)
存储非结构化的语义信息,通过向量相似度检索实现语义匹配。这种记忆特别适合存储对话摘要、文档内容等需要语义检索的数据。
1.2 记忆存储的选择
根据不同的场景和需求,可以选择不同的存储方案:
内存存储:适合开发测试、小规模应用,优点是速度快、成本低,缺点是重启后数据丢失。
文件存储:适合单机应用、简单场景,使用JSON或SQLite存储,优点是部署简单,缺点是扩展性差。
数据库存储:适合生产环境,使用关系型数据库(SQL Server、PostgreSQL)或NoSQL数据库(MongoDB、Cassandra),优点是可靠、可扩展,缺点是需要额外的基础设施。
向量数据库:适合需要语义检索的场景,使用Pinecone、Milvus、Qdrant等向量数据库,优点是支持语义相似度搜索,缺点是成本较高。
二、Agent Framework记忆实现
2.1 基础记忆接口定义
首先,我们定义记忆系统的基础接口:
// IMemoryStore.cs
public interface IMemoryStore
{
// 保存记忆
Task SaveMemoryAsync(MemoryItem memory);
// 检索记忆
Task<IEnumerable<MemoryItem>> SearchAsync(
string userId,
string query,
int limit = 5);
// 获取用户所有记忆
Task<IEnumerable<MemoryItem>> GetUserMemoriesAsync(
string userId,
MemoryType? type = null);
// 删除记忆
Task DeleteMemoryAsync(string memoryId);
// 更新记忆
Task UpdateMemoryAsync(MemoryItem memory);
}
// 记忆类型
public enum MemoryType
{
UserPreference, // 用户偏好
ConversationSummary, // 对话摘要
ImportantInfo, // 重要信息
UserProfile, // 用户档案
InteractionHistory // 交互历史
}
// 记忆项
public class MemoryItem
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string UserId { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public MemoryType Type { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastAccessedAt { get; set; } = DateTime.UtcNow;
public Dictionary<string, object> Metadata { get; set; } = new();
public List<string> Tags { get; set; } = new();
public float Importance { get; set; } = 1.0f; // 重要性评分
public int AccessCount { get; set; }
}
2.2 文件存储实现
对于简单的应用,可以使用文件存储:
// FileMemoryStore.cs
public class FileMemoryStore : IMemoryStore
{
private readonly string _storagePath;
private readonly ILogger<FileMemoryStore> _logger;
private readonly ConcurrentDictionary<string, List<MemoryItem>> _cache;
private readonly SemaphoreSlim _lock = new(1, 1);
public FileMemoryStore(string storagePath, ILogger<FileMemoryStore> logger)
{
_storagePath = storagePath;
_logger = logger;
_cache = new ConcurrentDictionary<string, List<MemoryItem>>();
// 确保存储目录存在
Directory.CreateDirectory(_storagePath);
}
public async Task SaveMemoryAsync(MemoryItem memory)
{
await _lock.WaitAsync();
try
{
var memories = await LoadUserMemoriesAsync(memory.UserId);
memories.Add(memory);
await SaveUserMemoriesAsync(memory.UserId, memories);
// 更新缓存
_cache.AddOrUpdate(memory.UserId, memories, (_, _) => memories);
_logger.LogInformation("保存记忆: UserId={UserId}, Type={Type}",
memory.UserId, memory.Type);
}
finally
{
_lock.Release();
}
}
public async Task<IEnumerable<MemoryItem>> SearchAsync(
string userId,
string query,
int limit = 5)
{
var memories = await LoadUserMemoriesAsync(userId);
// 简单的关键词匹配(生产环境应使用向量检索)
var results = memories
.Where(m => m.Content.Contains(query, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(m => m.Importance)
.ThenByDescending(m => m.LastAccessedAt)
.Take(limit);
// 更新访问时间
foreach (var memory in results)
{
memory.AccessCount++;
memory.LastAccessedAt = DateTime.UtcNow;
}
return results;
}
public async Task<IEnumerable<MemoryItem>> GetUserMemoriesAsync(
string userId,
MemoryType? type = null)
{
var memories = await LoadUserMemoriesAsync(userId);
if (type.HasValue)
{
return memories.Where(m => m.Type == type.Value)
.OrderByDescending(m => m.LastAccessedAt);
}
return memories.OrderByDescending(m => m.LastAccessedAt);
}
public async Task DeleteMemoryAsync(string memoryId)
{
await _lock.WaitAsync();
try
{
// 从缓存中查找并删除
foreach (var kvp in _cache)
{
var item = kvp.Value.FirstOrDefault(m => m.Id == memoryId);
if (item != null)
{
kvp.Value.Remove(item);
await SaveUserMemoriesAsync(kvp.Key, kvp.Value);
break;
}
}
}
finally
{
_lock.Release();
}
}
public async Task UpdateMemoryAsync(MemoryItem memory)
{
await _lock.WaitAsync();
try
{
var memories = await LoadUserMemoriesAsync(memory.UserId);
var index = memories.FindIndex(m => m.Id == memory.Id);
if (index >= 0)
{
memories[index] = memory;
await SaveUserMemoriesAsync(memory.UserId, memory.UserId, memories);
// 更新缓存
if (_cache.TryGetValue(memory.UserId, out var cached))
{
cached[index] = memory;
}
}
}
finally
{
_lock.Release();
}
}
private async Task<List<MemoryItem>> LoadUserMemoriesAsync(string userId)
{
if (_cache.TryGetValue(userId, out var cached))
{
return cached;
}
var filePath = GetUserMemoryFilePath(userId);
if (!File.Exists(filePath))
{
return new List<MemoryItem>();
}
try
{
var json = await File.ReadAllTextAsync(filePath);
var memories = JsonSerializer.Deserialize<List<MemoryItem>>(json)
?? new List<MemoryItem>();
_cache.AddOrUpdate(userId, memories, (_, _) => memories);
return memories;
}
catch (Exception ex)
{
_logger.LogError(ex, "加载用户记忆失败: UserId={UserId}", userId);
return new List<MemoryItem>();
}
}
private async Task SaveUserMemoriesAsync(string userId, List<MemoryItem> memories)
{
var filePath = GetUserMemoryFilePath(userId);
var json = JsonSerializer.Serialize(memories, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(filePath, json);
}
private string GetUserMemoryFilePath(string userId)
{
return Path.Combine(_storagePath, $"{userId}_memories.json");
}
}
2.3 数据库存储实现
对于生产环境,推荐使用数据库存储:
// SqlMemoryStore.cs
public class SqlMemoryStore : IMemoryStore
{
private readonly AppDbContext _context;
private readonly ILogger<SqlMemoryStore> _logger;
public SqlMemoryStore(AppDbContext context, ILogger<SqlMemoryStore> logger)
{
_context = context;
_logger = logger;
}
public async Task SaveMemoryAsync(MemoryItem memory)
{
var entity = new MemoryEntity
{
Id = memory.Id,
UserId = memory.UserId,
Content = memory.Content,
Type = (int)memory.Type,
CreatedAt = memory.CreatedAt,
LastAccessedAt = memory.LastAccessedAt,
Metadata = JsonSerializer.Serialize(memory.Metadata),
Tags = JsonSerializer.Serialize(memory.Tags),
Importance = memory.Importance,
AccessCount = memory.AccessCount
};
_context.Memories.Add(entity);
await _context.SaveChangesAsync();
_logger.LogInformation("保存记忆到数据库: Id={Id}, UserId={UserId}",
memory.Id, memory.UserId);
}
public async Task<IEnumerable<MemoryItem>> SearchAsync(
string userId,
string query,
int limit = 5)
{
// 简化实现:使用LIKE搜索
// 生产环境应使用全文检索或向量搜索
var entities = await _context.Memories
.Where(m => m.UserId == userId && m.Content.Contains(query))
.OrderByDescending(m => m.Importance)
.ThenByDescending(m => m.LastAccessedAt)
.Take(limit)
.ToListAsync();
// 更新访问信息
foreach (var entity in entities)
{
entity.AccessCount++;
entity.LastAccessedAt = DateTime.UtcNow;
}
await _context.SaveChangesAsync();
return entities.Select(MapToMemoryItem);
}
public async Task<IEnumerable<MemoryItem>> GetUserMemoriesAsync(
string userId,
MemoryType? type = null)
{
var query = _context.Memories.Where(m => m.UserId == userId);
if (type.HasValue)
{
query = query.Where(m => m.Type == (int)type.Value);
}
var entities = await query
.OrderByDescending(m => m.LastAccessedAt)
.ToListAsync();
return entities.Select(MapToMemoryItem);
}
public async Task DeleteMemoryAsync(string memoryId)
{
var entity = await _context.Memories.FindAsync(memoryId);
if (entity != null)
{
_context.Memories.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task UpdateMemoryAsync(MemoryItem memory)
{
var entity = await _context.Memories.FindAsync(memory.Id);
if (entity != null)
{
entity.Content = memory.Content;
entity.Metadata = JsonSerializer.Serialize(memory.Metadata);
entity.Tags = JsonSerializer.Serialize(memory.Tags);
entity.Importance = memory.Importance;
entity.LastAccessedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
private MemoryItem MapToMemoryItem(MemoryEntity entity)
{
return new MemoryItem
{
Id = entity.Id,
UserId = entity.UserId,
Content = entity.Content,
Type = (MemoryType)entity.Type,
CreatedAt = entity.CreatedAt,
LastAccessedAt = entity.LastAccessedAt,
Metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(entity.Metadata)
?? new Dictionary<string, object>(),
Tags = JsonSerializer.Deserialize<List<string>>(entity.Tags)
?? new List<string>(),
Importance = entity.Importance,
AccessCount = entity.AccessCount
};
}
}
// 数据库实体
public class MemoryEntity
{
[Key]
public string Id { get; set; } = string.Empty;
[Required]
[Index]
public string UserId { get; set; } = string.Empty;
[Required]
public string Content { get; set; } = string.Empty;
public int Type { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LastAccessedAt { get; set; }
public string Metadata { get; set; } = "{}";
public string Tags { get; set; } = "[]";
public float Importance { get; set; }
public int AccessCount { get; set; }
}
三、用户偏好记忆
3.1 用户偏好模型
用户偏好是最重要的记忆类型之一:
// UserPreference.cs
public class UserPreference
{
public string UserId { get; set; } = string.Empty;
// 语言偏好
public string PreferredLanguage { get; set; } = "zh-CN";
public string PreferredLanguageCode { get; set; } = "zh";
// 地区偏好
public string TimeZone { get; set; } = "Asia/Shanghai";
public string PreferredCity { get; set; } = string.Empty;
// 沟通偏好
public bool PreferDetailedResponse { get; set; } = true;
public bool PreferEmoji { get; set; } = false;
public string ResponseTone { get; set; } = "professional"; // professional, casual, friendly
// 功能偏好
public List<string> EnabledFeatures { get; set; } = new();
public Dictionary<string, object> CustomSettings { get; set; } = new();
// 历史交互
public int TotalConversations { get; set; }
public DateTime LastConversationAt { get; set; }
public List<string> FrequentlyUsedIntents { get; set; } = new();
// 更新时间
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
3.2 偏好记忆管理器
// UserPreferenceManager.cs
public class UserPreferenceManager
{
private readonly IMemoryStore _memoryStore;
private readonly ILogger<UserPreferenceManager> _logger;
private readonly ConcurrentDictionary<string, UserPreference> _cache;
public UserPreferenceManager(
IMemoryStore memoryStore,
ILogger<UserPreferenceManager> logger)
{
_memoryStore = memoryStore;
_logger = logger;
_cache = new ConcurrentDictionary<string, UserPreference>();
}
public async Task<UserPreference> GetPreferenceAsync(string userId)
{
// 先从缓存获取
if (_cache.TryGetValue(userId, out var cached))
{
return cached;
}
// 从存储加载
var memories = await _memoryStore.GetUserMemoriesAsync(
userId,
MemoryType.UserPreference);
var preferenceMemory = memories.FirstOrDefault();
if (preferenceMemory != null)
{
var preference = DeserializePreference(preferenceMemory.Content);
_cache.AddOrUpdate(userId, preference, (_, _) => preference);
return preference;
}
// 创建默认偏好
var defaultPreference = new UserPreference { UserId = userId };
_cache.AddOrUpdate(userId, defaultPreference, (_, _) => defaultPreference);
return defaultPreference;
}
public async Task SavePreferenceAsync(UserPreference preference)
{
preference.UpdatedAt = DateTime.UtcNow;
var memory = new MemoryItem
{
UserId = preference.UserId,
Content = SerializePreference(preference),
Type = MemoryType.UserPreference,
Importance = 5.0f, // 用户偏好非常重要
Metadata = new Dictionary<string, object>
{
{ "language", preference.PreferredLanguage },
{ "tone", preference.ResponseTone }
}
};
await _memoryStore.SaveMemoryAsync(memory);
// 更新缓存
_cache.AddOrUpdate(preference.UserId, preference, (_, _) => preference);
_logger.LogInformation("保存用户偏好: UserId={UserId}", preference.UserId);
}
public async Task UpdatePreferenceAsync(
string userId,
Action<UserPreference> updateAction)
{
var preference = await GetPreferenceAsync(userId);
updateAction(preference);
await SavePreferenceAsync(preference);
}
// 从交互中学习偏好
public async Task LearnFromInteractionAsync(
string userId,
string userMessage,
string assistantResponse)
{
// 分析用户消息,推断偏好
var inferredPreferences = AnalyzePreferences(userMessage);
if (inferredPreferences.Any())
{
var preference = await GetPreferenceAsync(userId);
foreach (var (key, value) in inferredPreferences)
{
ApplyPreference(preference, key, value);
}
await SavePreferenceAsync(preference);
}
}
private Dictionary<string, object> AnalyzePreferences(string message)
{
var preferences = new Dictionary<string, object>();
// 检测语言
if (message.Contains("用英文") || message.Contains("please"))
{
preferences["PreferredLanguage"] = "en-US";
}
else if (message.Contains("用中文"))
{
preferences["PreferredLanguage"] = "zh-CN";
}
// 检测语气
if (message.Contains("幽默") || message.Contains("俏皮"))
{
preferences["ResponseTone"] = "friendly";
}
else if (message.Contains("正式"))
{
preferences["ResponseTone"] = "professional";
}
return preferences;
}
private void ApplyPreference(UserPreference preference, string key, object value)
{
var property = typeof(UserPreference).GetProperty(key);
if (property != null && property.CanWrite)
{
property.SetValue(preference, value);
}
}
private string SerializePreference(UserPreference preference)
{
return JsonSerializer.Serialize(preference, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
private UserPreference DeserializePreference(string json)
{
return JsonSerializer.Deserialize<UserPreference>(
json,
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
?? new UserPreference();
}
}
四、对话摘要记忆
4.1 自动摘要生成
为了在有限的上下文中存储更多信息,我们需要对对话进行摘要:
// ConversationSummarizer.cs
public class ConversationSummarizer
{
private readonly IAIAgent _agent;
private readonly ILogger<ConversationSummarizer> _logger;
private const string SummaryPrompt = @"
请将以下对话内容压缩成简洁的摘要,包含:
1. 用户的主要需求和意图
2. 已收集的关键信息
3. 未完成的事项
4. 用户偏好(如果有)
对话内容:
{conversation_history}
请用中文输出摘要,控制在100字以内。";
public ConversationSummarizer(IAIAgent agent, ILogger<ConversationSummarizer> logger)
{
_agent = agent;
_logger = logger;
}
public async Task<string> SummarizeAsync(List<ConversationTurn> turns)
{
if (turns.Count < 4)
{
return string.Empty; // 对话太短,不需要摘要
}
var historyText = string.Join("\n", turns.Select(t =>
$"{(t.Role == "user" ? "用户" : "助手")}: {t.Content}"));
var prompt = SummaryPrompt.Replace("{conversation_history}", historyText);
try
{
var summary = await _agent.CompleteAsync(prompt);
_logger.LogInformation("生成对话摘要成功,长度: {Length}", summary.Length);
return summary.Trim();
}
catch (Exception ex)
{
_logger.LogError(ex, "生成对话摘要失败");
return GenerateSimpleSummary(turns);
}
}
// 降级方案:简单规则生成摘要
private string GenerateSimpleSummary(List<ConversationTurn> turns)
{
var userMessages = turns.Where(t => t.Role == "user").ToList();
var firstMessage = userMessages.FirstOrDefault()?.Content ?? "";
var lastMessage = userMessages.LastOrDefault()?.Content ?? "";
return $"用户咨询主题:{ExtractTopic(firstMessage)}。" +
$"最后交互:{ExtractTopic(lastMessage)}。" +
$"共{userMessages.Count}轮对话。";
}
private string ExtractTopic(string message)
{
// 简单的主题提取
var keywords = new[] { "机票", "酒店", "天气", "订单", "查询", "预订" };
foreach (var keyword in keywords)
{
if (message.Contains(keyword))
{
return keyword;
}
}
return message.Length > 10 ? message.Substring(0, 10) + "..." : message;
}
}
4.2 智能体记忆集成
将记忆系统集成到智能体中:
// AgentWithMemory.cs
public class AgentWithMemory
{
private readonly IAIAgent _agent;
private readonly IMemoryStore _memoryStore;
private readonly UserPreferenceManager _preferenceManager;
private readonly ConversationSummarizer _summarizer;
private readonly ILogger<AgentWithMemory> _logger;
// 对话历史缓冲区
private readonly Dictionary<string, List<ConversationTurn>> _conversationBuffers;
private readonly int _summaryThreshold = 10; // 多少轮对话后生成摘要
public AgentWithMemory(
IAIAgent agent,
IMemoryStore memoryStore,
UserPreferenceManager preferenceManager,
ConversationSummarizer summarizer,
ILogger<AgentWithMemory> logger)
{
_agent = agent;
_memoryStore = memoryStore;
_preferenceManager = preferenceManager;
_summarizer = summarizer;
_logger = logger;
_conversationBuffers = new Dictionary<string, List<ConversationTurn>>();
}
public async Task<string> ProcessMessageAsync(
string userId,
string conversationId,
string message)
{
// 1. 获取用户偏好
var preference = await _preferenceManager.GetPreferenceAsync(userId);
// 2. 检索相关记忆
var relevantMemories = await _memoryStore.SearchAsync(userId, message, limit: 3);
// 3. 构建上下文
var context = await BuildContextAsync(userId, conversationId, message,
relevantMemories, preference);
// 4. 调用智能体
var response = await _agent.ProcessAsync(context, message);
// 5. 保存对话到缓冲区
await AddToBufferAsync(conversationId, userId, message, response);
// 6. 检查是否需要生成摘要
await CheckAndGenerateSummaryAsync(conversationId, userId);
// 7. 从交互中学习偏好
await _preferenceManager.LearnFromInteractionAsync(userId, message, response);
// 8. 根据偏好格式化响应
response = FormatResponseByPreference(response, preference);
return response;
}
private async Task<ConversationContext> BuildContextAsync(
string userId,
string conversationId,
string message,
IEnumerable<MemoryItem> relevantMemories,
UserPreference preference)
{
var context = new ConversationContext
{
UserId = userId,
ConversationId = conversationId
};
// 添加记忆到系统提示
var memoryContext = new List<string>();
if (relevantMemories.Any())
{
memoryContext.Add("以下是与此用户相关的历史信息:");
foreach (var memory in relevantMemories)
{
memoryContext.Add($"- {memory.Content}");
}
}
// 添加用户偏好
memoryContext.Add($"\n用户偏好:语言={preference.PreferredLanguage}," +
$"语气={preference.ResponseTone}");
context.SystemMessage = string.Join("\n", memoryContext);
// 添加历史对话(从缓冲区)
if (_conversationBuffers.TryGetValue(conversationId, out var buffer))
{
context.History = buffer.TakeLast(5).ToList();
}
return context;
}
private async Task AddToBufferAsync(
string conversationId,
string userId,
string userMessage,
string assistantResponse)
{
if (!_conversationBuffers.ContainsKey(conversationId))
{
_conversationBuffers[conversationId] = new List<ConversationTurn>();
}
var buffer = _conversationBuffers[conversationId];
buffer.Add(new ConversationTurn
{
Role = "user",
Content = userMessage,
Timestamp = DateTime.UtcNow
});
buffer.Add(new ConversationTurn
{
Role = "assistant",
Content = assistantResponse,
Timestamp = DateTime.UtcNow
});
}
private async Task CheckAndGenerateSummaryAsync(
string conversationId,
string userId)
{
if (!_conversationBuffers.TryGetValue(conversationId, out var buffer))
{
return;
}
if (buffer.Count >= _summaryThreshold)
{
// 生成摘要
var summary = await _summarizer.SummarizeAsync(buffer);
if (!string.IsNullOrEmpty(summary))
{
// 保存摘要记忆
var summaryMemory = new MemoryItem
{
UserId = userId,
Content = summary,
Type = MemoryType.ConversationSummary,
Metadata = new Dictionary<string, object>
{
{ "conversationId", conversationId },
{ "turnCount", buffer.Count / 2 }
},
Tags = new List<string> { "conversation_summary" }
};
await _memoryStore.SaveMemoryAsync(summaryMemory);
_logger.LogInformation("保存对话摘要: UserId={UserId}, Turns={TurnCount}",
userId, buffer.Count / 2);
}
// 清空缓冲区(保留最近的对话)
var keepCount = 4; // 保留最近2轮对话
if (buffer.Count > keepCount)
{
_conversationBuffers[conversationId] = buffer.TakeLast(keepCount).ToList();
}
}
}
private string FormatResponseByPreference(
string response,
UserPreference preference)
{
// 根据偏好格式化响应
if (preference.PreferDetailedResponse)
{
// 确保响应足够详细
if (response.Split('。').Length < 2)
{
response += " 如果您需要更多信息,请告诉我。";
}
}
return response;
}
}
五、向量记忆与语义检索
5.1 向量嵌入服务
对于更智能的记忆检索,我们需要向量嵌入:
// EmbeddingService.cs
public interface IEmbeddingService
{
Task<float[]> GetEmbeddingAsync(string text);
Task<List<float[]>> GetEmbeddingsAsync(List<string> texts);
}
public class OpenAIEmbeddingService : IEmbeddingService
{
private readonly HttpClient _httpClient;
private readonly string _apiKey;
private readonly string _model;
public OpenAIEmbeddingService(
HttpClient httpClient,
string apiKey,
string model = "text-embedding-3-small")
{
_httpClient = httpClient;
_apiKey = apiKey;
_model = model;
}
public async Task<float[]> GetEmbeddingAsync(string text)
{
var request = new
{
model = _model,
input = text
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
var response = await _httpClient.PostAsync(
"https://api.openai.com/v1/embeddings",
content);
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<EmbeddingResponse>(responseJson);
return result?.Data?.FirstOrDefault()?.Embedding
?? throw new Exception("获取嵌入失败");
}
public async Task<List<float[]>> GetEmbeddingsAsync(List<string> texts)
{
var request = new
{
model = _model,
input = texts
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
var response = await _httpClient.PostAsync(
"https://api.openai.com/v1/embeddings",
content);
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<EmbeddingResponse>(responseJson);
return result?.Data?.Select(d => d.Embedding).ToList()
?? new List<float[]>();
}
}
5.2 向量记忆存储
使用向量数据库实现语义检索:
// VectorMemoryStore.cs
public class VectorMemoryStore : IMemoryStore
{
private readonly IEmbeddingService _embeddingService;
private readonly IMemoryStore _baseStore; // 底层存储
private readonly ILogger<VectorMemoryStore> _logger;
public VectorMemoryStore(
IEmbeddingService embeddingService,
IMemoryStore baseStore,
ILogger<VectorMemoryStore> logger)
{
_embeddingService = embeddingService;
_baseStore = baseStore;
_logger = logger;
}
public async Task SaveMemoryAsync(MemoryItem memory)
{
// 生成向量嵌入
var embedding = await _embeddingService.GetEmbeddingAsync(memory.Content);
memory.Metadata["embedding"] = embedding;
// 保存到基础存储
await _baseStore.SaveMemoryAsync(memory);
_logger.LogInformation("保存向量记忆: Id={Id}, UserId={UserId}",
memory.Id, memory.UserId);
}
public async Task<IEnumerable<MemoryItem>> SearchAsync(
string userId,
string query,
int limit = 5)
{
// 生成查询的向量
var queryEmbedding = await _embeddingService.GetEmbeddingAsync(query);
// 获取用户所有记忆
var allMemories = await _baseStore.GetUserMemoriesAsync(userId);
// 计算相似度并排序
var results = allMemories
.Select(m => new
{
Memory = m,
Similarity = CalculateCosineSimilarity(
queryEmbedding,
m.Metadata.GetValueOrDefault("embedding") as float[] ?? Array.Empty<float>())
})
.Where(x => x.Similarity > 0.7f) // 相似度阈值
.OrderByDescending(x => x.Similarity)
.Take(limit)
.Select(x => x.Memory);
return results;
}
public async Task<IEnumerable<MemoryItem>> GetUserMemoriesAsync(
string userId,
MemoryType? type = null)
{
return await _baseStore.GetUserMemoriesAsync(userId, type);
}
public async Task DeleteMemoryAsync(string memoryId)
{
await _baseStore.DeleteMemoryAsync(memoryId);
}
public async Task UpdateMemoryAsync(MemoryItem memory)
{
await _baseStore.UpdateMemoryAsync(memory);
}
private float CalculateCosineSimilarity(float[] a, float[] b)
{
if (a.Length != b.Length || a.Length == 0)
return 0;
var dotProduct = 0.0;
var normA = 0.0;
var normB = 0.0;
for (int i = 0; i < a.Length; i++)
{
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return (float)(dotProduct / (Math.Sqrt(normA) * Math.Sqrt(normB)));
}
}
六、隐私与安全
6.1 记忆访问控制
// MemoryAccessControl.cs
public class MemoryAccessControl
{
public bool CanAccessMemory(string userId, string memoryUserId, string action)
{
// 用户只能访问自己的记忆
if (userId != memoryUserId)
{
return false;
}
// 管理员可以查看所有记忆
if (IsAdmin(userId))
{
return action == "read";
}
return true;
}
public bool CanDeleteMemory(string userId, string memoryUserId)
{
return userId == memoryUserId;
}
public bool CanExportMemory(string userId, string targetUserId)
{
return userId == targetUserId;
}
private bool IsAdmin(string userId)
{
// 实现管理员检查逻辑
var adminIds = new[] { "admin@example.com" };
return adminIds.Contains(userId);
}
}
6.2 数据脱敏
// DataSanitizer.cs
public class DataSanitizer
{
private static readonly Regex EmailRegex = new(
@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
RegexOptions.Compiled);
private static readonly Regex PhoneRegex = new(
@"1[3-9]\d{9}",
RegexOptions.Compiled);
public string Sanitize(string content)
{
var sanitized = content;
// 脱敏邮箱
sanitized = EmailRegex.Replace(sanitized, "***@***.***");
// 脱敏手机号
sanitized = PhoneRegex.Replace(sanitized, "***********");
return sanitized;
}
public bool ContainsSensitiveData(string content)
{
return EmailRegex.IsMatch(content) || PhoneRegex.IsMatch(content);
}
}
七、性能优化
7.1 记忆缓存策略
// MemoryCacheManager.cs
public class MemoryCacheManager
{
private readonly IMemoryStore _memoryStore;
private readonly MemoryCache _cache;
private readonly ILogger<MemoryCacheManager> _logger;
public MemoryCacheManager(IMemoryStore memoryStore, ILogger<MemoryCacheManager> logger)
{
_memoryStore = memoryStore;
_logger = logger;
_cache = MemoryCache.Create(new CacheOptions
{
SizeLimit = 1000,
ExpirationScanFrequency = TimeSpan.FromMinutes(5)
});
}
public async Task<IEnumerable<MemoryItem>> GetCachedMemoriesAsync(
string userId,
MemoryType type)
{
var cacheKey = $"{userId}:{type}";
if (_cache.TryGetValue(cacheKey, out IEnumerable<MemoryItem> cached))
{
_logger.LogDebug("命中缓存: {CacheKey}", cacheKey);
return cached;
}
// 从存储加载
var memories = await _memoryStore.GetUserMemoriesAsync(userId, type);
var memoryList = memories.ToList();
// 存入缓存
_cache.Set(cacheKey, memoryList, new CacheEntryOptions
{
Size = memoryList.Count,
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});
return memoryList;
}
public void InvalidateCache(string userId)
{
// 清除用户相关的所有缓存
// 注意:这里简化实现,实际需要更复杂的缓存管理
_logger.LogInformation("清除缓存: UserId={UserId}", userId);
}
}
八、完整示例:个性化助手
整合所有组件,创建完整的个性化助手:
// PersonalizedAssistant.cs
public class PersonalizedAssistant
{
private readonly AgentWithMemory _agentWithMemory;
private readonly UserPreferenceManager _preferenceManager;
private readonly MemoryAccessControl _accessControl;
private readonly ILogger<PersonalizedAssistant> _logger;
public PersonalizedAssistant(
AgentWithMemory agentWithMemory,
UserPreferenceManager preferenceManager,
MemoryAccessControl accessControl,
ILogger<PersonalizedAssistant> logger)
{
_agentWithMemory = agentWithMemory;
_preferenceManager = preferenceManager;
_accessControl = accessControl;
_logger = logger;
}
public async Task<AssistantResponse> ProcessAsync(Request request)
{
// 验证访问权限
if (!_accessControl.CanAccessMemory(request.UserId, request.UserId, "read"))
{
return new AssistantResponse
{
Success = false,
Message = "您没有权限访问此功能。"
};
}
try
{
var response = await _agentWithMemory.ProcessMessageAsync(
request.UserId,
request.ConversationId,
request.Message);
return new AssistantResponse
{
Success = true,
Message = response
};
}
catch (Exception ex)
{
_logger.LogError(ex, "处理请求失败: UserId={UserId}", request.UserId);
return new AssistantResponse
{
Success = false,
Message = "处理您的请求时发生错误,请稍后重试。"
};
}
}
// 用户管理自己的记忆
public async Task<MemoryManagementResult> ManageMemoriesAsync(
string userId,
MemoryManagementRequest request)
{
if (!_accessControl.CanAccessMemory(userId, userId, request.Action))
{
return new MemoryManagementResult { Success = false, Message = "权限不足" };
}
// 实现记忆管理逻辑
return new MemoryManagementResult { Success = true };
}
}
九、总结与展望
通过本文的学习,我们已经掌握了智能体长期记忆的核心技术:
✅ 三层记忆架构:短期记忆、长期记忆、向量记忆
✅ 多种存储方案:文件存储、数据库存储、向量数据库
✅ 用户偏好管理:自动学习和更新用户偏好
✅ 对话摘要:自动压缩长对话为摘要
✅ 向量检索:基于语义相似度的记忆检索
✅ 隐私安全:访问控制和数据脱敏
✅ 性能优化:缓存策略提升响应速度
关键收获:
记忆系统是实现真正智能化服务的基础。通过合理设计记忆系统,智能体可以记住用户的偏好、历史交互、重要信息,从而提供更加个性化和连贯的服务体验。在实际应用中,需要根据具体场景选择合适的存储方案,并注意保护用户隐私。
下一篇文章预告:
在第六篇文章中,我们将探索工作流编排。我们将学习如何使用Agent Framework构建复杂的业务工作流,实现多步骤的任务处理和自动化流程。
实践建议:
从简单的文件存储开始,逐步升级到数据库和向量存储
重视用户隐私,实现完善的数据保护机制
定期清理无用记忆,避免存储空间膨胀
监控记忆检索性能,优化查询效率
相关资源:
Agent Framework记忆文档
向量数据库选择指南
数据隐私最佳实践
"最好的智能体不是知道最多,而是记住最准的。"
