当前位置: 首页 > news >正文

MAF预定义ChatClient中间件-04]ReducingChatClient——精减对话历史又不丢失基本语义

利用ReducingChatClient摘要对话内容

如下的程序演示了如何利用ReducingChatClient来部分对话内容进行摘要,保证在不丢失基本语义的前提下,腾出更多的上下文窗口。如代码片段所示,我们基于OpenAIClient创建了一个IChatClient对象,并在此基础上利用ChatClientBuilder注册了ReducingChatClient中间件,并指定了一个SummarizingChatReducer对象来提供基于摘要的队对话精减功能。我们在创建SummarizingChatReducer对象的时候,传入了一个用于对摘要进行生成的ChatClient对象,该对象依然是基于OpenAIClient创建的,并且使用了相同的模型来生成摘要。我们还为SummarizingChatReducer对象指定了targetCountthreshold两个参数,前者表示我们希望在摘要之后保留多少条消息,后者则是一个阈值,用于触发摘要操作的阈值(超过targetCount+threshold)。

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( $"请求消息共计{messages.Count()}条"); var index = 1; foreach (var message in messages) { Console.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条 1. 用户询问了今天苏州的天气情况,助手回答为晴天。随后用户进一步询问气温,助手回答室外温度为25度。对话围绕苏州当日的天气状况和具体气温展开,内容简洁明确。 2. 有风吗? 3. 西北风4级。 4. 根据天气,给我一些着装建议。 今天苏州**晴天,25℃,西北风4级**,体感会比较清爽,风稍微有点明显。给你一些穿搭建议: ### 👕 上衣 - **短袖T恤、薄衬衫**都可以 - 如果怕风,建议带一件**薄外套/防风夹克** ### 👖 下装 - **牛仔裤、休闲裤**都合适 - 不怕冷的话也可以穿**薄款长裙/半裙** ### 👟 鞋子 - 运动鞋、休闲鞋都很舒服 - 风有点大,尽量避免太轻薄易飘的穿搭 ### 🌞 其他建议 - 晴天紫外线可能偏强,出门可以**戴太阳镜、涂防晒** - 风力4级骑车会有点顶风,注意安全 整体来说是**舒适偏清爽型天气**,穿得轻松一点就好 👍

2. IChatReducer

ReducingChatClient的核心是IChatReducer接口,我们可以称之为精简器。它定义了一个ReduceAsync方法,用于对传入的消息列表进行精减处理。我们可以通过实现IChatReducer接口来定义自己的消息精减策略,从而满足不同场景下的需求。

public interface IChatReducer { Task<IEnumerable<ChatMessage>> ReduceAsync( IEnumerable<ChatMessage> messages, CancellationToken cancellationToken); }

2.1 SummarizingChatReducer

SummarizingChatReducerIChatReducer接口的一个实现,它通过生成摘要的方式来对消息列表进行精减。我们在创建SummarizingChatReducer对象的时候,需要传入一个用于生成摘要的IChatClient对象,以及targetCountthreshold两个参数。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 Task<IEnumerable<ChatMessage>> ReduceAsync(IEnumerable<ChatMessage> 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 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 summary

2.2 MessageCountingChatReducer

SummarizingChatReducer不同,MessageCountingChatReducer是一个纯轻量级、零AI消耗、基于消息数量进行滑动窗口裁剪(Sliding Window)的精简器。MessageCountingChatReducer的精简策略简单粗暴,直接保留最近的N条消息,其中N由targetCount参数指定。

public sealed class MessageCountingChatReducer : IChatReducer { public MessageCountingChatReducer(int targetCount); public Task<IEnumerable<ChatMessage>> ReduceAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken); }

两者选择保留消息的策略会不一样:

  • MessageCountingChatReducer:它会保留最近的targetCount条消息,但不包含FunctionCallContentFunctionResultContent的消息。整个消息列表包含系统消息,第一条(最旧的那条系统消息)会被保留,并置于保留消息的最前端,后续的系统消息会被直接抹除。系统消息不占用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( $"请求消息共计{messages.Count()}条"); var index = 1; foreach (var message in messages) { Console.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}");
请求消息共计3条 1. 有风吗? 2. 西北风4级。 3. 根据天气,给我一些着装建议。 目前是**西北风4级**,风力算是比较明显的,体感温度可能会比实际温度低一些。给你一些穿衣建议: - ✅ **外套必备**:建议穿一件防风外套、风衣或薄款夹克。 - ✅ **内搭可叠穿**:长袖T恤或薄针织衫比较合适,方便根据冷热增减。 - ✅ **下装**:长裤更舒适,避免被风吹得发凉。 - ✅ **怕冷的话**:可以加一条薄围巾,尤其是西北风通常偏干偏凉。 如果你告诉我现在的气温,我可以给你更具体的搭配建议 😊

3. ReducingChatClient

ReducingChatClient中间件的实现非常简单,它在接收到消息列表之后会调用IChatReducerReduceAsync方法来对消息列表进行精减处理,然后将精减后的消息列表传递给管道中的下一个中间件或者最终的IChatClient来生成响应。通过这种方式,ReducingChatClient能够帮助我们精简对话内容,从而腾出更多的上下文窗口来保证LLM推理的质量。

public sealed class ReducingChatClient : DelegatingChatClient { public ReducingChatClient(IChatClient innerClient, IChatReducer reducer); public override async Task<ChatResponse> GetResponseAsync( IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default); public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync( IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default); }

4. UseChatReducer扩展方法

UseChatReducer是一个ChatClientBuilder的扩展方法,它提供了一种简便的方式来注册ReducingChatClient中间件。我们只需要在构建IChatClient对象的时候调用UseChatReducer方法,并传入一个IChatReducer对象来指定我们想要使用的精简器,就可以轻松地将ReducingChatClient中间件添加到我们的IChatClient对象中了。除此之外,UseChatReducer方法还提供了一个可选的configure参数,它允许我们在注册ReducingChatClient中间件的时候对其进行一些额外的配置。

public static class ReducingChatClientBuilderExtensions { public static ChatClientBuilder UseChatReducer( this ChatClientBuilder builder, IChatReducer? reducer = null, Action<ReducingChatClient>? configure = null); }
http://www.jsqmd.com/news/1091302/

相关文章:

  • DNS在线验证工具、在线查询、DNS地址查询、DNS验证、DNS查询
  • TI TLK10xL以太网PHY芯片MII/RMII接口时序与硬件设计实战指南
  • 免费开源ModBus调试工具QModMaster:5分钟快速上手完整指南
  • AI 哲学故事系列 · 第二讲:AI 是否有评判心
  • 开关电源模块全套测试项目总结
  • 上海人工智能实验室新论文:不换模型也能变强?MinerU2.5-Pro 把答案藏在样本里
  • COM3D2 MaidFiddler实时编辑器:5分钟掌握终极女仆定制技巧
  • 好用的水下电机怎么挑?水下电机如何选——基于低压智能路线的工程化观察
  • 解决AI翻译模型部署复杂性的技术挑战:Sakura启动器GUI架构解析与实施指南
  • 成都买茶叶店铺推荐:新手如何根据口感与场景选茶
  • ppt模板_0126_彩色话框
  • 数据加密传输
  • 从理论到实践:基于混合整数二阶锥规划的主动配电网优化运行全流程解析与代码实现
  • 【openpyxl】从数据到洞察:用折线图动态呈现销售趋势
  • 我把那个迭代了 18 个版本的 SDK 整个掀翻重写了:stock-sdk v2 升级手记
  • 《计算机网络自顶向下》Wireshark实验:TCP连接与数据传输深度剖析
  • NukeSurvivalToolkit终极指南:292个专业插件如何让Nuke合成效率提升300%
  • 免费开源CPU优化神器CPUDoc:让你的电脑性能瞬间提升30%
  • Embedding向量一致性失效危机:当同一文本两次API调用余弦相似度<0.93——你必须在下次部署前验证的2个隐藏配置
  • Memtest86+:终极内存诊断工具,彻底解决电脑蓝屏死机问题
  • 语谱图(二)从频谱到声景:STFT的工程实践与调优解析
  • 第一章Netty,NIO阻塞和非阻塞模式,代码效果演示
  • Minecraft区块修复工具完全指南:拯救损坏的游戏世界
  • 前端可视化开发实战
  • Cursor免费试用限制深度解析:从设备指纹识别到一键重置的完整方案
  • Python QQ机器人开发实战:3步构建智能消息处理系统
  • MTK车机开机动画深度定制:从提取、解包到刷入的完整实战
  • macos支持的画质修复软件有哪些?5款Mac剪辑横评实测
  • Windows 11系统优化终极指南:使用Win11Debloat实现高效清理与性能提升
  • Gmail账号自动生成器:Python脚本快速创建随机邮箱的完整教程