Mem0Provider集成了Mem0记忆平台为Agent提供长期记忆FoundryMemoryProvider集成Azure AI Foundry的 Memory Store来为Agent提供长期记忆Mem0是一个专为AIAgent打造的长期记忆层平台让AI能像人一样跨会话记住事实、偏好、背景信息并通过向量 图谱的混合架构实现高效、可扩展、可演化的记忆管理。Mem0目前提供免费试用我们可以通过这里申请API-Key来使用它的服务。集成它的Mem0Provider很新新到对应的NuGet包还没有发布所以我不得不将其源代码从Github上扒下来。所以当对用的NuGet包发布之后相关的API肯定与本文介绍的有所不同不过由于Mem0 API不会改变所以这套实现方式肯定是使用的。1. 将Mem0Provide应用到点餐Agent上在上一篇文章中我们将ChatHistoryMemoryProvider应用到作为点餐助手的Agent上使Agent能够记住用户的口味偏好。接下来我们将ChatHistoryMemoryProvider替换成Mem0Provider来实现同样的功能。如代码片段所示我们利用一个用于远程调用Mem0 API的HttpClient和一个应用初始化状态的委托来创建了一个Mem0Provider对象。这个HttpClient使用固定的目标地址“https://api.mem0.ai并将申请的API-Key放在Authorization请求头中。using Azure; using Azure.AI.Projects; using dotenv.net; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Mem0; using Microsoft.Extensions.AI; using OpenAI; using OpenAI.Responses; using System.ComponentModel; using System.Net.Http.Headers; DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; var mem0ApiKey Environment.GetEnvironmentVariable(MEM0_API_KEY)!; using var httpClient new HttpClient(); httpClient.BaseAddress new Uri(https://api.mem0.ai); httpClient.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Token, mem0ApiKey); var memoryProvider new Mem0Provider(httpClient: httpClient, stateInitializer: InitializeMemoryState); AITool[] tools [AIFunctionFactory.Create(GetMenu, GetMenu), AIFunctionFactory.Create(PlaceOrder, PlaceOrder)]; var agent new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }) .GetChatClient(model: model) .AsIChatClient() .AsAIAgent(options: new ChatClientAgentOptions { Name delivery-order, AIContextProviders [ memoryProvider ], ChatOptions new ChatOptions { ModelIdmodel, Instructions 你是一个贴心外卖点餐助手。 用户授权自己选择菜品和数量的权力无需用户确认。但下单数量务必控制在三份以内。 点餐时既要考虑用户的口味偏好也要考虑菜品多样性以及尽可能与上次订餐有所不同。 , Tools tools } }); var session await agent.CreateSessionAsync(); session.StateBag.SetValue(user_id, Alice); var response await agent.RunAsync(帮我点一份外卖一荤一素我不能吃辣, session); Console.WriteLine($ {new string(-, 30)} 来自 Alice 的订单 {new string(-, 30)} {response} ); await OrderDelivery(Alice); await OrderDelivery(Alice); await OrderDelivery(Bob); await OrderDelivery(Bob); async Task OrderDelivery(string userName) { var session await agent.CreateSessionAsync(); session.StateBag.SetValue(user_id, userName); var response await agent.RunAsync(帮我点一份外卖, session); Console.WriteLine($ {new string(-, 30)} 来自 {userName} 的订单 {new string(-, 30)} {response} ); } static Mem0Provider.State InitializeMemoryState(AgentSession? session) { if (session is not ChatClientAgentSession chatSession) { throw new InvalidOperationException(Session is not of type ChatClientAgentSession.); } if (chatSession.StateBag?.TryGetValuestring(user_id, out var userId) ! true || string.IsNullOrWhiteSpace(userId)) { throw new InvalidOperationException(User ID not found in session state.); } var scope new Mem0ProviderScope { UserId userId }; return new Mem0Provider.State(storageScope: scope, searchScope: scope); } [Description(提取外卖菜单)] static string[] GetMenu() [辣椒炒肉, 剁椒鱼头, 番茄炒蛋, 清蒸鲈鱼, 清炒菜心, 酸辣土豆丝, 西芹百合]; [Description(外卖下单)] static IReadOnlyListOrderItem PlaceOrder(params KeyValuePairstring, int[] orderItems) [.. orderItems.Select(item new OrderItem(item.Key, item.Value))]; public readonly record struct OrderItem(string DishName, int Quantity);虽然我们不再需要为Mem0Provider提供向量数据库但是基于Scope针对对话历史消息文本的存储和检索依然没有改变所以我们在创建Mem0Provider对象的时候依然需要提供一个StateInitializer的委托对象来为每次调用初始化一个State对象该对象用于封装了上下文检索和对话历史存储使用的Scope维度。我们通过注册两个工具GetMenu和PlaceOrder和Mem0Provider将Agent创建出来后就可以开始点餐了。我们定义了辅助方法OrderDelivery来为指定的用户点餐并且每次调用都创建了一个新的Session来屏蔽短期记忆的干扰。我们第一次直接调用Agent以Alice的名义点餐并且告诉Agent“帮我点一份外卖一荤一素我不能吃辣”。后续则通过调用OrderDelivery方法来为Alice和Bob点餐。整个程序会生成如下的输出------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您搭配好一荤一素而且都不辣 ✅ 清蒸鲈鱼 ×1清淡鲜嫩 ✅ 清炒菜心 ×1爽口解腻 营养均衡又清爽很适合不能吃辣的您 祝您用餐愉快️ ------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您安排好啦 ✅ 本次为您搭配 - 清蒸鲈鱼 ×1清淡鲜美不辣 - 西芹百合 ×1清爽解腻营养丰富 一荤一素口味清淡搭配均衡也避开了辣味菜品 祝您用餐愉快呀如果下次想换换口味也可以告诉我 ------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您下单完成 ✅ **清蒸鲈鱼** ×1清淡不辣优质蛋白 **清炒菜心** ×1清爽蔬菜营养均衡 搭配一荤一素口味清淡符合您不吃辣的需求而且与常见重口味菜品有所区分。 祝您用餐愉快 ️ ------------------------------ 来自 Bob 的订单 ------------------------------ 已经帮您下单啦 ✅ 本次点餐 - 辣椒炒肉 ×1下饭又有点辣味 - 清蒸鲈鱼 ×1清淡鲜美搭配均衡 - 清炒菜心 ×1清爽解腻 荤素搭配、口味有层次而且控制在三份以内 祝您用餐愉快 ------------------------------ 来自 Bob 的订单 ------------------------------ 已经帮你下单啦 ✅ 本次为你搭配的是 - ️ 辣椒炒肉 ×1下饭主菜 - 番茄炒蛋 ×1经典家常 - 清炒菜心 ×1清爽解腻 荤素搭配均衡口味丰富又不油腻。 祝你用餐愉快呀 面的输出显示了5次点餐的结果。可以看到Alice的三次点餐都没有辣椒炒肉和剁椒鱼头因为她不能吃辣Bob的两次点餐都包含了辣椒炒肉和剁椒鱼头因为对话中并没有涉及Bob的口味偏好。2. 检索和存储的ScopeMem0Provider和ChatHistoryMemoryProvider前者通过指定的HttpClient调用远程的Mem0服务来实现对话历史的存储和检索后者则是直接操作我们提供的向量数据库来实现存储和检索。除了这点不之外其他方面的实现非常类似。所以我们会看到类似到几乎一样的类型定义比如内嵌于Mem0Provider封装了检索和存储Scope的State类型。除了Mem0ProviderScope将SessionId属性重写命名为ThreadId之外其他定义完全一样。public sealed class Mem0Provider : MessageAIContextProvider { public sealed class State { public Mem0ProviderScope StorageScope { get; } public Mem0ProviderScope SearchScope { get; } public State( Mem0ProviderScope storageScope, Mem0ProviderScope? searchScope null); } } public sealed class Mem0ProviderScope { public string? ApplicationId { get; set; } public string? AgentId { get; set; } public string? ThreadId { get; set; } public string? UserId { get; set; } }Socpe四个维度的值来源于Session状态构造Mem0Provider的时候需要提供一个StateInitializer类型的委托来根据当前的Session来初始化State对象。构造函数会根据此委托对象结合由Mem0ProviderOptions指定的键如果没有指定则使用Mem0Provider的类名创建一个ProviderSessionStateState对象在Session中维护这个State对象。对于上面点餐的例子来说我们将UserId作为Scope的维度所以StateInitializer委托对象的实现逻辑就是从Session状态中获取UserId的值并将其分别赋值给StorageScope和SearchScope的UserId属性。public sealed class Mem0Provider : MessageAIContextProvider { private readonly ProviderSessionStateState _sessionState; private IReadOnlyListstring? _stateKeys; public override IReadOnlyListstring StateKeys _stateKeys ?? [_sessionState.StateKey]; public Mem0Provider( HttpClient httpClient, FuncAgentSession?, State stateInitializer, Mem0ProviderOptions? options null, ILoggerFactory? loggerFactory null) : base( options?.SearchInputMessageFilter, options?.StorageInputRequestMessageFilter, options?.StorageInputResponseMessageFilter) { _sessionState new ProviderSessionStateState( stateInitializer, options?.StateKey ?? GetType().Name, AgentJsonUtilities.DefaultOptions); } }3. Mem0ProviderOptions作为Mem0Provider的配置选项类型Mem0ProviderOptions与ChatHistoryMemoryProviderOptions的定义也有很多相似之处。但是Mem0ProviderOptions中没有定义SearchTime属性意味着Mem0Provider需要在调用LLM之前自行完成上下文的检索并将检索到的上下文消息添加到输入消息列表中去。而不像ChatHistoryMemoryProvider那样还将检索工作实现在注册的工具中由LLM自行决定调用此工具补充上下文信息。也许在未来的版本中Mem0Provider也会提供对应的功能也未可知。Mem0ProviderOptions还提供了如下的配置选项当检索内容被格式化成提示词文本后还添加由配置选项ContextPrompt指定的前缀。如果没有显式设置会采用默认值## Memories\nConsider the following memories when answering user questions:。EnableSensitiveTelemetryData和Redactor用来控制是否以及如何对包含敏感数据的消息进行脱敏处理StateKey用来指定在Session中维护Mem0Provider.State对象所使用的键SearchInputMessageFilter、StorageInputRequestMessageFilter和StorageInputResponseMessageFilter分别用来指定在检索输入消息、存储请求消息和存储响应消息时需要应用的过滤器。public sealed class Mem0ProviderOptions { public string? ContextPrompt { get; set; } public bool EnableSensitiveTelemetryData { get; set; } public Redactor? Redactor { get; set; } public string? StateKey { get; set; } public FuncIEnumerableChatMessage, IEnumerableChatMessage? SearchInputMessageFilter { get; set; } public FuncIEnumerableChatMessage, IEnumerableChatMessage? StorageInputRequestMessageFilter { get; set; } public FuncIEnumerableChatMessage, IEnumerableChatMessage? StorageInputResponseMessageFilter { get; set; } }4. 基于对话历史的检索和存储由于Mem0Provider并为提供基于注册工具的上下文检索方法所以它只需要重写MessageAIContextProvider的ProvideMessagesAsync方法来实现基于对话历史的检索就可以了。具体的检索工作通过调用Mem0Client的SearchAsync方法来实现的作为输入的正是初始化时设置的检索Scope的维度列表。这个Mem0Client对象则是通过构造函数提供的HttpClient创建的。SearchAsync方法返回的消息文本列表被拼接在一起后会添加上ContextPrompt指定的前缀。最终返回的ChatMesage将生成的这段文本作为内容并且角色被设置为User。这个消息最终会被添加到输入消息列表中去参与LLM的推理。public sealed class Mem0Provider : MessageAIContextProvider { protected override async ValueTaskIEnumerableChatMessage ProvideMessagesAsync( InvokingContext context, CancellationToken cancellationToken default); protected override async ValueTask StoreAIContextAsync( InvokedContext context, CancellationToken cancellationToken default) } internal sealed class Mem0Client { public Mem0Client(HttpClient httpClient); public async TaskIEnumerablestring SearchAsync( string? applicationId, string? agentId, string? threadId, string? userId, string? inputText, CancellationToken cancellationToken); public async Task CreateMemoryAsync( string? applicationId, string? agentId, string? threadId, string? userId, string messageContent, string messageRole, CancellationToken cancellationToken); public async Task ClearMemoryAsync( string? applicationId, string? agentId, string? threadId, string? userId, CancellationToken cancellationToken); }针对对话历史的存储实现在重写的StoreAIContextAsync方法中最终通过调用Mem0Client的CreateMemoryAsync方法来实现的。就目前的实现来说StoreAIContextAsync方法会针对每个消息调用一次Mem0Client的CreateMemoryAsync方法很明显这是不合理的。这些瑕疵都说明了这个Mem0Provider还是个半吊子。5. Mem0的运维工具虽然Mem0Provider尚未发布但是只要了解了Mem0 API的定义可以自己提供实现。在不考虑收费的前提下利用这种云平台现成的解决方案有效避免了自行运维的麻烦。Memo0站点还提供一些运维工具来帮助我们查看和管理存储在Mem0上的记忆内容比如针对每次调用的跟踪等等。如下所示的是它提供的仪表盘