菜鸟AI - 让提示词生成更简单! 全站导航 全站导航
AI工具安装 新手教程 进阶教程 辅助资源 AI提示词 热点资讯 技术资讯 产业资讯 内容生成 模型技术 AI信息库

已有账号?

首页 > AI教程 > MAF多轮对话进阶:清除历史、注入System与截断策略
进阶教程 MAF多轮对话进阶

MAF多轮对话进阶:清除历史、注入System与截断策略

2026-06-01
阅读 0
热度 0
作者 菜鸟AI编辑部
摘要

摘要

清除历史通过SetMessages或SetInMemoryChatHistory清空消息列表,保留会话身份;系统注入利用Messag

MAF 入门(3 下):多轮对话进阶——清除历史、注入 System、截断策略

前一篇我们让Agent学会了“记住”——在多轮对话里能准确回答“你叫小明”或者“你喜欢C#”。但到了真正的产品环境,光有“记忆”这个能力还不够,还得学会“忘记”,学会“改规矩”,以及学会“省Token”。

具体来说,这三件事构成了本篇的核心:

  • 清除历史:用 SetMessagesSetInMemoryChatHistory 实现“一键清空”。
  • 注入System消息:靠 MessageInjectingChatClient.EnqueueMessages 在对话中途改变系统指令。
  • 截断策略:通过 IChatReducerMessageCountingChatReducer 来裁剪过长历史。

下面我们就逐一拆解。

一、清除会话历史——「一键新开聊天」

1.1 为什么需要清除?

多轮记忆是一把双刃剑。用户点击了“新对话”,你还把上一轮“记住数字42”带进上下文,不仅浪费Token,还可能让模型答非所问。清除历史不等于销毁AgentSession——Session还在(同一会话ID、同一块StateBag),只是消息列表被清空了,就像微信里“清空聊天记录”但窗口没关一样。

1.2 实现步骤

步骤1:照旧创建带 InMemoryChatHistoryProvider 的Agent和Session。

步骤2:先聊两轮,验证“记得住”:

await agent.RunAsync("记住这个数字:42。", session);
await agent.RunAsync("我刚才让你记住的数字是多少?", session);
// 预期:42

步骤3:清空历史(两种写法等价):

// 写法 A:通过 Provider
historyProvider.SetMessages(session, []);

// 写法 B:通过 Session 扩展方法
session.SetInMemoryChatHistory([]);

步骤4:再问同一个问题:

await agent.RunAsync("我刚才让你记住的数字是多少?", session);
// 预期:不知道 / 没有相关信息

1.3 Demo 关键代码

Console.WriteLine("--- 执行清除历史 ---");
historyProvider.SetMessages(session, []);
PrintHistory("清除后", historyProvider, session); // 应为 0 条
await RunTurnAsync(agent, session, "我刚才让你记住的数字是多少?", cancellationToken);

1.4 注意点

  • 清的是ChatHistory消息,不是Instructions(创建Agent时的系统角色仍然在)。
  • 如果只清了Session,但换了一个没有挂同一Provider的Agent,行为可能不一致——最稳妥的方式是:同一Agent + 同一Provider实例。
  • 生产环境中也可以新建Session(CreateSessionAsync())来代替清空,效果类似于“全新的对话窗口”。

二、运行时注入 System Message——「对话中途改规矩」

2.1 和 Instructions 有什么不同?

前面讲过,ChatOptions.Instructions 是在创建Agent时写好的,相当于入职手册。但有些场景需要在聊了一半时修改规则,比如:

  • 用户点击“切换英文”
  • 运营活动临时加一条“今天禁止讨论价格”
  • 工具执行完后插入一条隐式的System提示

这时候不适合重建Agent,而是应该往当前Session里再塞一条System消息。

Instructions(静态)运行时注入(动态)
时机AsAIAgent / ChatClientAgentOptions任意一轮 RunAsync 之前
改法换配置或换AgentEnqueueMessages
历史每轮都有从注入时刻起影响后续轮次

2.2 机制:MessageInjectingChatClient

MAF在管道里加了一层 MessageInjectingChatClient,流程大概是这样:RunAsync触发 → 从Session.StateBag取出“待注入消息队列” → 合并进本次发给模型的messages → 调用大模型。

要启用它,创建Agent时必须设置:

var options = new ChatClientAgentOptions
{
    Name = "InjectSystemAgent",
    ChatOptions = new ChatOptions { Instructions = BaseInstructions },
    ChatHistoryProvider = historyProvider,
    EnableMessageInjection = true,  // 关键开关
};

2.3 实现步骤

步骤1:enableMessageInjection: true 创建Agent,并 CreateSessionAsync()

步骤2:正常聊一轮(用中文):

await agent.RunAsync("用一句话介绍你自己。", session);

步骤3:拿到注入器并排队System消息:

MessageInjectingChatClient? injector = agent.GetService();
if (injector is null)
{
    // 说明 EnableMessageInjection 未生效
    return;
}

injector.EnqueueMessages(session,
    [new ChatMessage(ChatRole.System, "From now on, reply only in brief English.")]);

步骤4:第二轮提问,观察是否变为英文:

await agent.RunAsync("用一句话介绍 MAF。", session);

2.4 形象理解

把对话想象成开会:

  • Instructions就是会议开始前发的议程,一直有效。
  • 而EnqueueMessages(System)就像是会议中途主席突然补充一句:“接下来请用英文发言。”

之前的发言记录还在(History没清),但后续模型会多看到一条System,从而改变行为。

2.5 注意点

  • 必须设置EnableMessageInjection = true,否则GetService()会返回null。
  • 注入的消息在下一轮(或同轮pipeline内下一次模型调用)才生效,不是修改已经发出去的历史。
  • 模型不一定100%遵守新System规则,这和写静态Instructions一样,需要靠prompt与评测来保证效果。

三、截断策略——「聊天记录太长就裁剪」

3.1 为什么需要截断?

历史消息会一直append。聊到50轮以后,问题就来了:

  • Token爆掉:超出context window,API报错或自动截断。
  • 变慢变贵:每次请求都要携带全长历史。
  • 干扰答案:早期无关内容会稀释模型的注意力。

所以,在发给模型之前,必须对历史进行Reduce(缩减)。MAF通过 IChatReducer 挂在 InMemoryChatHistoryProvider 上实现这个机制。

3.2 存储 vs 发给模型:两个数量

Demo里有一个容易混淆的点:

概念含义
存储条数GetMessages(session).Count —— StateBag里完整保存的轮次
发给模型的条数ChatReducer 裁剪之后才拼进API的messages

截断默认在 BeforeMessagesRetrieval(取历史给模型之前)触发:

new InMemoryChatHistoryProviderOptions
{
    ChatReducer = new MessageCountingChatReducer(maxMessages),
    ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval,
}

因此可能出现:存储了12条消息,但实际只把最近4条非System消息发给了模型。

3.3 MessageCountingChatReducer 做什么?

ChatReducer = new MessageCountingChatReducer(4)// 最多保留 4 条「非 System」消息

它的行为可以简化理解为:

  • 保留第一条System(如果有)。
  • 保留最近4条user / assistant消息。
  • 丢掉更早的user / assistant。
  • 包含工具调用的消息通常不参与计数,会被排除,避免tool链断裂。

3.4 Demo 设计:水果游戏

连续6轮让用户只说水果名,第6轮问“按顺序列出你记得的水果”:

string[] prompts = [
    "第1轮:说「苹果」。",
    "第2轮:说「香蕉」。",
    "第3轮:说「橙子」。",
    "第4轮:说「葡萄」。",
    "第5轮:说「西瓜」。",
    "第6轮:请按顺序列出你记得我说过哪些水果(只列水果名)。",
];

如果不截断,模型可能列出6个水果;如果只保留4条,模型往往只能稳定记住后4个(香蕉、橙子、葡萄、西瓜),苹果大概率会被裁掉。每轮打印存储条数,你会看到存储量持续增长,但模型的“记忆”却受reducer限制——这就是截断策略最直观的实验。

3.5 方法代码

AgentFactory.CreateWithTruncation 把配置收敛成一行:

public static AIAgent CreateWithTruncation(IChatClient chatClient, string instructions, string name, int maxNonSystemMessages)
{
    var historyProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
    {
        ChatReducer = new MessageCountingChatReducer(maxNonSystemMessages),
        ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval,
    });
    return CreateWithSessionHistory(chatClient, instructions, name, historyProvider);
}

3.6 注意点

  • maxMessages 太小会导致“失忆”,过早的内容被丢掉;太大则失去截断的意义,需要根据模型的context大小和业务场景来调参。
  • 如果多轮对话中使用了Function Tool,需要谨慎截断,避免裁断tool call / tool result的配对。
  • 还有 SummarizingChatReducer(把旧对话摘要成一条)等其他选择,适合需要“保留语义”而不是“硬砍条数”的场景——这个可以后面单独开一篇讲。
  • ReducerTriggerEvent.AfterMessageAdded 会在写入存储时就缩减;而 BeforeMessagesRetrieval 只影响读出,存储仍然完整。Demo用的是后者,便于观察“存得多、读得少”这种状态。

四、三种能力一张表

能力核心 API是否清空 Session典型场景
清除历史SetMessages(session, [])否,只清消息新对话、隐私、换话题
注入 SystemEnqueueMessages(session, [System...])否,追加规则切换语言、临时策略
截断ChatReducer on Provider否,裁剪读出长对话、控 Token
AgentSession(会话身份不变)
│
├── 清除历史 → 消息列表 = []
├── 注入 System → 队列里多一条 System,下轮生效
└── 截断 → 存储可很长,读出时变短

五、拓展知识

5.1 清除 vs 新建 Session

做法优点缺点
SetMessages([], …)同一sessionId,前端不用换StateBag 里其它状态还在
CreateSessionAsync() 新的彻底隔离要管理更多 session 对象

具体选哪种取决于产品需求。很多App的“新对话”功能,其实就是创建了一个新Session。

5.2 注入消息还能干什么?

EnqueueMessages 不限于System消息,也可以注入 UserAssistant 消息(例如模拟用户确认、插入RAG检索结果)。当然,System注入最常见,因为它的目的是“改变行为”而不是“冒充用户原话”。

5.3 Reducer 生态(Microsoft.Extensions.AI)

Reducer策略
MessageCountingChatReducer按条数保留最近 N 条
SummarizingChatReducer旧消息用大模型摘要成一条

MAF的 Compaction 命名空间下还有更复杂的压缩管线,适合超长Agent任务。

5.4 和手动 History 的关系

如果你手动维护 List

  • 清除:history.Clear()
  • 注入:history.Insert(0, new ChatMessage(System, …)),自己控制位置
  • 截断:history = (await reducer.ReduceAsync(history)).ToList()

MAF的Provider + Reducer本质上就是把这套流程标准化、可插拔。理解手动版有助于遇到问题时debug。

5.5 生产 checklist

  1. 长会话必须配置截断或摘要,并持续监控Token消耗。
  2. “新对话”要清历史或新建Session,避免串话。
  3. 动态规则用注入,静态角色用Instructions,不要混为一谈。
  4. 预览API(如MessageInjectingChatClient)要关注MAF版本的升级说明。

六、系列小结(3 上 + 3 下)

(3 上)Agent 会「记住」
    AgentSession + InMemoryChatHistoryProvider
    手动 List

(3 下)Agent 会「管记忆」
    清除历史 → SetMessages / SetInMemoryChatHistory
    注入 System → EnableMessageInjection + EnqueueMessages
    截断策略 → MessageCountingChatReducer + BeforeMessagesRetrieval

配合系列前两篇:

(1)会「说」→ RunAsync / RunStreamingAsync
(2)会「做」→ AIFunctionFactory + tools
(3)会「记」→ Session + ChatHistory
(3 下)会「管」→ 清除 / 注入 / 截断

来源:互联网

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

同类文章推荐

相关文章推荐

更多