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

绝大部分的Agent都采用对话的方式来和用户进行交互,所以对话的内容就成了Agent决策的基础,对话历史也成为占据LLM上下文窗口的主要内容。LLM推理的质量并非与上下文的丰富程度成正向关系,有时候过多的上下文信息反而会干扰Agent的判断,导致它做出错误的决策。ReducingChatClient就是为了解决这个问题而设计的一个中间件,它通过精减对话内容来帮助Agent更好地理解用户的意图,从而做出更准确的决策。为上下文窗口腾出更多空间也是保证可靠性的一种基本的手段。

1. 利用ReducingChatClient摘要对话内容

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

csharp 复制代码
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条消息进行了摘要,这一切体现在如下的输出中:

markdown 复制代码
请求消息共计4条
1. 用户询问了今天苏州的天气情况,助手回答为晴天。随后用户进一步询问气温,助手回答室外温度为25度。对话围绕苏州当日的天气状况和具体气温展开,内容简洁明确。
2. 有风吗?
3. 西北风4级。
4. 根据天气,给我一些着装建议。


今天苏州**晴天,25℃,西北风4级**,体感会比较清爽,风稍微有点明显。给你一些穿搭建议:

### 👕 上衣
- **短袖T恤、薄衬衫**都可以
- 如果怕风,建议带一件**薄外套/防风夹克**

### 👖 下装
- **牛仔裤、休闲裤**都合适
- 不怕冷的话也可以穿**薄款长裙/半裙**

### 👟 鞋子
- 运动鞋、休闲鞋都很舒服
- 风有点大,尽量避免太轻薄易飘的穿搭

### 🌞 其他建议
- 晴天紫外线可能偏强,出门可以**戴太阳镜、涂防晒**
- 风力4级骑车会有点顶风,注意安全

整体来说是**舒适偏清爽型天气**,穿得轻松一点就好 👍

2. IChatReducer

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

csharp 复制代码
public interface IChatReducer
{
	Task<IEnumerable<ChatMessage>> ReduceAsync(
        IEnumerable<ChatMessage> messages, 
        CancellationToken cancellationToken);
}

2.1 SummarizingChatReducer

SummarizingChatReducerIChatReducer接口的一个实现,它通过生成摘要的方式来对消息列表进行精减。我们在创建SummarizingChatReducer对象的时候,需要传入一个用于生成摘要的IChatClient对象,以及targetCountthreshold两个参数。targetCount表示我们希望在摘要之后保留多少条消息,threshold表示触发摘要的阈值,具体来说当总消息数量>targetCount + threshold时,摘要会被触发。理想状态下,系统会尝试保留最新的targetCount条消息不被摘要,将其余的旧消息进行压缩。

csharp 复制代码
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如何对消息列表进行摘要处理,从而保证在不丢失基本语义的前提下,尽可能地精简消息列表。如下所示的是默认的提示词。

markdown 复制代码
**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参数指定。

csharp 复制代码
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,那么在输出当前传入的消息列表的时候,我们会发现它直接保留了最后的三条消息,而没有对前面的消息进行任何摘要处理。

csharp 复制代码
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}");
markdown 复制代码
请求消息共计3条
1. 有风吗?
2. 西北风4级。
3. 根据天气,给我一些着装建议。


目前是**西北风4级**,风力算是比较明显的,体感温度可能会比实际温度低一些。给你一些穿衣建议:

- ✅ **外套必备**:建议穿一件防风外套、风衣或薄款夹克。
- ✅ **内搭可叠穿**:长袖T恤或薄针织衫比较合适,方便根据冷热增减。
- ✅ **下装**:长裤更舒适,避免被风吹得发凉。
- ✅ **怕冷的话**:可以加一条薄围巾,尤其是西北风通常偏干偏凉。

如果你告诉我现在的气温,我可以给你更具体的搭配建议 😊

3. ReducingChatClient

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

csharp 复制代码
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中间件的时候对其进行一些额外的配置。

csharp 复制代码
public static class ReducingChatClientBuilderExtensions
{
    public static ChatClientBuilder UseChatReducer(
        this ChatClientBuilder builder,
        IChatReducer? reducer = null,
        Action<ReducingChatClient>? configure = null);
}
相关推荐
小满Autumn13 小时前
WPF 进阶:样式、触发器与控件模板
c#·.net·wpf
xixixi7777713 小时前
GPT-5.6(Iris-Alpha)细节泄露 + 国产 AI 芯片最高安全认证落地,全球 AI 格局迎来大变局
大数据·人工智能·gpt·ai·大模型·算力·智能体
Agent手记13 小时前
跨境电商从选品到售后全流程自动化可能吗?基于实在Agent与LLM+RPA的端到端落地实战指南
运维·人工智能·ai·自动化·rpa
程序员柒叔13 小时前
Dify 一周动态-2026-W22
人工智能·大模型·github·agent·知识库·dify
光泽雨13 小时前
C# 扩展方法(Extension Method)在语法上的核心灵魂。
开发语言·c#
影寂ldy13 小时前
C#Lambda表达式
开发语言·c#
GJGCY13 小时前
智能体平台横评|Dify、Coze、阿里云、金智维:技术架构与场景适配深度对比
人工智能·ai·架构·智能体
LoserChaser14 小时前
初识智能体
人工智能·ai·语言模型
夜雪闻竹14 小时前
Tailwind CSS v4 + Vite:现代前端样式方案
前端·css·ai