
如下的程序演示了如何利用ReducingChatClient来部分对话内容进行摘要保证在不丢失基本语义的前提下腾出更多的上下文窗口。如代码片段所示我们基于OpenAIClient创建了一个IChatClient对象并在此基础上利用ChatClientBuilder注册了ReducingChatClient中间件并指定了一个SummarizingChatReducer对象来提供基于摘要的队对话精减功能。我们在创建SummarizingChatReducer对象的时候传入了一个用于对摘要进行生成的ChatClient对象该对象依然是基于OpenAIClient创建的并且使用了相同的模型来生成摘要。我们还为SummarizingChatReducer对象指定了targetCount和threshold两个参数前者表示我们希望在摘要之后保留多少条消息后者则是一个阈值用于触发摘要操作的阈值超过targetCountthreshold。using Azure;using dotenv.net;using Microsoft.Extensions.AI;using OpenAI;DotEnv.Load();var apiKey Environment.GetEnvironmentVariable(“API_KEY”)!;var endpoint Environment.GetEnvironmentVariable(“OPENAI_URL”)!;var summaryClient new OpenAIClient(credential: new AzureKeyCredential(apiKey),options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }).GetChatClient(model: “DeepSeek-V4-Pro”).AsIChatClient();var client new OpenAIClient(credential: new AzureKeyCredential(apiKey),options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }).GetChatClient(model: “gpt-5.2-chat”).AsIChatClient().AsBuilder().UseChatReducer(reducer: new SummarizingChatReducer(chatClient:summaryClient, targetCount: 3, threshold:1)).Use((messages,options, next, cancelToken) {Console.WriteLine(KaTeX parse error: Expected }, got EOF at end of input: …sole.WriteLine(“{index}. {message}”);}return next(messages, options, cancelToken);}).Build();ChatMessage[] messages [new ChatMessage(ChatRole.User, “今天苏州的天气怎么样”),new ChatMessage(ChatRole.Assistant, “苏州今天是晴天。”),new ChatMessage(ChatRole.User, “气温多少。”),new ChatMessage(ChatRole.Assistant, “室外温度25度。”),new ChatMessage(ChatRole.User, “有风吗”),new ChatMessage(ChatRole.Assistant, “西北风4级。”),new ChatMessage(ChatRole.User, “根据天气给我一些着装建议。”)];var response await client.GetResponseAsync(messages);Console.WriteLine($“\n\n{response}”);为了查看经过ReducingChatClient精减之后的对话历史我们在ChatClientBuilder中注册了一个简单的中间件来输出当前传入的消息列表。IChatClient管道构建成功之后我们调用GetResponseAsync方法并指定了一组消息共7条来模拟一段对话的历史。由于我们在ReducingChatClient中指定了targetCount为3并且threshold为1必然会触发摘要操作。摘要完成后保留了最后三条消息只对对前4条消息进行了摘要这一切体现在如下的输出中请求消息共计4条用户询问了今天苏州的天气情况助手回答为晴天。随后用户进一步询问气温助手回答室外温度为25度。对话围绕苏州当日的天气状况和具体气温展开内容简洁明确。有风吗西北风4级。根据天气给我一些着装建议。今天苏州晴天25℃西北风4级体感会比较清爽风稍微有点明显。给你一些穿搭建议 上衣短袖T恤、薄衬衫都可以如果怕风建议带一件薄外套/防风夹克 下装牛仔裤、休闲裤都合适不怕冷的话也可以穿薄款长裙/半裙 鞋子运动鞋、休闲鞋都很舒服风有点大尽量避免太轻薄易飘的穿搭 其他建议晴天紫外线可能偏强出门可以戴太阳镜、涂防晒风力4级骑车会有点顶风注意安全整体来说是舒适偏清爽型天气穿得轻松一点就好 2. IChatReducerReducingChatClient的核心是IChatReducer接口我们可以称之为精简器。它定义了一个ReduceAsync方法用于对传入的消息列表进行精减处理。我们可以通过实现IChatReducer接口来定义自己的消息精减策略从而满足不同场景下的需求。public interface IChatReducer{TaskIEnumerable ReduceAsync(IEnumerable messages,CancellationToken cancellationToken);}2.1 SummarizingChatReducerSummarizingChatReducer是IChatReducer接口的一个实现它通过生成摘要的方式来对消息列表进行精减。我们在创建SummarizingChatReducer对象的时候需要传入一个用于生成摘要的IChatClient对象以及targetCount和threshold两个参数。targetCount表示我们希望在摘要之后保留多少条消息threshold表示触发摘要的阈值具体来说当总消息数量targetCount threshold时摘要会被触发。理想状态下系统会尝试保留最新的targetCount条消息不被摘要将其余的旧消息进行压缩。public sealed class SummarizingChatReducer : IChatReducer{public string SummarizationPrompt{ get; set;}public SummarizingChatReducer(IChatClient chatClient, int targetCount, int? threshold);public async TaskIEnumerable ReduceAsync(IEnumerable messages, CancellationToken cancellationToken);}为了防止对话上下文被生硬切断系统在确定从哪条消息开始保留时有两条关键的边界保护规则保持工具调用完整性如果切分点刚好处于工具函数调用或返回结果的中间切分点会向前更旧的消息移动确保函数调用消息包含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 assistantPreserve context to support ongoing dialogueIncorporate any previously provided summaryEmphasize the most relevant and meaningful pointsThe summary must never:Offer critique, correction, interpretation, or speculationHighlight errors, misunderstandings, or judgments of accuracyComment on events or ideas not present in the conversationOmit any details included in an earlier summary2.2 MessageCountingChatReducer与SummarizingChatReducer不同MessageCountingChatReducer是一个纯轻量级、零AI消耗、基于消息数量进行滑动窗口裁剪Sliding Window的精简器。MessageCountingChatReducer的精简策略简单粗暴直接保留最近的N条消息其中N由targetCount参数指定。public sealed class MessageCountingChatReducer : IChatReducer{public MessageCountingChatReducer(int targetCount);public TaskIEnumerable ReduceAsync(IEnumerable messages, CancellationToken cancellationToken);}两者选择保留消息的策略会不一样MessageCountingChatReducer它会保留最近的targetCount条消息但不包含FunctionCallContent 或FunctionResultContent的消息。整个消息列表包含系统消息第一条最旧的那条系统消息会被保留并置于保留消息的最前端后续的系统消息会被直接抹除。系统消息不占用targetCount的配额也就说最多会有targetCount 1条消息被保留SummarizingChatReducer它不会丢弃工具消息。相反它通过向前更旧的消息移动寻找边界确保只要最新的上下文里触发了工具调用整个工具调用链调用 结果就完整地保留在未摘要的消息列表中对于前面的实例如果我们将ReducingChatClient中使用的精简器从SummarizingChatReducer换成MessageCountingChatReducer那么在输出当前传入的消息列表的时候我们会发现它直接保留了最后的三条消息而没有对前面的消息进行任何摘要处理。using Azure;using dotenv.net;using Microsoft.Extensions.AI;using OpenAI;DotEnv.Load();var apiKey Environment.GetEnvironmentVariable(“API_KEY”)!;var endpoint Environment.GetEnvironmentVariable(“OPENAI_URL”)!;var summaryClient new OpenAIClient(credential: new AzureKeyCredential(apiKey),options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }).GetChatClient(model: “gpt-5.2-chat”).AsIChatClient();var client new OpenAIClient(credential: new AzureKeyCredential(apiKey),options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }).GetChatClient(model: “gpt-5.2-chat”).AsIChatClient().AsBuilder().UseChatReducer(reducer: new MessageCountingChatReducer(targetCount: 3)).Use((messages,options, next, cancelToken) {Console.WriteLine(KaTeX parse error: Expected }, got EOF at end of input: …sole.WriteLine(“{index}. {message}”);}return next(messages, options, cancelToken);}).Build();ChatMessage[] messages [new ChatMessage(ChatRole.User, “今天苏州的天气怎么样”),new ChatMessage(ChatRole.Assistant, “苏州今天是晴天。”),new ChatMessage(ChatRole.User, “气温多少。”),new ChatMessage(ChatRole.Assistant, “室外温度25度。”),new ChatMessage(ChatRole.User, “有风吗”),new ChatMessage(ChatRole.Assistant, “西北风4级。”),new ChatMessage(ChatRole.User, “根据天气给我一些着装建议。”)];