MAF / MEAI 初学者笔记:为什么需要 Chat Reducer?

decade
35
2026-01-06

当上下文越来越长时,应该怎么聪明地“缩短历史消息”。

你可以把 Chat Reducer 理解成:

对话历史的“压缩器”或“整理器”

它不会让 Agent 失忆,而是帮助 Agent 带着更精简、更有价值的上下文继续对话


一、这节课到底在解决什么问题?

在多轮对话里,消息会越来越多。

比如:

  • 第 1 轮:用户提问,助手回答

  • 第 2 轮:用户继续追问,助手再回答

  • 第 10 轮:上下文已经很长

  • 第 50 轮:如果还把所有历史原封不动发给模型,问题就来了

这时候通常会出现 4 个典型挑战。

1. 上下文窗口有限

大多数大模型都有最大上下文长度限制,比如:

  • 8K tokens

  • 32K tokens

  • 甚至更大

但无论多大,都不是无限的

如果历史消息太长,超出模型可接收范围,请求就可能失败,或者效果明显下降。


2. 成本会上升

模型调用通常按 Token 收费。

这意味着:

  • 输入越长

  • Token 越多

  • 成本越高

尤其是高频对话场景,比如:

  • 智能客服

  • 在线问答

  • 企业助手

如果不做历史压缩,长期运行时成本会很可观。


3. 性能会下降

上下文越长,模型处理越慢。

结果就是:

  • 响应变慢

  • 用户等待变久

  • 体验变差

所以 Chat Reducer 不只是省钱,也是在帮你提升性能。


4. 历史里有很多冗余信息

并不是所有历史内容都对当前问题有帮助。

例如:

  • 前面几轮只是寒暄

  • 很早之前讨论过不再相关的话题

  • 某些重复确认的信息价值很低

这些内容如果继续塞给模型,只会浪费上下文预算。


一句话理解 Chat Reducer 的价值

用更少的消息,保留尽可能多的有效上下文。

这就是 Chat Reducer 的核心目标。


二、什么是 Chat Reducer?

Chat Reducer 是对话历史压缩机制。

它负责在真正调用模型之前,对历史消息做一次“筛选、裁剪、压缩”。

这样模型拿到的不是“完整但冗长的历史”,而是“精简但仍然够用的上下文”。


三、核心接口:IChatReducer

所有 Reducer 都遵循同一个接口:

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

这个接口非常好理解。

它做的事只有一件:

  • 输入:一组消息 messages

  • 输出:压缩后的消息列表


你可以这样理解它

老师式解释:

  • 原始消息是一大叠聊天记录

  • ReduceAsync() 就像一个“整理员”

  • 它会从这堆记录里挑出更适合继续发给模型的内容

最终返回的,是一个新的、更短的消息列表。


四、MEAI 内置的两种 Chat Reducer

课程中提到,MEAI 提供了两种开箱即用的实现:

  1. MessageCountingChatReducer

  2. SummarizingChatReducer

这两种一定要分清,因为它们代表了两种完全不同的压缩思路。


五、MessageCountingChatReducer:按消息数量裁剪

这是最容易理解、也最适合初学者上手的一种。

它的核心思路

只保留最近的 N 条非系统消息。

比如你设置:

new MessageCountingChatReducer(targetCount: 3)

意思通常可以理解为:

  • 保留第一条系统消息(如果有)

  • 再保留最近的 3 条非系统消息

  • 更早的消息直接丢弃


它的特点

1. 永远优先保留系统消息

比如:

你是一个专业客服助手

这种系统提示通常很重要,所以会保留。


2. 只关注“最近消息”

因为很多场景下,最近几轮才最关键。


3. 会自动排除函数调用相关消息

如果消息中包含函数调用或函数结果,它会自动跳过,避免破坏函数调用链条。


适合什么场景?

MessageCountingChatReducer 特别适合:

  • 客服对话

  • 快速问答

  • 技术支持

  • 成本敏感场景

  • 不需要长期上下文的对话

简单说:

如果你的业务更关心“最近几轮对话”,而不是完整历史语义”,那它非常合适。


它的优点

优点 1:简单直接

没有复杂逻辑,很好理解。

优点 2:速度快

不需要额外调用模型生成摘要。

优点 3:零额外成本

不会为了压缩再调用一次 LLM。


它的缺点

缺点 1:可能丢失重要上下文

如果重要信息出现在更早的消息里,而又被裁掉了,就可能影响回答质量。

缺点 2:不保留长期语义

它只保留“最近”,不保留“总结”。


六、SummarizingChatReducer:按摘要压缩

这个就更“聪明”了。

它不是简单删掉旧消息,而是:

把前面的历史内容总结成摘要,再保留最近几条原始消息。

也就是说,它会把长历史压缩成一种“摘要 + 最近消息”的结构。


它的核心思路

假设原始消息很多:

  • System

  • User 1

  • Assistant 1

  • User 2

  • Assistant 2

  • User 3

  • Assistant 3

  • ...

那么压缩后可能变成:

  • System

  • Summary(前面若干轮摘要)

  • 最近几条原始消息

这样就做到了两件事:

  1. 不把所有旧消息原样保留

  2. 但又尽量不丢失历史语义


它的特点

1. 超过阈值后自动摘要

当消息数量超过一定阈值时,它会自动触发摘要生成。

2. 需要额外的 IChatClient

因为摘要本身也是 AI 生成的,所以需要一个模型来做总结。

3. 支持渐进式摘要

新摘要会包含旧摘要内容,不是每次都从零总结。

4. 保留最近若干条原始消息

这样既保留整体语义,也保留最近对话细节。


适合什么场景?

非常适合:

  • 医疗咨询

  • 法律咨询

  • 教育辅导

  • 长时间复杂协作

  • 需要长期上下文连贯性的对话

一句话总结:

如果你不能简单粗暴地删掉旧消息,而又必须控制长度,就选摘要型 Reducer。


它的优点

优点 1:保留语义连续性更好

虽然删掉了原始细节,但核心内容被总结下来了。

优点 2:适合长对话

特别适合十几轮、几十轮甚至更长的会话。


它的缺点

缺点 1:有额外调用成本

每次生成摘要,本质上都是再调用一次模型。

缺点 2:会增加延迟

通常会额外增加 1~3 秒左右,视模型和网络情况而定。

缺点 3:摘要可能有偏差

摘要毕竟依赖 LLM 理解,不保证 100% 完整无误。


七、两种 Reducer 的本质区别

这是这节课最核心的对比,必须真正理解。

对比项

MessageCountingChatReducer

SummarizingChatReducer

压缩方式

直接保留最近 N 条

旧消息摘要 + 最近消息

是否额外调用模型

成本

更低

更高

速度

更快

更慢

语义保留能力

一般

更强

适用场景

短对话、快速问答、客服

长对话、复杂咨询、持续协作


老师式判断口诀

你可以这样记:

如果你想要:

  • 更简单

  • 更省钱

  • 更快

选:

MessageCountingChatReducer

如果你想要:

  • 更完整的长期语义

  • 更适合复杂多轮上下文

选:

SummarizingChatReducer

八、Chat Reducer 的整体工作流程

一个完整流程可以这样理解:

第一步:准备原始消息

比如:

  • System:你是一个客服助手

  • User:问题 1

  • Assistant:回答 1

  • User:问题 2

  • Assistant:回答 2

  • ...

  • User:问题 10


第二步:Reducer 处理

在真正请求模型前,Reducer 会介入。


第三步:得到压缩后的消息

例如:

  • System

  • Summary(如果是摘要型)

  • 最近几条消息


第四步:把压缩后的消息发给模型

模型看到的是“更精简的上下文”。


九、如何把 Reducer 集成到 Chat Client 中?

这部分非常实战。

Reducer 不是单独乱用的,而是通常接入到 ChatClient 的管道中。

典型方式:

var reducingClient = baseChatClient.AsBuilder()
    .UseChatReducer(reducer: countingReducer)
    .Build();

这句话的意思是:

  • 基于原始 ChatClient

  • 构建一个带消息压缩能力的新客户端

  • 以后调用这个新客户端时,Reducer 会自动生效


这就是 Pipeline 思维

也就是说,Reducer 是一个中间处理环节。

流程变成:

  • 你传入消息

  • Reducer 先压缩

  • 然后才发给模型

所以你平时继续正常调用:

await reducingClient.GetResponseAsync(messages);

就可以了。


十、示例一:MessageCountingChatReducer 实战理解

1. 创建 Reducer

var countingReducer = new MessageCountingChatReducer(targetCount: 3);

含义:

  • 最多保留 3 条非系统消息


2. 集成进 Client

var reducingClient = baseChatClient.AsBuilder()
    .UseChatReducer(reducer: countingReducer)
    .Build();

3. 模拟多轮对话

你本地可能维护一个完整消息列表:

var messages = new List<ChatMessage>
{
    new ChatMessage(ChatRole.System, "你是一个专业的客服助手。")
};

然后不断追加用户和助手消息。

这里一个特别容易误解的点是:

本地 messages 列表可以继续保留完整历史,但真正发给模型前会被 Reducer 自动压缩。

也就是说:

  • 本地历史:可能是 12 条

  • 发给模型:可能只有 4 条


4. 手动验证压缩效果

var reducedMessages = await countingReducer.ReduceAsync(messages, CancellationToken.None);

这一步非常适合学习和调试,因为你能直接看到:

  • 压缩前多少条

  • 压缩后多少条

  • 哪些消息被保留

  • 哪些消息被丢弃


初学者一定要记住这一点

Reducer 不会直接修改你的原始消息列表。

它返回的是一份新的压缩结果

所以如果你要审计、日志、回放,完全可以继续保留原始消息。


十一、示例二:SummarizingChatReducer 实战理解

1. 创建摘要型 Reducer

var summarizingReducer = new SummarizingChatReducer(
    chatClient: baseChatClient,
    targetCount: 2,
    threshold: 1
);

参数怎么理解?

targetCount: 2

表示压缩后,最近的 2 条消息会尽量原样保留。

threshold: 1

表示超过 targetCount + threshold 后触发摘要。

这里就是:

2 + 1 = 3

超过这个数量时,就会开始摘要。


2. 为什么它需要 chatClient

因为摘要不是程序硬编码出来的,而是模型生成的。
所以它需要一个 IChatClient 来专门执行摘要任务。

这点你一定要理解:

SummarizingChatReducer 本身也要“调用 AI”来完成压缩。


3. 压缩后的结构

课程中提到,摘要会存储在消息的 AdditionalProperties 中。

你可以把它理解成一种特殊消息,里面带有:

  • 历史摘要

  • 上文压缩结果

  • 结构化附加信息

这意味着后续再压缩时,它不是完全丢弃前情,而是可以继续在已有摘要基础上递进总结。


十二、自定义摘要提示词:为什么很重要?

这是摘要型 Reducer 的高级但很实用的能力。

比如你在医疗场景下,普通摘要可能会写得太泛。
但医疗场景往往更关心:

  • 主诉症状

  • 时长

  • 过敏史

  • 已给建议

这时候你就可以自定义摘要提示词:

customReducer.SummarizationPrompt = """
请为以下医疗咨询对话生成简洁的临床摘要(不超过 3 句话):
...
""";

这件事说明了什么?

说明摘要不是“固定模板”,而是可以按领域调优的。

你可以为不同业务定义不同摘要风格:

  • 医疗:强调症状、病程、过敏史

  • 法律:强调案情事实、证据、诉求

  • 教育:强调学习进度、难点、已掌握知识

  • 客服:强调订单号、问题类型、处理结果


老师式提醒

如果你的业务特别依赖某些关键信息,
不要只相信“模型会自己总结到位”。

你应该:

  1. 自定义更明确的摘要 Prompt

  2. 对关键字段做结构化存储

比如:

  • 订单号

  • 金额

  • 用户 ID

  • 病历编号

这些关键信息更适合单独存数据库,而不是只靠摘要保留。


十三、如何选择合适的 Reducer?

这是实际开发中最常问的问题。


适合 MessageCountingChatReducer 的场景

客服机器人

通常只关心最近几轮交流内容。

技术支持

很多问题是局部性的,不一定依赖很久以前的上下文。

快速问答

问题短、对话轻,不需要复杂摘要。

成本优先系统

如果你非常在意性能和费用,计数型更合适。


适合 SummarizingChatReducer 的场景

医疗咨询

病史、症状演变、建议过程都很重要。

法律咨询

前后事实链不能轻易丢。

教育辅导

学习过程和已讲知识需要连续追踪。

长时协作类任务

比如复杂项目助手、长期顾问型 Agent。


十四、参数怎么调?初学者先学会这个思路

一、MessageCountingChatReducer

保守策略

new MessageCountingChatReducer(targetCount: 10);

适合:

  • 上下文较敏感

  • 希望保留更多消息

  • 不想压缩得太激进

激进策略

new MessageCountingChatReducer(targetCount: 2);

适合:

  • 成本优先

  • 对历史依赖很低

  • 对话偏短平快


二、SummarizingChatReducer

频繁摘要

new SummarizingChatReducer(chatClient, targetCount: 3, threshold: 0);

意思是:

  • 只要超过 targetCount 就立即摘要

优点:

  • 历史始终很紧凑

缺点:

  • 摘要调用频繁

  • 成本更高


延迟摘要

new SummarizingChatReducer(chatClient, targetCount: 5, threshold: 3);

意思是:

  • 等历史积累得更多一点再摘要

优点:

  • 减少额外摘要调用次数

缺点:

  • 某些时刻上下文可能仍然偏长


十五、与其他中间件组合使用时,要注意顺序

课程里给了一个非常关键的建议:

var client = baseChatClient.AsBuilder()
    .UseChatReducer(reducer: summarizingReducer)
    .UseFunctionInvocation()
    .Build();

重点不是代码本身,而是顺序


为什么 Reducer 要尽量放前面?

因为它的职责是:

在真正调用 API 前,先把消息压缩好。

如果顺序乱了,后面的中间件可能拿到的是未压缩消息,导致:

  • Token 仍然很长

  • 成本没有降下来

  • 部分中间件处理上下文时不够高效

所以一般建议:

Reducer 放在处理链前端。


十六、函数调用消息怎么处理?

课程里特别提到一个点:

两种 Reducer 都会自动排除包含 FunctionCallContentFunctionResultContent 的消息。

这很重要。

为什么?

因为函数调用相关消息常常有严格的调用链关系,如果你随便裁掉,可能导致:

  • 工具调用上下文断裂

  • 结果解释不完整

  • 后续对话混乱

所以默认策略是:

  • 系统消息:保留

  • 普通用户/助手消息:纳入压缩

  • 函数调用相关消息:自动特殊处理/跳过

这是一种比较稳妥的设计。


十七、性能与成本怎么权衡?

这部分很工程化,也很实用。


MessageCountingChatReducer

优点

  • 无额外 API 调用

  • 没有额外延迟

  • 成本最低

代价

  • 容易丢失较早的重要信息


SummarizingChatReducer

优点

  • 更能保留长期语义

  • 更适合复杂多轮会话

代价

  • 每次摘要都要额外调用 LLM

  • 会增加延迟

  • 会增加成本


实用优化建议

课程中提到一个很好的思路:

用更小的模型专门做摘要。

例如:

  • 主任务用更强的模型

  • 摘要压缩用更便宜的小模型

这样可以在一定程度上兼顾:

  • 成本

  • 速度

  • 摘要质量

这是实际项目里非常常见的策略。


十八、常见问题答疑

Q1:Reducer 会修改原始消息列表吗?

不会。

ReduceAsync() 返回的是一个新的消息集合,原始消息不变。

所以你可以:

  • 原始列表做审计

  • 压缩列表发给模型

两者并存。


Q2:多用户场景下怎么用?

通常做法是:

  • 每个用户维护自己的消息历史

  • Reducer 可以共享同一个实例

因为 Reducer 本身通常是无状态的。

例如:

var sharedReducer = new MessageCountingChatReducer(5);

var user1Messages = new List<ChatMessage>();
var user2Messages = new List<ChatMessage>();

关键原则:

消息历史按用户隔离,Reducer 逻辑可以共享。


Q3:可以自定义自己的 Reducer 吗?

可以,完全可以。

只要实现 IChatReducer 接口即可:

public class CustomReducer : IChatReducer
{
    public Task<IEnumerable<ChatMessage>> ReduceAsync(
        IEnumerable<ChatMessage> messages, 
        CancellationToken cancellationToken)
    {
        var reduced = messages.Where(m => /* 自定义条件 */);
        return Task.FromResult(reduced);
    }
}

Q4:摘要压缩会不会丢信息?

会有这个风险。

它通常能保留:

  • 主要事实

  • 核心语义

  • 关键上下文

但也可能丢掉:

  • 情绪细节

  • 语气风格

  • 玩笑和口语化表达

  • 某些不够显眼但实际重要的细节

所以对于关键字段,还是建议结构化存储,不要只靠摘要。


Q5:支持流式响应吗?

支持。

Reducer 不只支持普通响应,也支持流式响应。
它会在开始流式输出前,先完成消息压缩。

也就是说:

  • 压缩先发生

  • 流式返回后发生

这点对实时对话体验很重要。


十九、初学者最该掌握的实战认知

这部分我帮你提炼成老师会反复强调的重点。


1. Reducer 不是“删除历史”,而是“控制历史输入”

这句话特别重要。

因为很多同学会误以为:

用了 Reducer,就等于聊天记录被删掉了。

其实不是。

更准确地说是:

  • 本地历史可以继续完整保留

  • Reducer 只是控制“本次发给模型的上下文”


2. MessageCountingChatReducer 是“简单裁剪”

它不总结,只保留最近消息。


3. SummarizingChatReducer 是“语义压缩”

它通过摘要保留历史意义。


4. 选择哪种 Reducer,取决于业务对“历史语义”的依赖程度

如果业务依赖低,计数型更省。
如果业务依赖高,摘要型更稳。


5. 关键业务信息不要只靠摘要

例如:

  • 订单号

  • 金额

  • 病史编号

  • 法律证据编号

最好单独做结构化存储。


二十、学习这一节后,你应该具备什么能力?

学完这节课,一个初学者应该至少能做到以下几点:

1. 能解释为什么需要 Chat Reducer

不是背定义,而是真的知道它解决:

  • 上下文限制

  • 成本问题

  • 性能问题

  • 信息冗余问题


2. 能区分两种内置 Reducer

知道:

  • MessageCountingChatReducer:保留最近 N 条

  • SummarizingChatReducer:摘要旧历史 + 保留最近消息


3. 能把 Reducer 接进 Chat Client

比如:

var client = baseChatClient.AsBuilder()
    .UseChatReducer(reducer)
    .Build();

4. 能根据场景做技术选择

比如:

  • 客服 → 计数型

  • 医疗 → 摘要型

  • 法律 → 摘要型

  • 快速问答 → 计数型


5. 能意识到摘要不是万能的

关键字段仍然要结构化保存。


二十一、整节课的最终总结

Chat Reducer 的本质

Chat Reducer 是一种“对话上下文管理工具”,用于在多轮对话中压缩历史消息,控制模型输入长度。


它解决的核心问题

  • 避免超出上下文窗口

  • 降低 Token 成本

  • 提升响应性能

  • 去除不必要的历史冗余


两种内置方案

MessageCountingChatReducer

适合短对话、成本优先、只关心最近几轮的场景。

SummarizingChatReducer

适合长对话、上下文连续性要求高、不能简单丢历史的场景。


使用建议

  • 简单系统先上 MessageCountingChatReducer

  • 长对话系统优先评估 SummarizingChatReducer

  • 关键业务字段不要仅靠摘要

  • Reducer 通常放在处理中间件链的前面


二十二、复习速记版

如果你只想快速记忆,请记住下面这几条:

1.

IChatReducer 的作用是:
输入一组消息,输出压缩后的消息。

2.

MessageCountingChatReducer
保留最近 N 条非系统消息。

3.

SummarizingChatReducer
把旧消息变成摘要,再保留最近消息。

4.

计数型更快、更便宜;摘要型更完整、但更贵。

5.

Reducer 不会强制修改原始消息列表,它返回新的压缩结果。

6.

关键数据不要只依赖摘要,要结构化存储。