1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI の会話がどんどん長くなる?スライディングウィンドウでコンテキストをダイエット

1
Posted at

スライディングウィンドウとは

AI アプリを作ると、対話のターン数が増えるほど、コンテキストはどんどん長くなります。

特にツール呼び出しを伴うシーンでは顕著です。ユーザーが一言質問し、モデルが一度ツールを呼び出し、ツールが大量の内容を返し、モデルがそれをもとにもう一度回答を組み立てる。数ラウンド進むだけで、Token はあっという間に増えていきます。

その解決策のひとつが、エージェントにスライディングウィンドウの圧縮を加えることです。

スライディングウィンドウとは

要するに、モデルには直近の会話だけに注目させます。対話が設定したターン数を超えると、古いチャット内容は有効なコンテキストから外され、最近のやり取りだけが残ります。

古い内容を要約したり、長期記憶として抽出したりはしません。やることはとても直接的です。古い内容を বাইরে に出し、新しい内容を入れて、コンテキストを常に制御可能な範囲に保ちます。

この方法は、小説検索、商品検索、天気検索などのシーンに向いています。各質問が比較的独立しており、古い内容を忘れても後続の結果に影響しません。

なぜメッセージを数件削除するだけではいけないのか

1 回の完全なツール呼び出しには、通常、ユーザーの質問、モデルの呼び出し開始、ツールの戻り値、そしてモデルの最終回答が含まれます。

これらのメッセージはひとつの完全な論理単位です。メッセージ数だけを見て適当に削除すると、ツール呼び出しだけ残ってツールの戻り値が削られたり、その逆になったりする可能性があります。

SlidingWindowCompactionStrategy はメッセージグループ単位で履歴を処理し、ツール呼び出しチェーンの完全性をできるだけ保ちます。同時に、システムメッセージは引き続き保持され、ウィンドウがスライドしても失われません。

このコードは何をしているのか

このサンプルは、小説検索アシスタントを作成し、GetNovel ツールを登録しています。

ユーザーが 5 冊の小説を連続で検索します。各検索で、モデルはツールを呼び出し、書名に応じた小説内容を取得できます。

ここでは TurnsExceed(4) によってトリガー条件を設定しています。対話ターンが 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;

#pragma warning disable

var 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"
);

void PrintMessageGroups(
    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("名前で 1 冊の小説を取得する")]
static string GetNovel(
    [Description("名前(書名かぎかっこ付き)")] string name)
{
    var novels = new Dictionary
    {
        {
            "《雾港来信》",
            "夜明けの霧が港を未開封の手紙のように包み込んでいた。林澈は古い郵便局で夜勤をしていたとき、消印のない青い封筒を見つける。宛先は、30年前に失踪した母親だった。封筒の中には一文だけが書かれていた。——灯台を消してはならない。その夜、廃墟になった灯台が突然点灯し、海面から聞き覚えのある歌声が響いた。林澈が声を追うと、母親が若い姿で波頭に立っていた。彼女はこう告げる。港は30年ごとに記憶を飲み込み、灯台は忘れられた人々を照らして呼び戻せるのだ、と。夜明けまでに、林澈は選ばなければならない。母親を留めるか、それとも都市全体が失った過去を取り戻すか。"
        },
        {
            "《纸月亮》",
            "町の空には毎晩、紙でできた月が昇る。薄くて、裏側の星明かりまで見えるほどだ。阿眠は町で唯一の月修理人で、銀糸で月面のひびを縫い合わせていた。ある日、彼女は裂け目の中に逆さまの都市を見つける。そこでは、人々は昼に生き、夜になると影になる。都市の少年・紀南は、百年も紙の裏側に閉じ込められているのだと語り、月を切り落としてほしいと頼む。だが月が落ちれば、町は永遠に夜を失ってしまう。阿眠は自分の影を裂け目へ送り込み、紀南の代わりに都市を支えさせた。それ以来、紙の月は変わらず昇り続ける。ただ月光の中には、ひとりの少女が縫い合わせる姿が映るようになった。"
        },
        {
            "《第七座花园》",
            "王宮の後ろには 6 つの庭園があり、どれも四季を通して花が絶えない。だが伝説の第 7 の庭園だけは、誰も見たことがなかった。庭師の少年・陸安はバラを剪定しているとき、壁の内側から自分の名前を呼ぶ声を聞いた。つる草に覆われた小さな扉を押し開けると、色のない花の海へと入っていった。庭を守る盲目の王女は言う。ここには人々が諦めた願いが集められており、色を取り戻してこそ、この庭園は人間界に戻れるのだ、と。陸安は自分の夢を一輪一輪、花に語りかけた。すると白い花は少しずつ、朝焼けや湖水や炎の色に染まっていった。だが最後の一輪には、彼の未来が必要だった。陸安は笑って種を植えた。目覚めると彼はすでに老人で、手には金色の花びらが一枚握られていた。"
        },
        {
            "《雨中棋局》",
            "江南に 7 日間雨が降り続いた。茶館に傘を持たない老人がやって来る。彼は盤を広げ、勝った者には失ったものを一つ返してやると言った。多くの人が記憶を失って負ける中、傘売りの娘・蘇青だけが席を立たなかった。彼女は戦死した兄の最後の言葉を取り戻したかったのだ。中盤に差しかかると、雨音が突然、馬蹄の音に変わり、碁石は戦場の兵器へと変わった。蘇青は兄が陣中に閉じ込められているのを見て、老人が賭博師ではなく、亡魂の帰路を探す引き碁人だと悟る。彼女は車を捨てて帥を守り、黒子に包囲を突破させた。対局が終わると老人は消え、傘の下にため息が一つ落ちた。——しっかり生きろ、もう雨が上がるのを待つな。"
        },
        {
            "《星火列车》",
            "真夜中0時、廃駅には切符のない列車が停まり、車窓は星明かりで満ちている。少年・遅野は家を出た父を探すため、こっそり最後尾の車両に乗り込んだ。車掌は、この列車は未完の別れだけを乗せるのだと告げる。各駅に着くたび、乗客は思いを寄せる人に1分だけ会えるのだ。遅野が流星のトンネルを抜けると、父は石炭ストーブのそばで懐中時計を修理していた。父は、自分は離れたのではなく、車輪を動かす星火となって、息子の成長を見守っていたのだと言う。終点の前に、遅野は懐中時計を父に返した。列車は再び走り出し、地平線にはかすかな新しい星が一つ増え、遅野はようやく帰ることを覚えた。"
        }
    };

    return novels.TryGetValue(name, out var novel)
        ? novel
        : "この小説は見つかりませんでした。";
}

スライディングウィンドウと要約圧縮、どちらを選ぶか

古い対話に長期的な価値がなく、直近数ラウンドだけを見ればよいなら、スライディングウィンドウが最も簡単です。ユーザーが前に言った予算、嗜好、重要な資料が、かなり後になっても必要になるなら、要約圧縮の方が適しています。片方は直接忘れ、もう片方は整理してから記憶します。どちらを選ぶべきかの鍵は、技術の複雑さではなく、古いメッセージに後続の業務上の価値がまだあるかどうかです。

最後に

AI のコンテキストは長ければ長いほどよいわけではありません。関係のない履歴メッセージが多すぎると、Token 消費が増えるだけでなく、モデルの判断を妨げることもあります。スライディングウィンドウの考え方はシンプルです。最近の、本当に有用な対話だけを残し、モデルを身軽にすることです。

(Translated by GPT)

元のリンク:https://mp.weixin.qq.com/s/cLQHDhgX795qukehZnEZIQ?token=620137141&lang=zh_CN&wt.mc_id=MVP_325642

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?