在正式介绍ChatHistoryMemoryProvider的设计和实现原理之前我们先来看一个简单的例子来感受一下ChatHistoryMemoryProvider的作用。我们利用如下的代码创建了一个用来帮助我们点外卖的Agent。注册的两个工具GetMenu和PlaceOrder分别用来获取菜单和下单。我们赋予Agent自主选择菜品和直接下单的权力所以我们希望Agent能够在为不同用户提供服务的过程中能够记住每个用户的口味偏好并且在后续的点餐过程中能够基于这些偏好来推荐菜品。using Azure; using dotenv.net; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.SemanticKernel.Connectors.InMemory; using OpenAI; using System.ComponentModel; DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; var openAIClient new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }); var embeddingGenerator openAIClient .GetEmbeddingClient(model: text-embedding-3-small) .AsIEmbeddingGenerator() .AsBuilder() .Use((texts, options, next, ct) { var sanitizedTexts texts.Select(t string.IsNullOrWhiteSpace(t) ? : t); return next.GenerateAsync(sanitizedTexts, options, ct); }) .Build(); ; var memoryProvider new ChatHistoryMemoryProvider( vectorStore: new InMemoryVectorStore(options: new InMemoryVectorStoreOptions { EmbeddingGenerator embeddingGenerator }), collectionName: user_preference, vectorDimensions: 1536, stateInitializer: InitializeMemoryState); AITool[] tools [AIFunctionFactory.Create(GetMenu, GetMenu), AIFunctionFactory.Create(PlaceOrder, PlaceOrder)]; var agent openAIClient .GetChatClient(model:model) .AsIChatClient() .AsAIAgent(options: new ChatClientAgentOptions { Name delivery-order, AIContextProviders [memoryProvider], ChatOptions new ChatOptions { 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 ChatHistoryMemoryProvider.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) { throw new InvalidOperationException(User ID not found in session state.); } var scope new ChatHistoryMemoryProviderScope { UserId userId }; return new ChatHistoryMemoryProvider.State(storageScope: scope,searchScope: scope); } [Description(提取外卖菜单)] static string[] GetMenu() { return [辣椒炒肉, 剁椒鱼头, 番茄炒蛋, 清蒸鲈鱼, 清炒菜心,酸辣土豆丝,西芹百合]; } [Description(外卖下单)] static IReadOnlyListOrderItem PlaceOrder(params KeyValuePairstring, int[] orderItems) { return [.. orderItems.Select(item new OrderItem(item.Key, item.Value))]; } public readonly record struct OrderItem(string DishName, int Quantity);ChatHistoryMemoryProvider利用一个向量数据库来存储对话历史简单起见我们使用了一个基于内存的向量数据库InMemoryVectorStore。InMemoryVectorStore需要一个IEmbeddingGenerator对象为输入的文本生成嵌入向量所以我们调用OpenAIClient的GetEmbeddingClient方法得到了一个针对text-embedding-3-small模型的EmbeddingClient并将其转换成了IEmbeddingGenerator对象。创建ChatHistoryMemoryProvider除了提供作为存储的向量数据库之外我们还提供了存储记忆的集合名称user_preference和嵌入向量的维度1536。ChatHistoryMemoryProvider存储的记忆来源于对话历史但是需要针对需要以不同的Scope进行存储。由于我们希望利用记忆了解用户的口味偏好所以我们选择了以用户为Scope来存储记忆并要求Session中必需包含用户的ID。创建ChatHistoryMemoryProvider提供的第四个参数是一个StateInitializer类型的委托它的作用就是根据当前的Session来初始化作为状态的ChatHistoryMemoryProvider.State对象而ChatHistoryMemoryProvider.State对象中包含了检索和存储记忆所需要的Scope信息。该参数对应的方法为InitializeMemoryState我们将两种Scope都设置为UserId。我们通过注册两个工具GetMenu和PlaceOrder和ChatHistoryMemoryProvider将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的口味偏好。但是这个Agent还是有一个问题我通过系统指令说明尽量尽可能与上次订餐有所不同但是在实际输出中Alice的三次点餐中有两次的搭配是相同的我将会在后面介绍背后的原因和改进方案。2. 检索和存储的ScopeChatHistoryMemoryProvider从本质上也属于RAG的一种实现方式它根据当前对话从向量数据库中检索相关的历史对话记录并将这些记录作为上下文提供给LLM来生成响应。这里存储的对话历史可不是针对某个Session甚至不是某个Agent多Agent可共享同一个向量数据库所以在检索的时候必需指定适当的Scope以保证检索结果的相关性。既然检索的时候需要限定范围存储的时候自然也需要指定针对Scope的维度而且还得保证检索的Scope维度是存储Scope的子集。ChatHistoryMemoryProvider将所需的检索和存储Scope封装到内嵌的State类中。所谓的Scope通过ChatHistoryMemoryProviderScope类表示四个属性ApplicationId、AgentId、SessionId和UserId分别代表了Scope针对应用、Agent、Session和用户的四个维度。public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDisposable { public sealed class State { public ChatHistoryMemoryProviderScope StorageScope { get; } public ChatHistoryMemoryProviderScope SearchScope { get; } public State( ChatHistoryMemoryProviderScope storageScope, ChatHistoryMemoryProviderScope? searchScope null); } } public sealed class ChatHistoryMemoryProviderScope { public string? ApplicationId { get; set; } public string? AgentId { get; set; } public string? SessionId { get; set; } public string? UserId { get; set; } }Socpe四个维度的值来源于Session状态构造ChatHistoryMemoryProvider的时候需要提供一个StateInitializer类型的委托来根据当前的Session来初始化State对象。构造函数会根据此委托对象结合由ChatHistoryMemoryProviderOptions指定的键如果没有指定则使用ChatHistoryMemoryProvider的类名创建一个ProviderSessionStateState对象在Session中维护这个State对象。对于上面点餐的例子来说我们将UserId作为Scope的维度所以StateInitializer委托对象的实现逻辑就是从Session状态中获取UserId的值并将其分别赋值给StorageScope和SearchScope的UserId属性。public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDisposable { private readonly ProviderSessionStateState _sessionState; private IReadOnlyListstring? _stateKeys; public override IReadOnlyListstring StateKeys _stateKeys ?? [_sessionState.StateKey]; public ChatHistoryMemoryProvider( VectorStore vectorStore, string collectionName, int vectorDimensions, FuncAgentSession?, State stateInitializer, ChatHistoryMemoryProviderOptions? options null, ILoggerFactory? loggerFactory null) : base(options?.SearchInputMessageFilter, options?.StorageInputRequestMessageFilter, options?.StorageInputResponseMessageFilter) { _sessionState new ProviderSessionStateState( stateInitializer, options?.StateKey ?? GetType().Name, AgentJsonUtilities.DefaultOptions); } }3. ChatHistoryMemoryProviderOptions由于ChatHistoryMemoryProvider本质上也是RAG的一种实现方式所以其实现原理与TextSearchProvider非常类似它们的配置选项类型也有很多共同的成员。ChatHistoryMemoryProvider的上下文检索方法也通过ChatHistoryMemoryProviderOptions的SearchTime属性分两种一种在调用LLM之前主动检索对应Scope维度历史对话记录并将这些记录作为上下文提供给LLM另一种是注册检索工具并利用工具描述和系统提示词指导LLM在最终作答之前调用它来获取上下文信息。对于第二种SearchTime的取值我们需要提供FunctionToolName和FunctionToolDescription属性来指定供LLM调用的工具的名称和描述信息。当检索内容被用作LLM上下文时ContextPrompt属性可以用来指定对应提示词的前缀。public sealed class ChatHistoryMemoryProviderOptions { public enum SearchBehavior { BeforeAIInvoke, OnDemandFunctionCalling } public SearchBehavior SearchTime { get; set; } SearchBehavior.BeforeAIInvoke; public string? FunctionToolName { get; set; } public string? FunctionToolDescription { get; set; } public string? ContextPrompt { get; set; } }SearchTime的默认值为BeforeAIInvoke。如果没有对FunctionToolName、FunctionToolDescription和ContextPrompt进行配置ChatHistoryMemoryProvider会使用如下的默认值FunctionToolName: SearchFunctionToolDescription:Allows searching for related previous chat history to help answer the user question.ContextPrompt:## Memories\nConsider the following memories when answering user questions:ChatHistoryMemoryProviderOptions还提供了如下的配置选项MaxResults用来指定每次检索返回的最大结果数量EnableSensitiveTelemetryData和Redactor用来控制是否以及如何对包含敏感数据的消息进行脱敏处理StateKey用来指定在Session中维护ChatHistoryMemoryProvider.State对象所使用的键SearchInputMessageFilter、StorageInputRequestMessageFilter和StorageInputResponseMessageFilter分别用来指定在检索输入消息、存储请求消息和存储响应消息时需要应用的过滤器。public sealed class ChatHistoryMemoryProviderOptions { public int? MaxResults { 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. 基于对话历史的检索ChatHistoryMemoryProvider与TextSearchProvider针对上下文的检索基本一致甚至还要简单一些因为TextSearchProvider用于检索的查询文本来源于整个Session的对话历史但是ChatHistoryMemoryProvider用于检索的查询文本来源于当前输入消息。具体的实现体现在重写的ProvideAIContextAsync和ProvideMessagesAsync方法中。具体的检索实现在私有方法SearchTextAsync中userQuestion参数由LLM提供SearchScope参数来源于State对象SearchTextAsync方法会根据这两个参数检索向量数据库最后将检索到的结果格式化成返回的文本。public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDisposable { protected override async ValueTaskAIContext ProvideAIContextAsync( AIContextProvider.InvokingContext context, CancellationToken cancellationToken default); protected override async ValueTaskIEnumerableChatMessage ProvideMessagesAsync( InvokingContext context, CancellationToken cancellationToken default); private async Taskstring SearchTextAsync( string userQuestion, ChatHistoryMemoryProviderScope searchScope, CancellationToken cancellationToken default) }如果SearchTime被设置成OnDemandFunctionCalling它会将上下文检索逻辑实现在一个工具中并封装在返回的AIConetxt中。如果SearchTime被设置成BeforeAIInvoke它会在ProvideMessagesAsync方法中直接进行上下文检索具体的流程为从Session状态中获取State对象并从State对象中获取SearchScope从当前输入的一个或者多个消息中提取文本内容并拼接成查询文本调用SearchTextAsync方法进行检索获取检索结果文本将检索结果文本与ContextPrompt进行拼接构造成一条新的ChatMessage它将被添加到当前的消息列表中并作为上下文提供给LLM。5. 查看工具和生成的消息ChatHistoryMemoryProvider注册的工具和根据检索内容生成的ChatMessage是整个ChatHistoryMemoryProvider的核心产出所以我们有必要来看看它会生成一个怎样的工具以及根据检索内容生成的ChatMessage具有怎样的内容。为此我们定义了如下这个AIContextTrackingProvider如代码所示这是一个自定义的AIContextProvider类它重写了InvokingCoreAsync方法在方法中我们打印了当前调用的AIContext中的消息列表和工具列表。class AIContextTrackingProvider : AIContextProvider { protected override ValueTaskAIContext InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken default) { var index 1; foreach (var message in context.AIContext?.Messages!) { Console.WriteLine($ {new string(-, 20)} Message {index} {new string(-, 20)} Role: {message.Role} Text: {message.Text} ); } var function context.AIContext?.Tools?.SingleOrDefault(tool tool.Name Search) as AIFunction; if (function is not null) { Console.WriteLine($ {new string(-, 20)} Tool {function.Name} {new string(-, 20)} Description: {function.Description} JsonSchema: {JsonSerializer.Serialize(function.JsonSchema, new JsonSerializerOptions { WriteIndented true })} ); } Console.WriteLine(\n\n); return base.InvokingCoreAsync(context, cancellationToken); } }5.1 查看根据检索结果生成的消息我们将这个AIContextTrackingProvider应用到我们开篇演示的实例中。如代码所示我们在创建Agent的时候将AIContextTrackingProvider添加到AIContextProviders列表中并放在ChatHistoryMemoryProvider的后面这样就可以保证在Agent调用LLM之前AIContextTrackingProvider能够看到ChatHistoryMemoryProvider添加的消息和工具了。然后我们针对同一个用户Alice采用不同的Session两次调用Agent来点餐。... var memoryProvider new ChatHistoryMemoryProvider( vectorStore: new InMemoryVectorStore(options: new InMemoryVectorStoreOptions { EmbeddingGenerator embeddingGenerator }), collectionName: chat_history_memory, vectorDimensions: 1536, stateInitializer: InitializeMemoryState); var trackingProvider new AIContextTrackingProvider(); AITool[] tools [AIFunctionFactory.Create(GetMenu, GetMenu), AIFunctionFactory.Create(PlaceOrder, PlaceOrder)]; var agent openAIClient .GetChatClient(model:model) .AsIChatClient() .AsAIAgent(options: new ChatClientAgentOptions { Name delivery-order, AIContextProviders [memoryProvider, trackingProvider], ChatOptions new ChatOptions { Instructions 你是一个贴心外卖点餐助手。 用户授权自己选择菜品和数量的权力无需用户确认。但下单数量务必控制在三份以内。 点餐时既要考虑用户的口味偏好也要考虑菜品多样性以及尽可能与上次订餐有所不同。 , Tools tools } } ); var session await agent.CreateSessionAsync(); session.StateBag.SetValue(user_id, Alice); await agent.RunAsync(帮我点一份外卖一荤一素我不能吃辣,session); session await agent.CreateSessionAsync(); session.StateBag.SetValue(user_id, Alice); await agent.RunAsync(帮我点一份外卖, session); ...输出-------------------- Message 1 -------------------- Role: user Text: 帮我点一份外卖一荤一素我不能吃辣 -------------------- Message 1 -------------------- Role: user Text: 帮我点一份外卖 -------------------- Message 2 -------------------- Role: user Text: ## Memories Consider the following memories when answering user questions: 帮我点一份外卖一荤一素我不能吃辣 已经帮您下单 ✅ 荤菜清蒸鲈鱼 ×1清淡不辣 素菜清炒菜心 ×1爽口清香 整体搭配清淡健康完全不辣营养也比较均衡。 祝您用餐愉快 ️从输出结果可以看出第二次调用会出现两个ChatMessage第二个ChatMessage的内容正好包含第一次调用的请求和响应。5.2 查看注册的工具为了查看ChatHistoryMemoryProvider注册的工具我们需要将SearchTime设置成OnDemandFunctionCalling这样它才会将检索逻辑封装成一个工具来供LLM调用。我们在创建ChatHistoryMemoryProvider的时候通过ChatHistoryMemoryProviderOptions来设置这个选项如代码所示var memoryProvider new ChatHistoryMemoryProvider( vectorStore: new InMemoryVectorStore(options: new InMemoryVectorStoreOptions { EmbeddingGenerator embeddingGenerator }), collectionName: chat_history_memory, vectorDimensions: 1536, stateInitializer: InitializeMemoryState, options: new ChatHistoryMemoryProviderOptions { SearchTime ChatHistoryMemoryProviderOptions.SearchBehavior.OnDemandFunctionCalling } );输出-------------------- Message 1 -------------------- Role: user Text: 帮我点一份外卖一荤一素我不能吃辣 -------------------- Tool Search -------------------- Description: Allows searching for related previous chat history to help answer the user question. JsonSchema: { type: object, properties: { userQuestion: { type: string } }, required: [ userQuestion ] }从输出结果可以看出ChatHistoryMemoryProvider注册了一个名为Search的工具工具函数具有一个字符串类型的参数userQuestion表示作为上下文检索查询文本的用户问题。6. 对话实例存储ChatHistoryMemoryProvider针对对话历史存储的实现在重写的StoreAIContextAsync方法中。它的实现流程为从InvokedContext提取出本轮调用新产生的请求和响应消息并提取它们的文本内容并拼接成一个字符串作为存储文本从Session状态中获取State对象并从State对象中获取StorageScope然后针对StorageScope的维度对生成的文本内容进行存储即可。public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDisposable { protected override async ValueTask StoreAIContextAsync( InvokedContext context, CancellationToken cancellationToken default) }7. 如何解决订单重复问题在上面的点餐例子中我们利用ChatHistoryMemoryProvider赋予了Agent记忆用户口味偏好的能力从而使得Agent能够基于这些偏好来推荐菜品。但是我们发现系统指令尽可能与上次订餐有所不同并没有生效。在了解了ChatHistoryMemoryProvider的实现原理之后我们可以回答这个问题了虽然ChatHistoryMemoryProvider存储了对话历史但是它进行上下文检索的时候总是使用当前输入消息中的文本作为查询文本来进行检索的所以检索过程并不会考虑系统指令的存在。我们看看将这个指令添加到请求消息中会不会有帮助。为了验证这个想法我们创建了一个新的AdditionalMessageProvider它会在每次调用时都往上下文中添加一条新的消息我们将系统指令作为这条消息的内容这样就可以保证系统指令会被ChatHistoryMemoryProvider用来进行上下文检索了。如下代码所示class AdditionalMessageProvider(string content) : MessageAIContextProvider { protected override ValueTaskIEnumerableChatMessage ProvideMessagesAsync(InvokingContext context, CancellationToken cancellationToken default) { var aiContent new TextContent(content); return ValueTask.FromResultIEnumerableChatMessage([new ChatMessage { Role ChatRole.User, Contents [aiContent] }]); } }然后我们将这个AdditionalMessageProvider添加到Agent的AIContextProviders列表中并放在ChatHistoryMemoryProvider的后面这样就可以保证在Agent调用LLM之前ChatHistoryMemoryProvider能够看到这个额外的消息了。如下代码所示var agent openAIClient .GetChatClient(model:model) .AsIChatClient() .AsAIAgent(options: new ChatClientAgentOptions { Name delivery-order, AIContextProviders [ memoryProvider, new AdditionalMessageProvider(点餐时既要考虑用户的口味偏好也要考虑菜品多样性以及尽可能与上次订餐有所不同)], ChatOptions new ChatOptions { 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(Alice); await OrderDelivery(Alice);输出------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您搭配好啦 ✅ **清蒸鲈鱼** ×1荤菜清淡不辣鲜嫩爽口 **西芹百合** ×1素菜清爽解腻口感清甜 整体口味清淡、不含辣椒营养搭配均衡一荤一素刚刚好 祝您用餐愉快如果下次想换换口味也可以告诉我偏好 ------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您重新搭配好啦 ✅避开上次的清蒸鲈鱼和西芹百合也不含辣 **番茄炒蛋** ×1酸甜开胃经典家常荤素结合 **清炒菜心** ×1清爽解腻口感脆嫩 这次换了不同搭配依然清淡不辣营养均衡 祝您用餐愉快下次想试试别的风味也可以告诉我 ------------------------------ 来自 Alice 的订单 ------------------------------ 已经为您搭配好新的餐品啦 ✅这次和上次不同更换了菜品保证多样性 **番茄炒蛋** ×1酸甜开胃家常经典不辣 **清炒菜心** ×1清爽脆嫩清淡健康 整体依然是不辣搭配清爽不油腻同时和上次的鲈鱼、西芹百合不同换了新口味 祝您用餐愉快下次如果想吃肉类或者想要更丰富一点也可以告诉我 ------------------------------ 来自 Alice 的订单 ------------------------------ 已为你下单 ✅ 清蒸鲈鱼 ×1清淡鲜美不辣优质蛋白 西芹百合 ×1清爽解腻搭配均衡 考虑到你不能吃辣这次特意避开了辣椒类和酸辣口味同时选择了一荤一素营养搭配也比较清爽不会太油腻。 祝你用餐愉快 ️ ------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您下单完成 ✅ 本次为您搭配了三道菜兼顾口味和营养也保证菜品多样性 - ️ **剁椒鱼头** ×1鲜辣开胃主菜担当 - **清炒菜心** ×1清爽解腻均衡搭配 - **番茄炒蛋** ×1经典家常酸甜适口 祝您用餐愉快如果有特别想吃的口味比如清淡一点、重辣一点或者想吃肉类为主下次告诉我我会帮您更精准搭配 ️从输出结果可以看出这样的解决方案还是有效果的。但是由于它无法精确地定位上次点餐依然还是会出现两次点餐内容相同的情况。标签: Agent, AI, C#, MAF, Memory免责声明本内容来自平台创作者博客园系信息发布平台仅提供信息存储空间服务。好文要顶 关注我 收藏该文 微信分享Artech粉丝 - 10569 关注 - 0推荐博客加关注10« 上一篇 [MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF» 下一篇 [MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案posted 2026-06-20 08:09 Artech 阅读(100) 评论(0) 收藏 举报
让Agent记住用户的偏好
在正式介绍ChatHistoryMemoryProvider的设计和实现原理之前我们先来看一个简单的例子来感受一下ChatHistoryMemoryProvider的作用。我们利用如下的代码创建了一个用来帮助我们点外卖的Agent。注册的两个工具GetMenu和PlaceOrder分别用来获取菜单和下单。我们赋予Agent自主选择菜品和直接下单的权力所以我们希望Agent能够在为不同用户提供服务的过程中能够记住每个用户的口味偏好并且在后续的点餐过程中能够基于这些偏好来推荐菜品。using Azure; using dotenv.net; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.SemanticKernel.Connectors.InMemory; using OpenAI; using System.ComponentModel; DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; var openAIClient new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }); var embeddingGenerator openAIClient .GetEmbeddingClient(model: text-embedding-3-small) .AsIEmbeddingGenerator() .AsBuilder() .Use((texts, options, next, ct) { var sanitizedTexts texts.Select(t string.IsNullOrWhiteSpace(t) ? : t); return next.GenerateAsync(sanitizedTexts, options, ct); }) .Build(); ; var memoryProvider new ChatHistoryMemoryProvider( vectorStore: new InMemoryVectorStore(options: new InMemoryVectorStoreOptions { EmbeddingGenerator embeddingGenerator }), collectionName: user_preference, vectorDimensions: 1536, stateInitializer: InitializeMemoryState); AITool[] tools [AIFunctionFactory.Create(GetMenu, GetMenu), AIFunctionFactory.Create(PlaceOrder, PlaceOrder)]; var agent openAIClient .GetChatClient(model:model) .AsIChatClient() .AsAIAgent(options: new ChatClientAgentOptions { Name delivery-order, AIContextProviders [memoryProvider], ChatOptions new ChatOptions { 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 ChatHistoryMemoryProvider.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) { throw new InvalidOperationException(User ID not found in session state.); } var scope new ChatHistoryMemoryProviderScope { UserId userId }; return new ChatHistoryMemoryProvider.State(storageScope: scope,searchScope: scope); } [Description(提取外卖菜单)] static string[] GetMenu() { return [辣椒炒肉, 剁椒鱼头, 番茄炒蛋, 清蒸鲈鱼, 清炒菜心,酸辣土豆丝,西芹百合]; } [Description(外卖下单)] static IReadOnlyListOrderItem PlaceOrder(params KeyValuePairstring, int[] orderItems) { return [.. orderItems.Select(item new OrderItem(item.Key, item.Value))]; } public readonly record struct OrderItem(string DishName, int Quantity);ChatHistoryMemoryProvider利用一个向量数据库来存储对话历史简单起见我们使用了一个基于内存的向量数据库InMemoryVectorStore。InMemoryVectorStore需要一个IEmbeddingGenerator对象为输入的文本生成嵌入向量所以我们调用OpenAIClient的GetEmbeddingClient方法得到了一个针对text-embedding-3-small模型的EmbeddingClient并将其转换成了IEmbeddingGenerator对象。创建ChatHistoryMemoryProvider除了提供作为存储的向量数据库之外我们还提供了存储记忆的集合名称user_preference和嵌入向量的维度1536。ChatHistoryMemoryProvider存储的记忆来源于对话历史但是需要针对需要以不同的Scope进行存储。由于我们希望利用记忆了解用户的口味偏好所以我们选择了以用户为Scope来存储记忆并要求Session中必需包含用户的ID。创建ChatHistoryMemoryProvider提供的第四个参数是一个StateInitializer类型的委托它的作用就是根据当前的Session来初始化作为状态的ChatHistoryMemoryProvider.State对象而ChatHistoryMemoryProvider.State对象中包含了检索和存储记忆所需要的Scope信息。该参数对应的方法为InitializeMemoryState我们将两种Scope都设置为UserId。我们通过注册两个工具GetMenu和PlaceOrder和ChatHistoryMemoryProvider将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的口味偏好。但是这个Agent还是有一个问题我通过系统指令说明尽量尽可能与上次订餐有所不同但是在实际输出中Alice的三次点餐中有两次的搭配是相同的我将会在后面介绍背后的原因和改进方案。2. 检索和存储的ScopeChatHistoryMemoryProvider从本质上也属于RAG的一种实现方式它根据当前对话从向量数据库中检索相关的历史对话记录并将这些记录作为上下文提供给LLM来生成响应。这里存储的对话历史可不是针对某个Session甚至不是某个Agent多Agent可共享同一个向量数据库所以在检索的时候必需指定适当的Scope以保证检索结果的相关性。既然检索的时候需要限定范围存储的时候自然也需要指定针对Scope的维度而且还得保证检索的Scope维度是存储Scope的子集。ChatHistoryMemoryProvider将所需的检索和存储Scope封装到内嵌的State类中。所谓的Scope通过ChatHistoryMemoryProviderScope类表示四个属性ApplicationId、AgentId、SessionId和UserId分别代表了Scope针对应用、Agent、Session和用户的四个维度。public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDisposable { public sealed class State { public ChatHistoryMemoryProviderScope StorageScope { get; } public ChatHistoryMemoryProviderScope SearchScope { get; } public State( ChatHistoryMemoryProviderScope storageScope, ChatHistoryMemoryProviderScope? searchScope null); } } public sealed class ChatHistoryMemoryProviderScope { public string? ApplicationId { get; set; } public string? AgentId { get; set; } public string? SessionId { get; set; } public string? UserId { get; set; } }Socpe四个维度的值来源于Session状态构造ChatHistoryMemoryProvider的时候需要提供一个StateInitializer类型的委托来根据当前的Session来初始化State对象。构造函数会根据此委托对象结合由ChatHistoryMemoryProviderOptions指定的键如果没有指定则使用ChatHistoryMemoryProvider的类名创建一个ProviderSessionStateState对象在Session中维护这个State对象。对于上面点餐的例子来说我们将UserId作为Scope的维度所以StateInitializer委托对象的实现逻辑就是从Session状态中获取UserId的值并将其分别赋值给StorageScope和SearchScope的UserId属性。public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDisposable { private readonly ProviderSessionStateState _sessionState; private IReadOnlyListstring? _stateKeys; public override IReadOnlyListstring StateKeys _stateKeys ?? [_sessionState.StateKey]; public ChatHistoryMemoryProvider( VectorStore vectorStore, string collectionName, int vectorDimensions, FuncAgentSession?, State stateInitializer, ChatHistoryMemoryProviderOptions? options null, ILoggerFactory? loggerFactory null) : base(options?.SearchInputMessageFilter, options?.StorageInputRequestMessageFilter, options?.StorageInputResponseMessageFilter) { _sessionState new ProviderSessionStateState( stateInitializer, options?.StateKey ?? GetType().Name, AgentJsonUtilities.DefaultOptions); } }3. ChatHistoryMemoryProviderOptions由于ChatHistoryMemoryProvider本质上也是RAG的一种实现方式所以其实现原理与TextSearchProvider非常类似它们的配置选项类型也有很多共同的成员。ChatHistoryMemoryProvider的上下文检索方法也通过ChatHistoryMemoryProviderOptions的SearchTime属性分两种一种在调用LLM之前主动检索对应Scope维度历史对话记录并将这些记录作为上下文提供给LLM另一种是注册检索工具并利用工具描述和系统提示词指导LLM在最终作答之前调用它来获取上下文信息。对于第二种SearchTime的取值我们需要提供FunctionToolName和FunctionToolDescription属性来指定供LLM调用的工具的名称和描述信息。当检索内容被用作LLM上下文时ContextPrompt属性可以用来指定对应提示词的前缀。public sealed class ChatHistoryMemoryProviderOptions { public enum SearchBehavior { BeforeAIInvoke, OnDemandFunctionCalling } public SearchBehavior SearchTime { get; set; } SearchBehavior.BeforeAIInvoke; public string? FunctionToolName { get; set; } public string? FunctionToolDescription { get; set; } public string? ContextPrompt { get; set; } }SearchTime的默认值为BeforeAIInvoke。如果没有对FunctionToolName、FunctionToolDescription和ContextPrompt进行配置ChatHistoryMemoryProvider会使用如下的默认值FunctionToolName: SearchFunctionToolDescription:Allows searching for related previous chat history to help answer the user question.ContextPrompt:## Memories\nConsider the following memories when answering user questions:ChatHistoryMemoryProviderOptions还提供了如下的配置选项MaxResults用来指定每次检索返回的最大结果数量EnableSensitiveTelemetryData和Redactor用来控制是否以及如何对包含敏感数据的消息进行脱敏处理StateKey用来指定在Session中维护ChatHistoryMemoryProvider.State对象所使用的键SearchInputMessageFilter、StorageInputRequestMessageFilter和StorageInputResponseMessageFilter分别用来指定在检索输入消息、存储请求消息和存储响应消息时需要应用的过滤器。public sealed class ChatHistoryMemoryProviderOptions { public int? MaxResults { 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. 基于对话历史的检索ChatHistoryMemoryProvider与TextSearchProvider针对上下文的检索基本一致甚至还要简单一些因为TextSearchProvider用于检索的查询文本来源于整个Session的对话历史但是ChatHistoryMemoryProvider用于检索的查询文本来源于当前输入消息。具体的实现体现在重写的ProvideAIContextAsync和ProvideMessagesAsync方法中。具体的检索实现在私有方法SearchTextAsync中userQuestion参数由LLM提供SearchScope参数来源于State对象SearchTextAsync方法会根据这两个参数检索向量数据库最后将检索到的结果格式化成返回的文本。public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDisposable { protected override async ValueTaskAIContext ProvideAIContextAsync( AIContextProvider.InvokingContext context, CancellationToken cancellationToken default); protected override async ValueTaskIEnumerableChatMessage ProvideMessagesAsync( InvokingContext context, CancellationToken cancellationToken default); private async Taskstring SearchTextAsync( string userQuestion, ChatHistoryMemoryProviderScope searchScope, CancellationToken cancellationToken default) }如果SearchTime被设置成OnDemandFunctionCalling它会将上下文检索逻辑实现在一个工具中并封装在返回的AIConetxt中。如果SearchTime被设置成BeforeAIInvoke它会在ProvideMessagesAsync方法中直接进行上下文检索具体的流程为从Session状态中获取State对象并从State对象中获取SearchScope从当前输入的一个或者多个消息中提取文本内容并拼接成查询文本调用SearchTextAsync方法进行检索获取检索结果文本将检索结果文本与ContextPrompt进行拼接构造成一条新的ChatMessage它将被添加到当前的消息列表中并作为上下文提供给LLM。5. 查看工具和生成的消息ChatHistoryMemoryProvider注册的工具和根据检索内容生成的ChatMessage是整个ChatHistoryMemoryProvider的核心产出所以我们有必要来看看它会生成一个怎样的工具以及根据检索内容生成的ChatMessage具有怎样的内容。为此我们定义了如下这个AIContextTrackingProvider如代码所示这是一个自定义的AIContextProvider类它重写了InvokingCoreAsync方法在方法中我们打印了当前调用的AIContext中的消息列表和工具列表。class AIContextTrackingProvider : AIContextProvider { protected override ValueTaskAIContext InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken default) { var index 1; foreach (var message in context.AIContext?.Messages!) { Console.WriteLine($ {new string(-, 20)} Message {index} {new string(-, 20)} Role: {message.Role} Text: {message.Text} ); } var function context.AIContext?.Tools?.SingleOrDefault(tool tool.Name Search) as AIFunction; if (function is not null) { Console.WriteLine($ {new string(-, 20)} Tool {function.Name} {new string(-, 20)} Description: {function.Description} JsonSchema: {JsonSerializer.Serialize(function.JsonSchema, new JsonSerializerOptions { WriteIndented true })} ); } Console.WriteLine(\n\n); return base.InvokingCoreAsync(context, cancellationToken); } }5.1 查看根据检索结果生成的消息我们将这个AIContextTrackingProvider应用到我们开篇演示的实例中。如代码所示我们在创建Agent的时候将AIContextTrackingProvider添加到AIContextProviders列表中并放在ChatHistoryMemoryProvider的后面这样就可以保证在Agent调用LLM之前AIContextTrackingProvider能够看到ChatHistoryMemoryProvider添加的消息和工具了。然后我们针对同一个用户Alice采用不同的Session两次调用Agent来点餐。... var memoryProvider new ChatHistoryMemoryProvider( vectorStore: new InMemoryVectorStore(options: new InMemoryVectorStoreOptions { EmbeddingGenerator embeddingGenerator }), collectionName: chat_history_memory, vectorDimensions: 1536, stateInitializer: InitializeMemoryState); var trackingProvider new AIContextTrackingProvider(); AITool[] tools [AIFunctionFactory.Create(GetMenu, GetMenu), AIFunctionFactory.Create(PlaceOrder, PlaceOrder)]; var agent openAIClient .GetChatClient(model:model) .AsIChatClient() .AsAIAgent(options: new ChatClientAgentOptions { Name delivery-order, AIContextProviders [memoryProvider, trackingProvider], ChatOptions new ChatOptions { Instructions 你是一个贴心外卖点餐助手。 用户授权自己选择菜品和数量的权力无需用户确认。但下单数量务必控制在三份以内。 点餐时既要考虑用户的口味偏好也要考虑菜品多样性以及尽可能与上次订餐有所不同。 , Tools tools } } ); var session await agent.CreateSessionAsync(); session.StateBag.SetValue(user_id, Alice); await agent.RunAsync(帮我点一份外卖一荤一素我不能吃辣,session); session await agent.CreateSessionAsync(); session.StateBag.SetValue(user_id, Alice); await agent.RunAsync(帮我点一份外卖, session); ...输出-------------------- Message 1 -------------------- Role: user Text: 帮我点一份外卖一荤一素我不能吃辣 -------------------- Message 1 -------------------- Role: user Text: 帮我点一份外卖 -------------------- Message 2 -------------------- Role: user Text: ## Memories Consider the following memories when answering user questions: 帮我点一份外卖一荤一素我不能吃辣 已经帮您下单 ✅ 荤菜清蒸鲈鱼 ×1清淡不辣 素菜清炒菜心 ×1爽口清香 整体搭配清淡健康完全不辣营养也比较均衡。 祝您用餐愉快 ️从输出结果可以看出第二次调用会出现两个ChatMessage第二个ChatMessage的内容正好包含第一次调用的请求和响应。5.2 查看注册的工具为了查看ChatHistoryMemoryProvider注册的工具我们需要将SearchTime设置成OnDemandFunctionCalling这样它才会将检索逻辑封装成一个工具来供LLM调用。我们在创建ChatHistoryMemoryProvider的时候通过ChatHistoryMemoryProviderOptions来设置这个选项如代码所示var memoryProvider new ChatHistoryMemoryProvider( vectorStore: new InMemoryVectorStore(options: new InMemoryVectorStoreOptions { EmbeddingGenerator embeddingGenerator }), collectionName: chat_history_memory, vectorDimensions: 1536, stateInitializer: InitializeMemoryState, options: new ChatHistoryMemoryProviderOptions { SearchTime ChatHistoryMemoryProviderOptions.SearchBehavior.OnDemandFunctionCalling } );输出-------------------- Message 1 -------------------- Role: user Text: 帮我点一份外卖一荤一素我不能吃辣 -------------------- Tool Search -------------------- Description: Allows searching for related previous chat history to help answer the user question. JsonSchema: { type: object, properties: { userQuestion: { type: string } }, required: [ userQuestion ] }从输出结果可以看出ChatHistoryMemoryProvider注册了一个名为Search的工具工具函数具有一个字符串类型的参数userQuestion表示作为上下文检索查询文本的用户问题。6. 对话实例存储ChatHistoryMemoryProvider针对对话历史存储的实现在重写的StoreAIContextAsync方法中。它的实现流程为从InvokedContext提取出本轮调用新产生的请求和响应消息并提取它们的文本内容并拼接成一个字符串作为存储文本从Session状态中获取State对象并从State对象中获取StorageScope然后针对StorageScope的维度对生成的文本内容进行存储即可。public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDisposable { protected override async ValueTask StoreAIContextAsync( InvokedContext context, CancellationToken cancellationToken default) }7. 如何解决订单重复问题在上面的点餐例子中我们利用ChatHistoryMemoryProvider赋予了Agent记忆用户口味偏好的能力从而使得Agent能够基于这些偏好来推荐菜品。但是我们发现系统指令尽可能与上次订餐有所不同并没有生效。在了解了ChatHistoryMemoryProvider的实现原理之后我们可以回答这个问题了虽然ChatHistoryMemoryProvider存储了对话历史但是它进行上下文检索的时候总是使用当前输入消息中的文本作为查询文本来进行检索的所以检索过程并不会考虑系统指令的存在。我们看看将这个指令添加到请求消息中会不会有帮助。为了验证这个想法我们创建了一个新的AdditionalMessageProvider它会在每次调用时都往上下文中添加一条新的消息我们将系统指令作为这条消息的内容这样就可以保证系统指令会被ChatHistoryMemoryProvider用来进行上下文检索了。如下代码所示class AdditionalMessageProvider(string content) : MessageAIContextProvider { protected override ValueTaskIEnumerableChatMessage ProvideMessagesAsync(InvokingContext context, CancellationToken cancellationToken default) { var aiContent new TextContent(content); return ValueTask.FromResultIEnumerableChatMessage([new ChatMessage { Role ChatRole.User, Contents [aiContent] }]); } }然后我们将这个AdditionalMessageProvider添加到Agent的AIContextProviders列表中并放在ChatHistoryMemoryProvider的后面这样就可以保证在Agent调用LLM之前ChatHistoryMemoryProvider能够看到这个额外的消息了。如下代码所示var agent openAIClient .GetChatClient(model:model) .AsIChatClient() .AsAIAgent(options: new ChatClientAgentOptions { Name delivery-order, AIContextProviders [ memoryProvider, new AdditionalMessageProvider(点餐时既要考虑用户的口味偏好也要考虑菜品多样性以及尽可能与上次订餐有所不同)], ChatOptions new ChatOptions { 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(Alice); await OrderDelivery(Alice);输出------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您搭配好啦 ✅ **清蒸鲈鱼** ×1荤菜清淡不辣鲜嫩爽口 **西芹百合** ×1素菜清爽解腻口感清甜 整体口味清淡、不含辣椒营养搭配均衡一荤一素刚刚好 祝您用餐愉快如果下次想换换口味也可以告诉我偏好 ------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您重新搭配好啦 ✅避开上次的清蒸鲈鱼和西芹百合也不含辣 **番茄炒蛋** ×1酸甜开胃经典家常荤素结合 **清炒菜心** ×1清爽解腻口感脆嫩 这次换了不同搭配依然清淡不辣营养均衡 祝您用餐愉快下次想试试别的风味也可以告诉我 ------------------------------ 来自 Alice 的订单 ------------------------------ 已经为您搭配好新的餐品啦 ✅这次和上次不同更换了菜品保证多样性 **番茄炒蛋** ×1酸甜开胃家常经典不辣 **清炒菜心** ×1清爽脆嫩清淡健康 整体依然是不辣搭配清爽不油腻同时和上次的鲈鱼、西芹百合不同换了新口味 祝您用餐愉快下次如果想吃肉类或者想要更丰富一点也可以告诉我 ------------------------------ 来自 Alice 的订单 ------------------------------ 已为你下单 ✅ 清蒸鲈鱼 ×1清淡鲜美不辣优质蛋白 西芹百合 ×1清爽解腻搭配均衡 考虑到你不能吃辣这次特意避开了辣椒类和酸辣口味同时选择了一荤一素营养搭配也比较清爽不会太油腻。 祝你用餐愉快 ️ ------------------------------ 来自 Alice 的订单 ------------------------------ 已经帮您下单完成 ✅ 本次为您搭配了三道菜兼顾口味和营养也保证菜品多样性 - ️ **剁椒鱼头** ×1鲜辣开胃主菜担当 - **清炒菜心** ×1清爽解腻均衡搭配 - **番茄炒蛋** ×1经典家常酸甜适口 祝您用餐愉快如果有特别想吃的口味比如清淡一点、重辣一点或者想吃肉类为主下次告诉我我会帮您更精准搭配 ️从输出结果可以看出这样的解决方案还是有效果的。但是由于它无法精确地定位上次点餐依然还是会出现两次点餐内容相同的情况。标签: Agent, AI, C#, MAF, Memory免责声明本内容来自平台创作者博客园系信息发布平台仅提供信息存储空间服务。好文要顶 关注我 收藏该文 微信分享Artech粉丝 - 10569 关注 - 0推荐博客加关注10« 上一篇 [MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF» 下一篇 [MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案posted 2026-06-20 08:09 Artech 阅读(100) 评论(0) 收藏 举报