在上一篇文章中,我们介绍了CompactionProvider的设计、实现原理和基本用法。我们知道CompactionProvider最终利用注册的用于表示压缩策略的CompactionStrategy对象来对对话历史进行压缩处理。CompactionStrategy是对压缩策略的抽象表示,MAF内置了一系列常用的压缩策略,接下来我们将逐个认识它们。为了演示方便,我们定义了如下两个辅助方法:
staticCompactionMessageIndexCreateMessageIndex(List<ChatMessage>messages){varmethod=typeof(CompactionMessageIndex).GetMethod("Create",BindingFlags.NonPublic|BindingFlags.Static)!;return(CompactionMessageIndex)method.Invoke(null,[messages,null])!;}staticvoidPrintIncludedMessages(CompactionMessageIndexmessageIndex){varindex=1;foreach(varmessageinmessageIndex.GetIncludedMessages()){varrole=message.Role;varcontent=message.Contents.Single();varline=contentswitch{TextContenttextContent=>$"[{role}]{textContent.Text}",FunctionCallContentfunctionCall=>$"[{role}]Function Call:{functionCall.Name}",FunctionResultContentfunctionResult=>$"[{role}]Function Result:{functionResult.Result}",_=>"Unknown Content Type"};Console.WriteLine($"{index}.{line}");index++;}}CreateMessageIndex以反射的方式调用静态的内部方法Create根据指定的消息列表来创建一个CompactionMessageIndex对象,我们会将创建的这个对象作为参数交给对应的CompactionStrategy对象实施压缩。PrintIncludedMessages方法则是用来输出当前消息索引中未被排除在外的消息列表,我们利用它输出压缩后的消息列表。
1. TruncationCompactionStrategy
TruncationCompactionStrategy采用直接截断的压缩策略。简单来说,它的核心逻辑是末位淘汰。当有效消息组的数量超过设定的阈值时,它会**从旧到新(从前到后)**删除超出的部分,只保留最新指定数量的消息组。
publicsealedclassTruncationCompactionStrategy:CompactionStrategy{publicintMinimumPreservedGroups{get;}publicTruncationCompactionStrategy(CompactionTriggertrigger,intminimumPreservedGroups=32,CompactionTrigger?target=null);protectedoverrideValueTask<bool>CompactCoreAsync(CompactionMessageIndexindex,ILoggerlogger,CancellationTokencancellationToken);}在下面的演示程序中,我们创建了一个包含9条消息的消息列表。根据前面针对消息组的划分,这9条消息会被划分成8个消息组(针对工具调用的Assistant和Tool消息会被组合成一个消息组)。我们将TruncationCompactionStrategy的触发条件设置为当消息组总数超过6条时触发压缩操作,并且至少保留最近的4条消息组不被压缩。
List<ChatMessage>history=[new(ChatRole.System,"你说一个聪明细心的私人助理"),new(ChatRole.Assistant,"[摘要]用户问询明后两天的工作安排,得到的答复:明后两天日程没有任何安排。"),new(ChatRole.User,"明天天气如何?"),new(ChatRole.Assistant,"阴,气温17度。"),new(ChatRole.User,"后天呢?"),new(ChatRole.Assistant,[newFunctionCallContent("call_123","get_weather")]),new(ChatRole.Tool,[newFunctionResultContent("call_123","多云,气温25度。")]),new(ChatRole.Assistant,"后天多云,气温25度。"),new(ChatRole.User,"明天组织家人就近外出交流,请制定一份方案出来。")];varproperties=history[1].AdditionalProperties??=[];properties[CompactionMessageGroup.SummaryPropertyKey]=true;varstrategy=newTruncationCompactionStrategy(trigger:messageIndex=>messageIndex.TotalMessageCount>6,minimumPreservedGroups:4);varmessageIndex=CreateMessageIndex(history);awaitstrategy.CompactAsync(messageIndex);PrintIncludedMessages(messageIndex);输出:
1.[system]你说一个聪明细心的私人助理2.[user]后天呢?3.[assistant]FunctionCall:get_weather4.[tool]FunctionResult:多云,气温25度。5.[assistant]后天多云,气温25度。6.[user]明天组织家人就近外出交流,请制定一份方案出来。由于系统消息总是会被保留下来,并且不占用保留消息的配额。所以最终5个用户组,共计6条消息被保留。
SlidingWindowCompactionStrategy
SlidingWindowCompactionStrategy采用滑动窗口压缩策略。与之前按消息组数量截断的策略不同,它是基于TurnIndex(轮次/对话回合索引)来管理数据的。它实现了一个基于回合的滑动窗口清理机制,非常类似于AI聊天应用中只保留最近几轮对话历史,其余旧对话清理掉的逻辑。如果存在TurnIndex == 0,则将其加入保护名单。这通常是为了强制保留配置信息、系统提示词和开场白。如下面的代码所示,我们在创建SlidingWindowCompactionStrategy对象通过构造函数的参数minimumPreservedTurns来指定我们想要保留的轮次数量。
publicsealedclassSlidingWindowCompactionStrategy:CompactionStrategy{publicintMinimumPreservedTurns{get;}publicSlidingWindowCompactionStrategy(CompactionTriggertrigger,intminimumPreservedTurns=1,CompactionTrigger?target=null);protectedoverrideValueTask<bool>CompactCoreAsync(CompactionMessageIndexindex,ILoggerlogger,CancellationTokencancellationToken);}我们直接将前面演示实例中使用的TruncationCompactionStrategy替换成SlidingWindowCompactionStrategy,并将MinimumPreservedTurns设置为2,其他的触发条件保持不变。
usingMicrosoft.Agents.AI.Compaction;usingMicrosoft.Extensions.AI;usingSystem.Reflection;List<ChatMessage>history=[new(ChatRole.System,"你说一个聪明细心的私人助理"),new(ChatRole.Assistant,"[摘要]用户问询明后两天的工作安排,得到的答复:明后两天日程没有任何安排。"),new(ChatRole.User,"明天天气如何?"),new(ChatRole.Assistant,"阴,气温17度。"),new(ChatRole.User,"后天呢?"),new(ChatRole.Assistant,[newFunctionCallContent("call_123","get_weather")]),new(ChatRole.Tool,[newFunctionResultContent("call_123","多云,气温25度。")]),new(ChatRole.Assistant,"后天多云,气温25度。"),new(ChatRole.User,"明天组织家人就近外出交流,请制定一份方案出来。")];varproperties=history[1].AdditionalProperties??=[];properties[CompactionMessageGroup.SummaryPropertyKey]=true;varstrategy=newSlidingWindowCompactionStrategy(trigger:messageIndex=>messageIndex.TotalMessageCount>6,minimumPreservedTurns:2);varmessageIndex=CreateMessageIndex(history);awaitstrategy.CompactAsync(messageIndex);PrintIncludedMessages(messageIndex);输出:
1. [system]你说一个聪明细心的私人助理 2. [assistant][摘要]用户问询明后两天的工作安排,得到的答复:明后两天日程没有任何安排。 3. [user]后天呢? 4. [assistant]Function Call: get_weather 5. [tool]Function Result: 多云,气温25度。 6. [assistant]后天多云,气温25度。 7. [user]明天组织家人就近外出交流,请制定一份方案出来。由于整个对话历史涉及三条User消息,所以总共有三轮对话。根据滑动窗口的逻辑,第一轮对话(系统消息和第一条Assistant消息)会被清理掉,而第二轮和第三轮对话,以及TurnIndex=0的系统消息和摘要消息会被保留下来。
3. ToolResultCompactionStrategy
ToolResultCompactionStrategy是专门针对工具调用结果的压缩策略。与前两个直接丢弃/删除数据的截断策略不同,这是一个折叠/摘要策略。它专门针对大模型(LLM)对话中高频出现且极其消耗Token的工具调用相关的消息。它的核心逻辑是将超出安全窗口(保留数外)的复杂工具调用数据,就地压缩成一条简化的纯文本摘要,既释放了大量Token,又为模型保留了曾调用过某工具并得到某结果的上下文线索。
publicsealedclassToolResultCompactionStrategy:CompactionStrategy{publicintMinimumPreservedGroups{get;}publicFunc<CompactionMessageGroup,string>?ToolCallFormatter{get;init;}publicToolResultCompactionStrategy(CompactionTriggertrigger,intminimumPreservedGroups=16,CompactionTrigger?target=null);protectedoverrideValueTask<bool>CompactCoreAsync(CompactionMessageIndexindex,ILoggerlogger,CancellationTokencancellationToken);publicstaticstringDefaultToolCallFormatter(CompactionMessageGroupgroup);}我们在创建ToolResultCompactionStrategy对象的时候,可以通过构造函数的参数minimumPreservedGroups来指定我们想要保留的消息组数量。除此之外,我们还可以通过ToolCallFormatter属性来指定一个委托对象,用于自定义工具调用消息被压缩成摘要消息时的格式。这个委托对象接受一个CompactionMessageGroup对象作为参数,并返回一个字符串作为摘要消息的内容。如果没有显式提供ToolCallFormatter,系统会使用默认的DefaultToolCallFormatter方法来生成摘要消息。
在如下的演示程序中,我们在现有的对话历史基础上,将原来针对get_weather工具调用的Assistant和Tool消息替换成了针对get_weather_conditions、get_temperature、get_precipitation和get_wind四个工具调用的消息。我们将ToolResultCompactionStrategy的触发条件设置为当消息组总数超过6条时触发压缩操作,并且至少保留最近的2条消息组不被压缩。
List<ChatMessage>history=[new(ChatRole.System,"你说一个聪明细心的私人助理"),new(ChatRole.Assistant,"[摘要]用户问询明后两天的工作安排,得到的答复:明后两天日程没有任何安排。"),new(ChatRole.User,"明天天气如何?"),new(ChatRole.Assistant,"阴,气温17度。"),new(ChatRole.User,"后天呢?"),new(ChatRole.Assistant,[newFunctionCallContent("call_001","get_weather_conditions"),newFunctionCallContent("call_002","get_temperature"),newFunctionCallContent("call_003","get_precipitation"),newFunctionCallContent("call_004","get_wind"),]),new(ChatRole.Tool,[newFunctionResultContent("call_001","多云")]),new(ChatRole.Tool,[newFunctionResultContent("call_002","25度")]),new(ChatRole.Tool,[newFunctionResultContent("call_003","无降水")]),new(ChatRole.Tool,[newFunctionResultContent("call_004","微风2-3级")]),new(ChatRole.Assistant,"后天多云,气温25度。"),new(ChatRole.User,"明天组织家人就近外出交流,请制定一份方案出来。")];varproperties=history[1].AdditionalProperties??=[];properties[CompactionMessageGroup.SummaryPropertyKey]=true;varstrategy=newToolResultCompactionStrategy(trigger:messageIndex=>messageIndex.TotalMessageCount>6,minimumPreservedGroups:2);varmessageIndex=CreateMessageIndex(history);awaitstrategy.CompactAsync(messageIndex);PrintIncludedMessages(messageIndex);输出:
1. [system]你说一个聪明细心的私人助理 2. [assistant][摘要]用户问询明后两天的工作安排,得到的答复:明后两天日程没有任何安排。 3. [user]明天天气如何? 4. [assistant]阴,气温17度。 5. [user]后天呢? 6. [assistant][Tool Calls] get_weather_conditions: - 多云 get_temperature: - 17度 get_precipitation: - 无降水 get_wind: - 微风2-3级 7. [assistant]后天多云,气温25度。 8. [user]明天组织家人就近外出交流,请制定一份方案出来。可以看出针对工具调用的消息被压缩成了一条摘要消息,虽然丢失了部分细节信息,但保留了工具调用的核心线索,极大地释放了Token空间。
4. ChatReducerCompactionStrategy
ChatReducerCompactionStrategy代表针对聊天内容缩减/规约的压缩策略。与前三个策略(截断、滑动窗口、工具调用折叠)完全不同,这是一个基于外部智能算法的重构式压缩策略。它自己不决定删除或折叠哪些消息,而是将所有当前有效的聊天历史打包,交给一个实现了IChatReducer接口的外部大模型或算法引擎去进行智能提炼与重写,最后用重写后的精简历史彻底替换原有的历史。
publicsealedclassChatReducerCompactionStrategy:CompactionStrategy{publicIChatReducerChatReducer{get;}publicChatReducerCompactionStrategy(IChatReducerchatReducer,CompactionTriggertrigger);protectedoverrideasyncValueTask<bool>CompactCoreAsync(CompactionMessageIndexindex,ILoggerlogger,CancellationTokencancellationToken);}publicinterfaceIChatReducer{Task<IEnumerable<ChatMessage>>ReduceAsync(IEnumerable<ChatMessage>messages,CancellationTokencancellationToken);}MAF内置了两种实现了IChatReducer接口的精简器,分别是MessageCountingChatReducer和SummarizingChatReducer。前者采用简单粗暴的计数方式来保留最近的消息,后者则通过调用一个外部的IChatClient来生成摘要消息,从而实现更智能的压缩效果。
publicsealedclassMessageCountingChatReducer:IChatReducer{publicMessageCountingChatReducer(inttargetCount);publicTask<IEnumerable<ChatMessage>>ReduceAsync(IEnumerable<ChatMessage>messages,CancellationTokencancellationToken);}publicsealedclassSummarizingChatReducer:IChatReducer{publicstringSummarizationPrompt{get;set;}publicSummarizingChatReducer(IChatClientchatClient,inttargetCount,int?threshold);publicasyncTask<IEnumerable<ChatMessage>>ReduceAsync(IEnumerable<ChatMessage>messages,CancellationTokencancellationToken);}5. ChatReducerCompactionStrategy
基于陈旧消息组的摘要要锁可以采用ChatReducerCompactionStrategy借助SummarizingChatReducer来完成,也可以直接使用SummarizationCompactionStrategy来实现。我们在构造SummarizationCompactionStrategy对象的时候,需要指定一个调用LLM实施摘要的IChatClient对象,以及对应的提示词。上一篇文章演示的就是这种压缩策略。
publicsealedclassSummarizationCompactionStrategy:CompactionStrategy{publicIChatClientChatClient{get;}publicintMinimumPreservedGroups{get;}publicstringSummarizationPrompt{get;}publicSummarizationCompactionStrategy(IChatClientchatClient,CompactionTriggertrigger,intminimumPreservedGroups=8,string?summarizationPrompt=null,CompactionTrigger?target=null);protectedoverrideasyncValueTask<bool>CompactCoreAsync(CompactionMessageIndexindex,ILoggerlogger,CancellationTokencancellationToken);}如果没有显式提供摘要提示词,默认采用如下这段文本:
You are a conversation summarizer. Produce a concise summary of the conversation that preserves: - Key facts, decisions, and user preferences - Important context needed for future turns - Tool call outcomes and their significance Omit pleasantries and redundant exchanges. Be factual and brief.6. PipelineCompactionStrategy
PipelineCompactionStrategy采用流水线压缩策略。它在整压缩过程中扮演的是容器的角色。它自身不包含任何具体的删除、折叠或缩减消息的算法,而是采用了组合模式,将前面介绍过的单一策略(如滑动窗口、工具折叠、ChatReducer等)串联成一条有顺序的流水线来协同工作。
publicsealedclassPipelineCompactionStrategy:CompactionStrategy{publicIReadOnlyList<CompactionStrategy>Strategies{get;}publicPipelineCompactionStrategy(paramsIEnumerable<CompactionStrategy>strategies);protectedoverrideasyncValueTask<bool>CompactCoreAsync(CompactionMessageIndexindex,ILoggerlogger,CancellationTokencancellationToken);}