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

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


写在前面

(3 上)我们让 Agent 会记住------多轮里能答出「你叫小明」「你喜欢 C#」。

但真实产品里,光有记忆还不够,还要会 、会 改规矩 、会 省 Token

需求 MAF 能力
清除历史 SetMessages / SetInMemoryChatHistory
注入 System MessageInjectingChatClient.EnqueueMessages
截断策略 IChatReducer + MessageCountingChatReducer

这三件事,就是本篇的全部内容。


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

1.1 为什么需要清除?

多轮记忆是双刃剑。用户点了 「新对话」,你还把上一轮「记住数字 42」带进上下文,既浪费 Token,也可能答非所问。

清除历史 ≠ 销毁 AgentSession

Session 还在 (同一会话 ID、同一块 StateBag),只是 消息列表被清空------像微信里「清空聊天记录」,窗口没关。

1.2 实现步骤

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

步骤 2:先聊两轮,验证「记得住」:

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

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

csharp 复制代码
// 写法 A:通过 Provider
historyProvider.SetMessages(session, []);

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

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

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

1.3 Demo 关键代码

csharp 复制代码
Console.WriteLine("--- 执行清除历史 ---");
historyProvider.SetMessages(session, []);
PrintHistory("清除后", historyProvider, session);  // 应为 0 条

await RunTurnAsync(agent, session, "我刚才让你记住的数字是多少?", cancellationToken);

1.4 注意点

  • 清的是 ChatHistory 消息 ,不是 Instructions(创建 Agent 时的系统角色仍在)。
  • 若只清 Session 却换了一个没挂同一 Provider 的 Agent,行为可能不一致------同一 Agent + 同一 Provider 实例 最稳妥。
  • 生产环境还可 新建 SessionCreateSessionAsync())代替清空,效果类似「全新对话窗口」。

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

2.1 和 Instructions 有什么不同?

(3 上)讲过:ChatOptions.Instructions创建 Agent 时 写好,相当于入职手册。

有时要在 聊了一半 才改规则,例如:

  • 用户点击「切换英文」
  • 运营活动临时加一条「今日禁止讨论价格」
  • 工具执行完后插入 hidden system 提示

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

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

2.2 机制:MessageInjectingChatClient

MAF 在管道里加一层 MessageInjectingChatClient

text 复制代码
RunAsync 触发
    → 从 Session.StateBag 取出「待注入消息队列」
    → 合并进本次发给模型的 messages
    → 调用大模型

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

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

2.3 实现步骤

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

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

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

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

csharp 复制代码
MessageInjectingChatClient? injector = agent.GetService<MessageInjectingChatClient>();
if (injector is null)
{
    // 说明 EnableMessageInjection 未生效
    return;
}

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

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

csharp 复制代码
await agent.RunAsync("用一句话介绍 MAF。", session);

2.4 形象理解

把对话想成开会:

  • Instructions:会议开始前发的议程(一直有效)
  • EnqueueMessages(System):会中主席突然补充:「接下来请用英文发言」

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

2.5 注意点

  • 必须 EnableMessageInjection = true,否则 GetService<MessageInjectingChatClient>() 为 null。
  • 注入的是 下一轮(或同轮 pipeline 内下一次模型调用)才生效,不是改已经发出去的历史。
  • 模型不一定 100% 遵守新 System,和写静态 Instructions 一样要靠 prompt 与评测。

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

3.1 为什么需要截断?

(3 上)历史会一直 append。聊 50 轮后:

  • Token 爆掉 ------ 超 context window,API 报错或截断
  • 变慢变贵 ------ 每次带全长历史
  • 干扰答案 ------ 早期无关内容稀释注意力

所以要在 发给模型之前 ,对历史做 Reduce(缩减) 。MAF 通过 IChatReducer 挂在 InMemoryChatHistoryProvider 上实现。

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

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

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

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

csharp 复制代码
new InMemoryChatHistoryProviderOptions
{
    ChatReducer = new MessageCountingChatReducer(maxMessages),
    ReducerTriggerEvent = InMemoryChatHistoryProviderOptions
        .ChatReducerTriggerEvent.BeforeMessagesRetrieval,
}

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

3.3 MessageCountingChatReducer 做什么?

csharp 复制代码
ChatReducer = new MessageCountingChatReducer(4)  // 最多保留 4 条「非 System」消息

行为(简化理解):

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

3.4 Demo 设计:水果游戏

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

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

不截断 ,模型可能列出 6 个;

只保留 4 条 ,模型往往只能稳定记住 后 4 个 (香蕉、橙子、葡萄、西瓜),苹果 可能被裁掉。

每轮打印存储条数,你会看到存储持续增长,但模型「记忆」受 reducer 限制------这就是截断策略的直观实验。

3.5 方法代码

AgentFactory.CreateWithTruncation 把配置收成一行:

csharp 复制代码
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, []) 否,只清消息 新对话、隐私、换话题
注入 System EnqueueMessages(session, [System...]) 否,追加规则 切换语言、临时策略
截断 ChatReducer on Provider 否,裁剪读出 长对话、控 Token
text 复制代码
         AgentSession(会话身份不变)
              │
              ├── 清除历史     → 消息列表 = []
              ├── 注入 System  → 队列里多一条 System,下轮生效
              └── 截断         → 存储可很长,读出时变短

五、拓展知识

5.1 清除 vs 新建 Session

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

按产品需求选;很多 App 的「新对话」其实是 新 Session

5.2 注入消息还能干什么?

EnqueueMessages 不限 System,也可注入 User / Assistant(例如模拟用户确认、插入 RAG 检索结果)。

System 注入最常见,因为 改行为而不冒充用户原话

5.3 Reducer 生态(Microsoft.Extensions.AI

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

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

5.4 和(3 上)手动 History 的关系

手动 List<ChatMessage> 时:

  • 清除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 下)

text 复制代码
(3 上)Agent 会「记住」
    AgentSession + InMemoryChatHistoryProvider
    手动 List<ChatMessage>

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

配合系列前两篇:

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

相关推荐
盛夏光年爱学习1 小时前
Agentic RAG 深度解析:让 Agent 自己决定要不要检索、检索几次,这才是 RAG 的正确打开方式
人工智能
Coder小相1 小时前
LangChain 1.0 第五篇 - Tool与MCP让Agent拥有行动力
人工智能·langchain·ai编程
太华1 小时前
学习AI Agent编程-第五天-LlamaIndex - 将Nodes生成索引并存储
人工智能
XLYcmy1 小时前
面向Agent权限系统的快速审计工具
python·网络安全·ai·llm·飞书·agent·字节跳动
guyoung1 小时前
BoxAgnts 运行时(1)——运行时工程决定 Agent 未来
agent·ai编程
太华1 小时前
学习AI Agent编程-第三天-LlamaIndex - 如何将PDF文件正确转成Document
人工智能
Artech1 小时前
[MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合
ai·agent·maf·agent管道
jiayong231 小时前
AI架构师面试问题与解答 - 深度学习架构篇
人工智能·深度学习
unclejet1 小时前
颠覆传统开发!AI根治软件工程技术债务顽疾
大数据·人工智能·软件工程