惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
L
LINUX DO - 热门话题
H
Hacker News: Front Page
Hacker News - Newest:
Hacker News - Newest: "LLM"
L
Lohrmann on Cybersecurity
Cisco Talos Blog
Cisco Talos Blog
O
OpenAI News
S
Securelist
Security Latest
Security Latest
T
Threat Research - Cisco Blogs
H
Heimdal Security Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recorded Future
Recorded Future
Microsoft Azure Blog
Microsoft Azure Blog
MyScale Blog
MyScale Blog
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
Latest news
Latest news
N
Netflix TechBlog - Medium
N
News and Events Feed by Topic
D
Docker
D
DataBreaches.Net
A
About on SuperTechFans
T
Tor Project blog
V
V2EX
G
Google Developers Blog
博客园 - Franky
N
News | PayPal Newsroom
T
The Blog of Author Tim Ferriss
I
InfoQ
H
Help Net Security
V2EX - 技术
V2EX - 技术
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Security Affairs
SecWiki News
SecWiki News
The Register - Security
The Register - Security
人人都是产品经理
人人都是产品经理
NISL@THU
NISL@THU
小众软件
小众软件
B
Blog
T
Threatpost
P
Palo Alto Networks Blog
博客园 - 【当耐特】
L
LangChain Blog
AWS News Blog
AWS News Blog
月光博客
月光博客
宝玉的分享
宝玉的分享

博客园 - Artech

[MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案 [MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力 [MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF [MAF预定义的AIContextProvider-01]TextSearchProvider——RAG在MAF中的实现 [MAF预定义ChatClient中间件-09]MessageInjectingChatClient-赋予工具消息注入的能力 [MAF预定义ChatClient中间件-08]OpenTelemetryChatClient-实现链路跟踪和性能监控 [MAF预定义ChatClient中间件-07]PerServiceCallChatHistoryPersistingChatClient——基于ReAct循环的一步一存档 [MAF预定义ChatClient中间件-06]利用ImageGeneratingChatClient开发专业图片生成Agent [MAF预定义ChatClient中间件-05]动态修改ChatOptions和请求消息 [MAF预定义ChatClient中间件-04]ReducingChatClient——精减对话历史又不丢失基本语义 [MAF预定义ChatClient中间件-03]CachingChatClient——利用缓存省钱省时间 [MAF预定义ChatClient中间件-02]FunctionInvokingChatClient——实现ReAct循环和人机交互的大功臣 [MAF预定义ChatClient中间件-01]LoggingChatClient——在调用LLM前后输出日志 [MAF的Agent管道详解-07]利用AIAgent中间件构建Agent管道 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 - Artech [MAF的Agent管道详解-05]对话历史的持久化和输入输出的增强 [MAF的Agent管道详解-04]如何让LLM按照要求的结构输出数据? [MAF的Agent管道详解-03]连接LLM的IChatClient对象 [MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型? [MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始 [对比学习LangChain和MAF-04]针对消息的设计 [对比学习LangChain和MAF-03]完全不同的Agent设计哲学 [对比学习LangChain和MAF-02]基本编程模式的差异(下篇) [对比学习LangChain和MAF-01]基本编程模式的差异(上篇) 我所理解的Python元模型 除了按值和引用,方法参数的第三种传递方式 方法的三种调用形式 可以调用Null的实例方法吗? 自定义Key类型的字典无法序列化的N种解决方案 为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate 深入解析ASP.NET Core MVC的模块化设计[下篇] 深入解析ASP.NET Core MVC的模块化设计[上篇] ASP.NET Core MVC应用模型的构建[4]: Action的选择 ASP.NET Core MVC应用模型的构建[3]: Controller的收集 ASP.NET Core MVC应用模型的构建[2]: 定制应用模型
[MAF预定义的AIContextProvider-05]CompactionProvider——采用多种策略压缩对话历史
Artech · 2026-07-13 · via 博客园 - Artech

为解决LLM调用无状态的问题,我们不得不将消息历史作为上下文的一部分发送给LLM。但随着对话的进行,消息历史会不断增长,最终可能会超出LLM的上下文窗口限制。随着LLM的上下文窗口的不断扩大,可以有朝一日上下文窗口不再成为问题,但也并不意味着将整个对话历史原封不动地发送给LLM就是最优的选择。LLM拥有上下文的丰富程度与推理的质量并不是完全正相关的,当对话历史的长度超过一定程度后,过长的上下文反而会干扰LLM对当前对话的理解和推理,导致LLM生成的响应质量下降。所以对话历史的压缩需求可能永远都不会消失。针对消息的压缩,MAF提供了不止一种解决方案,一种是使用ReducingChatClient这个IChatClient中间件,另一种就是使用本篇文章着重介绍的CompactionProvider这个AIContextProvider

1. 利用CompactionProvider摘要对话历史

CompactionProvider支持不同的消息压缩策略,比如摘要、折叠、删除和替换等。由于CompactionProvider对压缩策略进行了抽象,原则上我们可以自行提供任一的策略来压缩对话历史。下面的例子演示了针对摘要的压缩策略,它可以在不丢失语义的前提下将多条消息压缩成一条。下面的实例演示了如何利用CompactionProvider,结合代表摘要压缩策略的SummarizationCompactionStrategy来对对话历史进行摘要压缩。

using Azure;
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Compaction;
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 compactionStrategy = new SummarizationCompactionStrategy(
    chatClient: summaryClient,
    trigger:  messageIndex => messageIndex.TotalMessageCount > 6, 
    minimumPreservedGroups: 1);
var compactionProvider = new CompactionProvider(compactionStrategy);

var agent = new OpenAIClient(
    credential: new AzureKeyCredential(apiKey),
    options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model: "gpt-5.2-chat")
    .AsIChatClient()
    .AsBuilder()
    .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()
    .AsAIAgent(options: new ChatClientAgentOptions {  AIContextProviders = [compactionProvider] });

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 agent.RunAsync(messages);
Console.WriteLine($"\n\n{response}");

如代码所示,我们创建了一个SummarizationCompactionStrategy对象来指定我们想要使用的摘要压缩策略,它的三个参数分别是:

  • summaryClient:摘要需要借助LLM的语言处理能力,所以需要这个参数提供的IChatClient来调用LLM生成摘要内容;
  • trigger:是一个CompactionTrigger类型的委托用于定义触发压缩的条件。上面的例子中,我们指定的触发条件是当消息总数超过6条时就触发压缩;
  • minimumPreservedGroups:是一个整数值,用于指定在触发压缩时至少要保留多少消息组。消息组在基于CompactionProvider的压缩策略中是一个重要的概念,我们将在后续部分对其进行详细介绍,这里我么姑且理解只需要保留最近的一条消息就可以了,需要对前面的消息进行压缩;

在基于这个SummarizationCompactionStrategy对象创建了一个CompactionProvider对象之后,我们将它注册到后面构建的AIAgent对象上。为了查看摘要压缩后的消息,我们调用ChatClientBuilderUse方法注册了一个IChatClient中间件输出所有的请求消息。程序的输出如下所示:

请求消息共计2条
1. [Summary]
**对话摘要:**

- 用户询问苏州今天天气情况。
- 提供信息:
  - 天气:晴天
  - 气温:25°C
  - 风力:西北风 4 级

无使用工具调用。对话围绕苏州当日天气展开。
2. 根据天气,给我一些着装建议。


根据目前 **25°C、晴天、西北风4级** 的天气情况,给你一些穿搭建议:

### 🌤️ 上衣
- ✅ **短袖T恤、衬衫、薄款POLO** 都很合适
- 如果怕风,可以带一件 **薄款防风外套或开衫**

### 👖 下装
- ✅ **薄款长裤、牛仔裤、休闲裤**
- ✅ 女生也可以穿 **半裙或连衣裙**
- 不建议太厚的裤子(会有点热)

### 👟 鞋子
- ✅ 运动鞋、休闲鞋、乐福鞋都合适
- 晴天路干,也可以穿透气一点的鞋

### 🌞 其他建议
- 有太阳,**可以带太阳镜或遮阳帽**
- 紫外线可能较强,建议涂防晒
- 风力4级稍有风感,头发容易被吹乱 😄

总体来说是一个**舒适偏暖、略有风的好天气**,穿得清爽一点最合适。

如果你是上班、约会还是户外活动,我也可以帮你具体搭配一下 😊

可以看到,原本的7条消息被压缩成了2条,其中第一条是压缩后的摘要消息,第二条是用户的最新消息。摘要消息中不仅保留了对话的核心信息,还对对话内容进行了适当的扩展和润色,使得它更像是人类总结出来的内容而不是简单地罗列式的摘要。虽然消息数量减少了,但是语义信息并没有丢失,所以LLM同样能够给出我们希望的响应内容。

2. 消息组

消息组对于CompactionProvider的压缩策略来说是一个非常重要的概念。我们上面多次提及的针对消息的压缩,其实应该改为针对消息组的压缩。消息组是压缩系统的原子单位,它把一条或多条逻辑相关的ChatMessage组合成一个可被压缩策略整体处理的结构。所有压缩策略都不是直接操作ChatMessage,而是操作消息组。消息组是MAF在构建可压缩对话历史时引入的结构化分组层。它的设计目标是:

  • 让压缩策略能够基于结构化的消息单元进行处理,而不是逐条操作零散的消息;
  • 让工具调用、多条针对同一角色的消息,以及折叠或摘要后的内容,都能够作为一个整体被压缩、替换或保留;
  • 让token/byte统计、turnIndex、排除标记等元数据绑定在消息组级别,从而实现更精确、更高效的压缩控制;

可以把它理解为:消息组 = ChatMessage的逻辑分组 + 压缩元数据 + 语义标签

上面提到的TurnIndex被称为轮次索引。Turn表示由一次User消息开启的交互轮次,包含该User消息之后直到下一条User消息之前的所有消息。对话历史的每一条消息都具有一个确定的TurnIndex,一如下这个对话历史为例:

User 发言 → 开启新Turn
Assistant回复 → 属于当前Turn
Tool调用/ToolResult → 属于当前Turn
Assistant主动发言(无User输入)→ 仍属于当前Turn
User再次发言 → 开启新Turn

目前没有看到由官方的文档对消息组的划分规则提供明确的说明,下面所述的划分规则来源于对源代码的分析。为么方面说明,我们定义如下几种消息类型:

  • Reasoning-Only:指的是那些只包含TextReasoningContent内容的Assistant消息。这些消息通常用于表达LLM的推理过程、思考路径或者一些辅助性的解释说明,它们不包含任何工具调用的内容;
  • Function-Call:指的是那些包含FunctionCallContent内容的Assistant消息;
  • Function-Result:指的是那些包含FunctionResultContent内容的Tool消息;

如果一个消息组包含多条消息,有且只有如下这一种排序,我们需要这种组合来保证工具调用的完整性:包含一条Function-Call消息和紧随其后的一条Function-Result消息,以及在这前后或者其间Assistant针对工具调用和结果的推理。

  • Reasoning-Only(0条或者多条)
  • Function-Call(1条)
  • Reasoning-Only/Function-Result(1条或者多条)
  • Reasoning-Only(0条或者多条)

上述的划分规则决定了通过如下这个CompactionGroupKind枚举表示的消息组类型:

public enum CompactionGroupKind
{
	System,
	User,
	AssistantText,
	ToolCall,
	Summary
}

具体类型说明如下:

  • System:系统消息组,包含所有角色为System的消息;
  • User:用户消息组,包含所有连续的角色为User的消息;
  • AssistantText:Assistant文本消息组,包含所有连续的角色为Assistant且不包含工具调用内容的消息;
  • ToolCall:工具调用消息组,按照我们前面的说法,一个包含多条消息的消息组必然属于这种类型;
  • Summary:摘要消息组,包含经过摘要压缩后的消息;

2.2 CompactionMessageGroup

消息组MAF中通过如下这个CompactionMessageGroup类来表示:

public sealed class CompactionMessageGroup
{
	public static readonly string SummaryPropertyKey = "_is_summary";
	public CompactionGroupKind Kind { get; }
	public IReadOnlyList<ChatMessage> Messages { get; }
	public int MessageCount { get; }
	public int ByteCount { get; }
	public int TokenCount { get; }
	public int? TurnIndex { get; }
	public bool IsExcluded { get; set; }
	public string? ExcludeReason { get; set; }
}

属性说明如下:

  • Kind:表示消息组的类型;
  • Messages:组成这个消息组的ChatMessage列表;
  • MessageCount、ByteCount、TokenCount:这个消息组中包含的消息数量、总字节数和总token数;
  • TurnIndex:这个消息组所属的Turn索引;
  • IsExcluded和ExcludeReason:用于标记这个消息组是否被排除在压缩之外,以及排除的原因;

为了避免语义丢失,对于摘要生成的ChatMessage,一般不允许对它进行二次摘要,所以CompactionProvider必需能够识别什么是摘要消息。具体做法很简单,这样的ChatMessageAdditionalProperties中会有一个键为专门的键来标记它是一个摘要消息,这个键就是CompactionMessageGroup类中的SummaryPropertyKey静态字段,值为布尔值。

3. 消息索引

作为对话历史的ChatMessage列表划分为一个个CompactionMessageGroup后,为了方便后续压缩,MAF还需要为这些消息组创建对应的消息索引。消息索引可以看作为消息组构建的、可以在上边应用相应的策略对其实施压缩的Workspace。消息索引通过如下这个CompactionMessageIndex类型表示。

public sealed class CompactionMessageIndex
{
    public IList<CompactionMessageGroup> Groups { get; }
    public Tokenizer? Tokenizer { get; }

    public CompactionMessageIndex(
        IList<CompactionMessageGroup> groups, 
        Tokenizer? tokenizer = null);

    public CompactionMessageGroup InsertGroup(
        int index, 
        CompactionGroupKind kind, 
        IReadOnlyList<ChatMessage> messages, 
        int? turnIndex = null);
	public CompactionMessageGroup AddGroup(
        CompactionGroupKind kind, 
        IReadOnlyList<ChatMessage> messages, 
        int? turnIndex = null);
}

一个CompactionMessageIndex对象通过指定的一组CompactionMessageGroup对象来构建而成。由于需要利用分词器计算Token数量,所以在构建CompactionMessageIndex对象的时候可以选择性地传入一个Tokenizer对象。消息索引提供了InsertGroupAddGroup两个方法来向索引中插入新的消息组或者添加新的消息组,并利用GroupsTokenizer属性来访问当前的消息组和分词器。如果没有显式提供分词器,默认会按照每4个字节算作一个Token的方式来估算Token数量。

我们可以调用GetAllMessages方法获取整个消息列表。CompactionMessageGroup利用IsExcluded属性来标记消息组是否被排除在压缩之外,GetIncludedMessages方法返回未被排除在外的消息组提供的消息。如果知道TurnIndex,我们还可以调用GetTurnGroups方法来获取隶属于指定轮次的所有消息组。

public sealed class CompactionMessageIndex
{
	public IEnumerable<ChatMessage> GetIncludedMessages();
	public IEnumerable<ChatMessage> GetAllMessages();
	public IEnumerable<CompactionMessageGroup> GetTurnGroups(int turnIndex);
}

CompactionMessageIndex还定义如下这些返回各种汇总统计的属性,这些统计信息对于压缩策略的决策非常重要。

public sealed class CompactionMessageIndex
{
	public int TotalGroupCount { get; }
	public int TotalMessageCount { get; }
	public int TotalByteCount { get; }
	public int TotalTokenCount { get; }
	public int IncludedGroupCount { get; }
	public int IncludedMessageCount { get; }
	public int IncludedByteCount { get; }
	public int IncludedTokenCount { get; }
	public int TotalTurnCount { get; }
	public int IncludedTurnCount { get; }
	public int IncludedNonSystemGroupCount { get; }
	public int RawMessageCount { get; }

    internal static CompactionMessageIndex Create(IList<ChatMessage> messages, Tokenizer? tokenizer = null);
}

具体汇总统计包括:

  • TotalGroupCount、TotalMessageCount、TotalByteCount、TotalTokenCount:整个消息索引中包含的消息组数量、消息数量、字节数和token数;
  • IncludedGroupCount、IncludedMessageCount、IncludedByteCount、IncludedTokenCount:未被排除在压缩之外的消息组数量、消息数量、字节数和token数;
  • TotalTurnCount、IncludedTurnCount:整个消息索引中包含的Turn数量和未被排除在压缩之外的Turn数量;
  • IncludedNonSystemGroupCount:未被排除在压缩之外的、且类型不为System的消息组数量;
  • RawMessageCount:原始的消息数量,也就是未经过任何分组或者压缩之前的消息数量;

4. 压缩策略

如下所示的CompactionStrategy抽象了所谓的压缩策略,它旨在从三个维度描述压缩对话历史的策略:何时触发压缩压缩后的目标状态以及具体的压缩执行逻辑

public abstract class CompactionStrategy
{
	protected CompactionTrigger Trigger { get; }
	protected CompactionTrigger Target { get; }

	protected CompactionStrategy(CompactionTrigger trigger, CompactionTrigger? target = null);

	protected abstract ValueTask<bool> CompactCoreAsync(
        CompactionMessageIndex index, 
        ILogger logger, 
        CancellationToken cancellationToken);
	public async ValueTask<bool> CompactAsync(
        CompactionMessageIndex index, 
        ILogger? logger = null, 
        CancellationToken cancellationToken = default);
}

CompactionStrategy中表示触发压缩的Trigger和表示压缩目标状态的Target都是如下这个CompactionTrigger类型的委托。这是一个输入参数为CompactionMessageIndex对象,返回值为布尔值的委托。实现具体压缩策略的子类必需重写的CompactCoreAsync方法也将压缩直接应用到作为参数的CompactionMessageIndex对象上。压缩完成之后,CompactionMessageIndexGetIncludedMessages方法返回的消息列表就是经过压缩后的消息列表了。

public delegate bool CompactionTrigger(CompactionMessageIndex index);

5. CompactionProvider

由于压缩操作最终落在注册的CompactionStrategy对象上,所以Compaction自身的实现其实很简单。前面已经说过了,压缩针对的原子单元是CompactionMessageGroup,而不是单个的ChatMessage,所以中间涉及一个从ChatMessage列表到CompactionMessageGroup列表的构建过程。为了避免这个过程重写执行,CompactionProvider会将针对当前消息列表构建好的CompactionMessageGroup列表存储在Session状态中。对于每次Agent调用,它只需要针对新产生的消息以增量的方式构建CompactionMessageGroup列表,并将其追加到现有的列表之中。

CompactionMessageGroup列表被封装在内嵌的State类中。当ProviderSessionState初始化的时候,可以执行存储此State对象对应Session状态的键,如果没有显式指定,会默认使用CompationProvider的类型名称作为键。CompationProvider会创建一个ProviderSessionState<State>对象来管理这个状态,

public sealed class CompactionProvider : AIContextProvider
{
    private readonly CompactionStrategy _compactionStrategy;
    private readonly ProviderSessionState<State> _sessionState;
    public CompactionProvider(CompactionStrategy compactionStrategy, string? stateKey = null, ILoggerFactory? loggerFactory = null)
    {
        _compactionStrategy = compactionStrategy;
        stateKey ??= _compactionStrategy.GetType().Name;
        StateKeys = [stateKey];
        _sessionState = new ProviderSessionState<State>(
            _ => new State(),
            stateKey,
            AgentJsonUtilities.DefaultOptions);
        ...
    }
    public override IReadOnlyList<string> StateKeys { get; }

    internal sealed class State
    {
        public List<CompactionMessageGroup> MessageGroups { get; set; } = [];
    }
}

CompactionProvider利用重写的InvokingCoreAsync方法来实现对话历史的压缩,针对指定CompactionStrategy压缩对话历史最终实现在静态方法CompactAsync中。这个方法接受一个CompactionStrategy对象和一个ChatMessage列表作为输入,首先将ChatMessage列表转换成CompactionMessageGroup列表并构建成一个CompactionMessageIndex对象,然后调用CompactionStrategyCompactAsync方法来执行压缩,最后调用GetIncludedMessages方法返回压缩后的消息列表。

public sealed class CompactionProvider : AIContextProvider
{
    public static async Task<IEnumerable<ChatMessage>> CompactAsync(
        CompactionStrategy compactionStrategy, 
        IEnumerable<ChatMessage> messages, 
        ILogger? logger = null, 
        CancellationToken cancellationToken = default)
    {
        List<ChatMessage> messageList = messages as List<ChatMessage> ?? [.. messages];
        CompactionMessageIndex messageIndex = CompactionMessageIndex.Create(messageList);
        await compactionStrategy.CompactAsync(messageIndex, logger, cancellationToken)
            .ConfigureAwait(false);
        return messageIndex.GetIncludedMessages();
    }

    protected override async ValueTask<AIContext> InvokingCoreAsync(
        InvokingContext context, 
        CancellationToken cancellationToken = default)
}

InvokingCoreAsync方法的具体实现逻辑如下:

  • 确认Session存在,消息列表不为空,否则直接返回InvokingContext携带的AIContext
  • 将消息列表转换成CompactionMessageGroup列表,如果Session状态中已经存在CompactionMessageGroup列表,则只针对新消息构建CompactionMessageGroup列表并追加到现有列表之中;
  • 利用CompactionMessageGroup列表创建CompactionMessageIndex对象,并将其传入CompactionStrategyCompactAsync方法来执行压缩,得到压缩后的消息列表;
  • 使用最新的CompactionMessageGroup列表更新Session状态;
  • 提取InvokingContext中原有的AIContext,并用压缩后的消息列表替换原有AIContext中的消息列表,构建一个新的AIContext对象返回;