欢迎光临
我们一直在努力

[MAF预定义的AIContextProvider-06]CompactionProvider——采用多种策略压缩对话历史[续]

在上一篇文章中,我们介绍了CompactionProvider的设计、实现原理和基本用法。我们知道CompactionProvider最终利用注册的用于表示压缩策略的CompactionStrategy对象来对对话历史进行压缩处理。CompactionStrategy是对压缩策略的抽象表示,MAF内置了一系列常用的压缩策略,接下来我们将逐个认识它们。为了演示方便,我们定义了如下两个辅助方法:

static CompactionMessageIndex CreateMessageIndex(List<ChatMessage> messages)
{
var method = typeof(CompactionMessageIndex).GetMethod("Create", BindingFlags.NonPublic | BindingFlags.Static)!;
return (CompactionMessageIndex)method.Invoke(null, [messages, null])!;
}

static void PrintIncludedMessages(CompactionMessageIndex messageIndex)
{
var index = 1;
foreach (var message in messageIndex.GetIncludedMessages())
{
var role = message.Role;
var content = message.Contents.Single();
var line = content switch
{
TextContent textContent => $"[{role}]{textContent.Text}",
FunctionCallContent functionCall => $"[{role}]Function Call: {functionCall.Name}",
FunctionResultContent functionResult => $"[{role}]Function Result: {functionResult.Result}",
_ => "Unknown Content Type"
};
Console.WriteLine($"{index}. {line}");
index++;
}
}

CreateMessageIndex以反射的方式调用静态的内部方法Create根据指定的消息列表来创建一个CompactionMessageIndex对象,我们会将创建的这个对象作为参数交给对应的CompactionStrategy对象实施压缩。PrintIncludedMessages方法则是用来输出当前消息索引中未被排除在外的消息列表,我们利用它输出压缩后的消息列表。

1. TruncationCompactionStrategy

TruncationCompactionStrategy采用直接截断的压缩策略。简单来说,它的核心逻辑是末位淘汰。当有效消息组的数量超过设定的阈值时,它会**从旧到新(从前到后)**删除超出的部分,只保留最新指定数量的消息组。

public sealed class TruncationCompactionStrategy : CompactionStrategy
{
public int MinimumPreservedGroups { get; }
public TruncationCompactionStrategy(
CompactionTrigger trigger,
int minimumPreservedGroups = 32,
CompactionTrigger? target = null);
protected override ValueTask<bool> CompactCoreAsync(
CompactionMessageIndex index,
ILogger logger,
CancellationToken cancellationToken);
}

在下面的演示程序中,我们创建了一个包含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, [new FunctionCallContent("call_123","get_weather")]),
new (ChatRole.Tool,[new FunctionResultContent("call_123","多云,气温25度。")]),
new (ChatRole.Assistant,"后天多云,气温25度。"),
new (ChatRole.User,"明天组织家人就近外出交流,请制定一份方案出来。")
];
var properties = history[1].AdditionalProperties ??= [];
properties[CompactionMessageGroup.SummaryPropertyKey] = true;

var strategy = new TruncationCompactionStrategy(
trigger: messageIndex => messageIndex.TotalMessageCount > 6,
minimumPreservedGroups: 4);
var messageIndex = CreateMessageIndex(history);
await strategy.CompactAsync(messageIndex);
PrintIncludedMessages(messageIndex);

输出:

1. [system]你说一个聪明细心的私人助理
2. [user]后天呢?
3. [assistant]Function Call: get_weather
4. [tool]Function Result: 多云,气温25度。
5. [assistant]后天多云,气温25度。
6. [user]明天组织家人就近外出交流,请制定一份方案出来。

由于系统消息总是会被保留下来,并且不占用保留消息的配额。所以最终5个用户组,共计6条消息被保留。

SlidingWindowCompactionStrategy

SlidingWindowCompactionStrategy采用滑动窗口压缩策略。与之前按消息组数量截断的策略不同,它是基于TurnIndex(轮次/对话回合索引)来管理数据的。它实现了一个基于回合的滑动窗口清理机制,非常类似于AI聊天应用中只保留最近几轮对话历史,其余旧对话清理掉的逻辑。如果存在 TurnIndex == 0,则将其加入保护名单。这通常是为了强制保留配置信息、系统提示词和开场白。如下面的代码所示,我们在创建SlidingWindowCompactionStrategy对象通过构造函数的参数minimumPreservedTurns来指定我们想要保留的轮次数量。

public sealed class SlidingWindowCompactionStrategy : CompactionStrategy
{
public int MinimumPreservedTurns { get; }
public SlidingWindowCompactionStrategy(
CompactionTrigger trigger,
int minimumPreservedTurns = 1, CompactionTrigger? target = null);

protected override ValueTask<bool> CompactCoreAsync(
CompactionMessageIndex index,
ILogger logger,
CancellationToken cancellationToken);
}

我们直接将前面演示实例中使用的TruncationCompactionStrategy替换成SlidingWindowCompactionStrategy,并将MinimumPreservedTurns设置为2,其他的触发条件保持不变。

using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
using System.Reflection;

List<ChatMessage> history = [
new (ChatRole.System, "你说一个聪明细心的私人助理"),
new (ChatRole.Assistant,"[摘要]用户问询明后两天的工作安排,得到的答复:明后两天日程没有任何安排。"),
new (ChatRole.User,"明天天气如何?"),
new (ChatRole.Assistant,"阴,气温17度。"),
new (ChatRole.User,"后天呢?"),
new (ChatRole.Assistant, [new FunctionCallContent("call_123","get_weather")]),
new (ChatRole.Tool,[new FunctionResultContent("call_123","多云,气温25度。")]),
new (ChatRole.Assistant,"后天多云,气温25度。"),
new (ChatRole.User,"明天组织家人就近外出交流,请制定一份方案出来。")
];
var properties = history[1].AdditionalProperties ??= [];
properties[CompactionMessageGroup.SummaryPropertyKey] = true;

var strategy = new SlidingWindowCompactionStrategy(
trigger: messageIndex => messageIndex.TotalMessageCount > 6,
minimumPreservedTurns: 2);
var messageIndex = CreateMessageIndex(history);
await strategy.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,又为模型保留了曾调用过某工具并得到某结果的上下文线索。

public sealed class ToolResultCompactionStrategy : CompactionStrategy
{
public int MinimumPreservedGroups { get; }
public Func<CompactionMessageGroup, string>? ToolCallFormatter { get; init; }
public ToolResultCompactionStrategy(
CompactionTrigger trigger,
int minimumPreservedGroups = 16,
CompactionTrigger? target = null);
protected override ValueTask<bool> CompactCoreAsync(
CompactionMessageIndex index,
ILogger logger,
CancellationToken cancellationToken);
public static string DefaultToolCallFormatter(CompactionMessageGroup group);
}

我们在创建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, [
new FunctionCallContent("call_001","get_weather_conditions"),
new FunctionCallContent("call_002","get_temperature"),
new FunctionCallContent("call_003","get_precipitation"),
new FunctionCallContent("call_004","get_wind"),
]),
new (ChatRole.Tool,[new FunctionResultContent("call_001","多云")]),
new (ChatRole.Tool,[new FunctionResultContent("call_002","25度")]),
new (ChatRole.Tool,[new FunctionResultContent("call_003","无降水")]),
new (ChatRole.Tool,[new FunctionResultContent("call_004","微风2-3级")]),
new (ChatRole.Assistant,"后天多云,气温25度。"),
new (ChatRole.User,"明天组织家人就近外出交流,请制定一份方案出来。")
];
var properties = history[1].AdditionalProperties ??= [];
properties[CompactionMessageGroup.SummaryPropertyKey] = true;

var strategy = new ToolResultCompactionStrategy(
trigger: messageIndex => messageIndex.TotalMessageCount > 6,
minimumPreservedGroups: 2);
var messageIndex = CreateMessageIndex(history);
await strategy.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接口的外部大模型或算法引擎去进行智能提炼与重写,最后用重写后的精简历史彻底替换原有的历史。

public sealed class ChatReducerCompactionStrategy : CompactionStrategy
{public IChatReducer ChatReducer { get; }
public ChatReducerCompactionStrategy(
IChatReducer chatReducer,
CompactionTrigger trigger);
protected override async ValueTask<bool> CompactCoreAsync(
CompactionMessageIndex index,
ILogger logger,
CancellationToken cancellationToken);
}
public interface IChatReducer
{
Task<IEnumerable<ChatMessage>> ReduceAsync(
IEnumerable<ChatMessage> messages,
CancellationToken cancellationToken);
}

MAF内置了两种实现了IChatReducer接口的精简器,分别是MessageCountingChatReducer和SummarizingChatReducer。前者采用简单粗暴的计数方式来保留最近的消息,后者则通过调用一个外部的IChatClient来生成摘要消息,从而实现更智能的压缩效果。

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

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);
}

5. ChatReducerCompactionStrategy

基于陈旧消息组的摘要要锁可以采用ChatReducerCompactionStrategy借助SummarizingChatReducer来完成,也可以直接使用SummarizationCompactionStrategy来实现。我们在构造SummarizationCompactionStrategy对象的时候,需要指定一个调用LLM实施摘要的IChatClient对象,以及对应的提示词。上一篇文章演示的就是这种压缩策略。

public sealed class SummarizationCompactionStrategy : CompactionStrategy
{
public IChatClient ChatClient { get; }
public int MinimumPreservedGroups { get; }
public string SummarizationPrompt { get; }

public SummarizationCompactionStrategy(
IChatClient chatClient,
CompactionTrigger trigger,
int minimumPreservedGroups = 8,
string? summarizationPrompt = null,
CompactionTrigger? target = null);

protected override async ValueTask<bool> CompactCoreAsync(
CompactionMessageIndex index,
ILogger logger,
CancellationToken cancellationToken);
}

如果没有显式提供摘要提示词,默认采用如下这段文本:

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等)串联成一条有顺序的流水线来协同工作。

public sealed class PipelineCompactionStrategy : CompactionStrategy
{
public IReadOnlyList<CompactionStrategy> Strategies { get; }
public PipelineCompactionStrategy(params IEnumerable<CompactionStrategy> strategies);
protected override async ValueTask<bool> CompactCoreAsync(CompactionMessageIndex index, ILogger logger, CancellationToken cancellationToken);
}

赞(0)
未经允许不得转载:171主机测评 » [MAF预定义的AIContextProvider-06]CompactionProvider——采用多种策略压缩对话历史[续]
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址