[MAF预定义ChatClient中间件-04]ReducingChatClient——通过精减对话实施又不丢失基本语义

[MAF预定义ChatClient中间件-04]ReducingChatClient——通过精减对话实施又不丢失基本语义 绝大部分的Agent都采用对话的方式来和用户进行交互所以对话的内容就成了Agent决策的基础对话历史也成为占据LLM上下文窗口的主要内容。LLM推理的质量并非与上下文的丰富程度成正向关系有时候过多的上下文信息反而会干扰Agent的判断导致它做出错误的决策。ReducingChatClient就是为了解决这个问题而设计的一个中间件它通过精减对话内容来帮助Agent更好地理解用户的意图从而做出更准确的决策。为上下文窗口腾出更多空间也是保证可靠性的一种基本的手段。1. 利用ReducingChatClient摘要对话内容如下的程序演示了如何利用ReducingChatClient来部分对话内容进行摘要保证在不丢失基本语义的前提下腾出更多的上下文窗口。如代码片段所示我们基于OpenAIClient创建了一个IChatClient对象并在此基础上利用ChatClientBuilder注册了ReducingChatClient中间件并指定了一个SummarizingChatReducer对象来提供基于摘要的队对话精减功能。我们在创建SummarizingChatReducer对象的时候传入了一个用于对摘要进行生成的ChatClient对象该对象依然是基于OpenAIClient创建的并且使用了相同的模型来生成摘要。我们还为SummarizingChatReducer对象指定了targetCount和threshold两个参数前者表示我们希望在摘要之后保留多少条消息后者则是一个阈值用于触发摘要操作的阈值超过targetCountthreshold。usingAzure;usingdotenv.net;usingMicrosoft.Extensions.AI;usingOpenAI;DotEnv.Load();varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_URL)!;varsummaryClientnewOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model:DeepSeek-V4-Pro).AsIChatClient();varclientnewOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model:gpt-5.2-chat).AsIChatClient().AsBuilder().UseChatReducer(reducer:newSummarizingChatReducer(chatClient:summaryClient,targetCount:3,threshold:1)).Use((messages,options,next,cancelToken){Console.WriteLine($请求消息共计{messages.Count()}条);varindex1;foreach(varmessageinmessages){Console.WriteLine(${index}.{message});}returnnext(messages,options,cancelToken);}).Build();ChatMessage[]messages[newChatMessage(ChatRole.User,今天苏州的天气怎么样),newChatMessage(ChatRole.Assistant,苏州今天是晴天。),newChatMessage(ChatRole.User,气温多少。),newChatMessage(ChatRole.Assistant,室外温度25度。),newChatMessage(ChatRole.User,有风吗),newChatMessage(ChatRole.Assistant,西北风4级。),newChatMessage(ChatRole.User,根据天气给我一些着装建议。)];varresponseawaitclient.GetResponseAsync(messages);Console.WriteLine($\n\n{response});为了查看经过ReducingChatClient精减之后的对话历史我们在ChatClientBuilder中注册了一个简单的中间件来输出当前传入的消息列表。IChatClient管道构建成功之后我们调用GetResponseAsync方法并指定了一组消息共7条来模拟一段对话的历史。由于我们在ReducingChatClient中指定了targetCount为3并且threshold为1必然会触发摘要操作。摘要完成后保留了最后三条消息只对对前4条消息进行了摘要这一切体现在如下的输出中请求消息共计4条 1. 用户询问了今天苏州的天气情况助手回答为晴天。随后用户进一步询问气温助手回答室外温度为25度。对话围绕苏州当日的天气状况和具体气温展开内容简洁明确。 2. 有风吗 3. 西北风4级。 4. 根据天气给我一些着装建议。 今天苏州**晴天25℃西北风4级**体感会比较清爽风稍微有点明显。给你一些穿搭建议 ### 上衣 - **短袖T恤、薄衬衫**都可以 - 如果怕风建议带一件**薄外套/防风夹克** ### 下装 - **牛仔裤、休闲裤**都合适 - 不怕冷的话也可以穿**薄款长裙/半裙** ### 鞋子 - 运动鞋、休闲鞋都很舒服 - 风有点大尽量避免太轻薄易飘的穿搭 ### 其他建议 - 晴天紫外线可能偏强出门可以**戴太阳镜、涂防晒** - 风力4级骑车会有点顶风注意安全 整体来说是**舒适偏清爽型天气**穿得轻松一点就好 2. IChatReducerReducingChatClient的核心是IChatReducer接口我们可以称之为精简器。它定义了一个ReduceAsync方法用于对传入的消息列表进行精减处理。我们可以通过实现IChatReducer接口来定义自己的消息精减策略从而满足不同场景下的需求。publicinterfaceIChatReducer{TaskIEnumerableChatMessageReduceAsync(IEnumerableChatMessagemessages,CancellationTokencancellationToken);}2.1 SummarizingChatReducerSummarizingChatReducer是IChatReducer接口的一个实现它通过生成摘要的方式来对消息列表进行精减。我们在创建SummarizingChatReducer对象的时候需要传入一个用于生成摘要的IChatClient对象以及targetCount和threshold两个参数。targetCount表示我们希望在摘要之后保留多少条消息threshold表示触发摘要的阈值具体来说当总消息数量targetCountthreshold时摘要会被触发。理想状态下系统会尝试保留最新的targetCount条消息不被摘要将其余的旧消息进行压缩。publicsealedclassSummarizingChatReducer:IChatReducer{publicstringSummarizationPrompt{get;set;}publicSummarizingChatReducer(IChatClientchatClient,inttargetCount,int?threshold);publicasyncTaskIEnumerableChatMessageReduceAsync(IEnumerableChatMessagemessages,CancellationTokencancellationToken);}为了防止对话上下文被生硬切断系统在确定从哪条消息开始保留时有两条关键的边界保护规则保持工具调用完整性如果切分点刚好处于工具函数调用或返回结果的中间切分点会向前更旧的消息移动确保函数调用消息包含FunctionCallContext与其响应结果消息包含FunctionResultContent完整保留在同一个作用域内不被摘要拆散避免用户问题孤立在缓冲阈值窗口threshold内系统会向前更旧的消息寻找角色为User的消息。一旦找到就会在用户消息之前切断。这样可以确保用户的提问与其后续的LLM回复、工具调用保存在一起避免问题被摘要但答案被保留的孤立现象。我们可以利用SummarizationPrompt属性来指定一个自定义的提示词来控制摘要的生成。默认情况下SummarizingChatReducer会使用一个预定义的提示词来生成摘要这个提示词会指导ChatClient如何对消息列表进行摘要处理从而保证在不丢失基本语义的前提下尽可能地精简消息列表。如下所示的是默认的提示词。**Generate a clear and complete summary of the entire conversation in no more than five sentences.** The summary must always: - Reflect contributions from both the user and the assistant - Preserve context to support ongoing dialogue - Incorporate any previously provided summary - Emphasize the most relevant and meaningful points The summary must never: - Offer critique, correction, interpretation, or speculation - Highlight errors, misunderstandings, or judgments of accuracy - Comment on events or ideas not present in the conversation - Omit any details included in an earlier summary2.2 MessageCountingChatReducer与SummarizingChatReducer不同MessageCountingChatReducer是一个**纯轻量级、零AI消耗、基于消息数量进行滑动窗口裁剪Sliding Window**的精简器。MessageCountingChatReducer的精简策略简单粗暴直接保留最近的N条消息其中N由targetCount参数指定。publicsealedclassMessageCountingChatReducer:IChatReducer{publicMessageCountingChatReducer(inttargetCount);publicTaskIEnumerableChatMessageReduceAsync(IEnumerableChatMessagemessages,CancellationTokencancellationToken);}两者选择保留消息的策略会不一样MessageCountingChatReducer它会保留最近的targetCount条消息但不包含FunctionCallContent或FunctionResultContent的消息。整个消息列表包含系统消息第一条最旧的那条系统消息会被保留并置于保留消息的最前端后续的系统消息会被直接抹除。系统消息不占用targetCount的配额也就说最多会有targetCount 1条消息被保留SummarizingChatReducer它不会丢弃工具消息。相反它通过向前更旧的消息移动寻找边界确保只要最新的上下文里触发了工具调用整个工具调用链调用 结果就完整地保留在未摘要的消息列表中对于前面的实例如果我们将ReducingChatClient中使用的精简器从SummarizingChatReducer换成MessageCountingChatReducer那么在输出当前传入的消息列表的时候我们会发现它直接保留了最后的三条消息而没有对前面的消息进行任何摘要处理。usingAzure;usingdotenv.net;usingMicrosoft.Extensions.AI;usingOpenAI;DotEnv.Load();varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_URL)!;varsummaryClientnewOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model:gpt-5.2-chat).AsIChatClient();varclientnewOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model:gpt-5.2-chat).AsIChatClient().AsBuilder().UseChatReducer(reducer:newMessageCountingChatReducer(targetCount:3)).Use((messages,options,next,cancelToken){Console.WriteLine($请求消息共计{messages.Count()}条);varindex1;foreach(varmessageinmessages){Console.WriteLine(${index}.{message});}returnnext(messages,options,cancelToken);}).Build();ChatMessage[]messages[newChatMessage(ChatRole.User,今天苏州的天气怎么样),newChatMessage(ChatRole.Assistant,苏州今天是晴天。),newChatMessage(ChatRole.User,气温多少。),newChatMessage(ChatRole.Assistant,室外温度25度。),newChatMessage(ChatRole.User,有风吗),newChatMessage(ChatRole.Assistant,西北风4级。),newChatMessage(ChatRole.User,根据天气给我一些着装建议。)];varresponseawaitclient.GetResponseAsync(messages);Console.WriteLine($\n\n{response});请求消息共计3条 1. 有风吗 2. 西北风4级。 3. 根据天气给我一些着装建议。 目前是**西北风4级**风力算是比较明显的体感温度可能会比实际温度低一些。给你一些穿衣建议 - ✅ **外套必备**建议穿一件防风外套、风衣或薄款夹克。 - ✅ **内搭可叠穿**长袖T恤或薄针织衫比较合适方便根据冷热增减。 - ✅ **下装**长裤更舒适避免被风吹得发凉。 - ✅ **怕冷的话**可以加一条薄围巾尤其是西北风通常偏干偏凉。 如果你告诉我现在的气温我可以给你更具体的搭配建议 3. ReducingChatClientReducingChatClient中间件的实现非常简单它在接收到消息列表之后会调用IChatReducer的ReduceAsync方法来对消息列表进行精减处理然后将精减后的消息列表传递给管道中的下一个中间件或者最终的IChatClient来生成响应。通过这种方式ReducingChatClient能够帮助我们精简对话内容从而腾出更多的上下文窗口来保证LLM推理的质量。publicsealedclassReducingChatClient:DelegatingChatClient{publicReducingChatClient(IChatClientinnerClient,IChatReducerreducer);publicoverrideasyncTaskChatResponseGetResponseAsync(IEnumerableChatMessagemessages,ChatOptions?optionsnull,CancellationTokencancellationTokendefault);publicoverrideasyncIAsyncEnumerableChatResponseUpdateGetStreamingResponseAsync(IEnumerableChatMessagemessages,ChatOptions?optionsnull,CancellationTokencancellationTokendefault);}4. UseChatReducer扩展方法UseChatReducer是一个ChatClientBuilder的扩展方法它提供了一种简便的方式来注册ReducingChatClient中间件。我们只需要在构建IChatClient对象的时候调用UseChatReducer方法并传入一个IChatReducer对象来指定我们想要使用的精简器就可以轻松地将ReducingChatClient中间件添加到我们的IChatClient对象中了。除此之外UseChatReducer方法还提供了一个可选的configure参数它允许我们在注册ReducingChatClient中间件的时候对其进行一些额外的配置。publicstaticclassReducingChatClientBuilderExtensions{publicstaticChatClientBuilderUseChatReducer(thisChatClientBuilderbuilder,IChatReducer?reducernull,ActionReducingChatClient?configurenull);}