夜雨聆风学习资料网

ARTICLE · 1067666

AI 对话越聊越长?用滑动窗口给上下文减减肥

AI 对话越聊越长?用滑动窗口给上下文减减肥
做 AI 应用时,只要对话轮数一多,上下文就会越来越长。

尤其是带工具调用的场景。用户问一句,模型调用一次工具,工具返回一大段内容,模型再组织一次回答。几轮下来,Token 很快就上去了。

解决办法之一,就是给智能体加上滑动窗口压缩。

滑动窗口是什么

说白了,就是只让模型关注最近的对话。当对话超过设定轮数后,较早的聊天内容会从有效上下文中移出,只保留最近的交流。

它不会给旧内容写摘要,也不会提炼长期记忆。它做的事情很直接:旧内容出去,新内容进来,让上下文始终保持在一个可控范围内。

这种方式很适合小说查询、商品查询、天气查询等场景。每次提问相对独立,旧内容忘掉也不会影响后面的结果。

为什么不能直接删除几条消息

一次完整的工具调用,通常包括用户提问、模型发起调用、工具返回结果以及模型最终回答。

这些消息是一个完整的逻辑单元。如果只按照消息数量随便删除,很可能留下工具调用,却删掉工具返回结果,或者反过来。

SlidingWindowCompactionStrategy 会按照消息组处理历史记录,尽量保证工具调用链的完整性。同时,系统消息会继续保留,不会因为窗口滑动而丢失。

这段代码做了什么

示例创建了一个小说查询助手,并注册了 GetNovel 工具。

用户连续查询五部小说。每次查询时,模型都可以调用工具,根据书名取得对应的小说内容。

这里通过 TurnsExceed(4) 设置触发条件。当对话轮次超过四轮后,滑动窗口开始工作,较早的消息组会被排除出后续发送给模型的有效上下文。

最后,程序把会话状态打印出来。红色代表已经被排除的旧消息组,绿色代表当前仍然参与对话的消息组。

完整代码

using AIConfig;using Azure;using Azure.AI.OpenAI;using Microsoft.Agents.AI;using Microsoft.Agents.AI.Compaction;using Microsoft.Extensions.AI;using System.ClientModel;using System.ComponentModel;using System.Text.Json;#pragmawarning disablevar aiConfig = new AzureOpenAIConfig(”gpt-5.6-luna”);var credential = new ApiKeyCredential(aiConfig.Key);var options = new JsonSerializerOptions(){    WriteIndented = true,    Encoder = System.Text.Encodings.Web.JavaScriptEncoder        .UnsafeRelaxedJsonEscaping};// 创建主对话模型客户端。var agentChatClient = new AzureOpenAIClient(        new Uri(aiConfig.Endpoint),        credential    )    .GetChatClient(aiConfig.DeploymentName)    .AsIChatClient();// 移出较早的聊天内容,只保留最近的对话窗口。// 压缩时按照完整对话单元处理,系统消息会始终保留。var compactionStrategy = new SlidingWindowCompactionStrategy(    trigger: CompactionTriggers.TurnsExceed(4));// 构建带有上下文压缩能力的智能体,// 并注册获取小说内容的工具函数。var agent = agentChatClient    .AsBuilder()    .UseAIContextProviders(        new CompactionProvider(compactionStrategy)    )    .BuildAIAgent(        new ChatClientAgentOptions        {            Name = ”Assistant”,            ChatOptions = new()            {                Instructions = ”你是一个友好的助手。”,                Tools =                [                    AIFunctionFactory.Create(                        GetNovel,                        nameof(GetNovel)                    )                ]            }        }    );// 创建会话,并连续发送多轮查询以观察压缩效果。var session = await agent.CreateSessionAsync();var list = new List{    ”我想找小说《雾港来信》”,    ”我想找小说《纸月亮》”,    ”我想找小说《第七座花园》”,    ”我想找小说《雨中棋局》”,    ”我想找小说《星火列车》”};foreach (var input in list){    Console.WriteLine(”您说:” + input);    var response = await agent.RunAsync(input, session);    Console.WriteLine(”助手说:”);    Console.WriteLine(response.Text);    Console.ForegroundColor = ConsoleColor.Yellow;    Console.WriteLine(        $”总Token数:{response.Usage.TotalTokenCount}\n” +        $”输入Token数:{response.Usage.InputTokenCount}\n” +        $”输出Token数:{response.Usage.OutputTokenCount}\n” +        $”缓存输入Token数:{response.Usage.CachedInputTokenCount}\n” +        $”推理Token数:{response.Usage.ReasoningTokenCount}”    );    Console.ResetColor();}var serializedSession =    await agent.SerializeSessionAsync(session);PrintMessageGroups(    serializedSession,    options,    ”SlidingWindowCompactionStrategy”);voidPrintMessageGroups(    JsonElement serializedSession,    JsonSerializerOptions options,    string propertyName){    if (serializedSession.TryGetProperty(            ”stateBag”,            out var stateBag        ) &&        stateBag.TryGetProperty(            propertyName,            out var strategy        ) &&        strategy.TryGetProperty(            ”messagegroups”,            out var messageGroups        ) &&        messageGroups.ValueKind == JsonValueKind.Array)    {        foreach (var item in messageGroups.EnumerateArray())        {            if (item.TryGetProperty(                    ”isExcluded”,                    out var isExcluded                ) &&                isExcluded.GetBoolean())            {                Console.ForegroundColor = ConsoleColor.Red;            }            else            {                Console.ForegroundColor = ConsoleColor.Green;            }            Console.WriteLine(                JsonSerializer.Serialize(item, options)            );        }    }    Console.ResetColor();}[Description(”按名称获取一部小说”)]staticstringGetNovel(    [Description(”名称(带书名号)”)] string name){    var novels = new Dictionary    {        {            ”《雾港来信》”,            ”清晨的雾把海港裹得像一封未拆的信。林澈在旧邮局值夜,发现一只没有邮戳的蓝色信封,收信人竟是三十年前失踪的母亲。信里只有一句话:别让灯塔熄灭。当晚,废弃灯塔忽然亮起,海面传来熟悉的歌声。林澈循声而去,看见母亲年轻时的身影站在浪尖。她告诉他,港口每隔三十年会吞下一段记忆,而灯塔能照回被遗忘的人。天亮前,林澈必须选择:留住母亲,还是救回整座城市失去的过去。”        },        {            ”《纸月亮》”,            ”小镇上空每晚都会升起一轮纸做的月亮,薄得能看见背后的星光。阿眠是镇上唯一的修月师,她用银线缝补月面的裂痕。一天,她在裂缝里发现一座倒悬的城市,那里的人白天生活,夜晚化作影子。城市少年纪南请求她剪断月亮,因为他们被困在纸背面已百年。可若月亮坠落,小镇将永远失去夜晚。阿眠把自己的影子送进裂缝,让它替纪南托住城市。此后纸月依旧升起,只是月光里多了一个女孩缝线的身影。”        },        {            ”《第七座花园》”,            ”王宫后有六座花园,四季不败,唯独传说中的第七座无人见过。园丁少年陆安在修剪玫瑰时,听见墙内有人呼唤他的名字。他推开爬满藤蔓的小门,进入一片没有颜色的花海。守园的盲眼公主说,这里收藏着世人放弃的愿望,只有找回颜色,花园才会重返人间。陆安把自己的梦想一朵朵讲给花听,白花渐渐染上晨霞、湖水与火焰的色彩。最后一朵花却需要他的未来。陆安笑着种下种子,醒来时已成老者,手中握着一瓣金色花瓣。”        },        {            ”《雨中棋局》”,            ”江南连下七日雨,茶馆里来了一位不带伞的老人。他摆开棋盘,说谁能赢他,便能换回一件失去之物。许多人输掉回忆,唯有卖伞女苏青坐下不走。她想换回战死兄长的最后一句话。棋至中盘,雨声忽变成马蹄声,棋子化作沙场上的兵甲。苏青看见兄长被困阵中,才明白老人并非赌徒,而是替亡魂寻归路的引棋人。她弃车保帅,让黑子冲出重围。棋局终了,老人消失,伞下落下一声轻叹:好好活着,别再等雨停。”        },        {            ”《星火列车》”,            ”午夜零点,废弃车站会停下一列没有车票的列车,车窗里盛满星光。少年迟野为寻找离家的父亲,偷偷登上最后一节车厢。列车长告诉他,这趟车只载未完成的告别,每到一站,乘客便能见到想念之人一分钟。迟野穿过流星隧道,看见父亲坐在煤炉旁修理怀表。父亲说自己并未离开,只是成了驱动车轮的星火,守着儿子长大。终点前,迟野把怀表还给他。列车再次启程,天边多了一颗微亮的新星,而迟野终于学会回家。”        }    };    return novels.TryGetValue(name, out var novel)        ? novel        : ”没有找到这部小说。”;}

滑动窗口和摘要压缩怎么选

如果旧对话没有长期价值,只需要关注最近几轮内容,使用滑动窗口最简单。如果用户前面说过的预算、偏好和关键资料,在很久以后仍然需要用到,就更适合使用摘要压缩。一个负责直接忘掉,一个负责整理后再记住。具体选择哪一种,关键不在技术有多复杂,而在旧消息对后续业务还有没有价值。

写在最后

AI 的上下文不是越长越好。无关的历史消息太多,不仅会增加 Token 消耗,还可能干扰模型判断。滑动窗口的思路很简单:只留下最近真正有用的对话,让模型轻装上阵。

相关学习资料