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 の対話が長すぎるときは? トランケーション圧縮で Token 上限を守る

1
Posted at

AI の対話は話せば話すほど長くなり、入力 Token もそれに伴って増えていきます。

会話の中にツール呼び出しがあると、増加速度はさらに速くなります。ユーザーが一言質問し、モデルがツールを呼び出し、ツールがさらに大量の内容を返す、という流れが何回か続くと、モデルのコンテキスト上限にかなり近づくことがあります。

そんなときに使えるのが TruncationCompactionStrategy で、コンテキストに強制的な制限を加えられます。

トランケーション圧縮とは

トランケーション圧縮の考え方はとてもシンプルです。

コンテキストが指定した Token 数を超えたら、最も古い非システムメッセージグループから削除し、Token 数が目標範囲に戻るまで続けます。

別のモデルを呼び出すことも、会話要約を生成することもありません。単純に古い内容を削除するだけです。

Microsoft の定義では、これはややアグレッシブなフォールバック戦略であり、コンテキストが厳格な Token 予算を超えないようにするのに適しています。

2つの重要なパラメータ

コードでは TokensExceed(1000) によってトリガー条件を設定しています。

有効なコンテキストが 1000 Token を超えると、圧縮戦略が開始されます。個別の停止条件を設定していなければ、Token 数が 1000 を超えなくなるまで、古いメッセージグループを削除し続けるのがデフォルトです。

minimumPreservedGroups を 3 に設定すると、直近 3 つの非システムメッセージグループは、どのような場合でも保持されます。

これは強制的な下限です。3 つのメッセージグループを残してもなお 1000 Token を超えている場合でも、戦略はそれ以上それらを削除しません。

なぜメッセージグループ単位で削除するのか

1 回のツール呼び出しは 1 件のメッセージだけではありません。

ユーザーの質問、モデルのツール呼び出し、ツールの戻り値、そしてモデルの最終回答を含む場合があります。

もし適当に 1 件だけ削除すると、ツール呼び出しの流れが不完全になります。トランケーション戦略はメッセージグループの境界を尊重し、ツール呼び出しと対応するツール結果をまとめて削除します。

システムメッセージは削除対象にならないため、エージェントの役割設定はそのまま保持されます。

完全なコード

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();

// Token が 1000 を超えたら、最も古い非システムメッセージグループから削除。
// 目標に達するかどうかにかかわらず、直近 3 つのメッセージグループは保持される。
var compactionStrategy = new TruncationCompactionStrategy(
    trigger: CompactionTriggers.TokensExceed(1000),
    minimumPreservedGroups: 3
);

// コンテキスト圧縮機能を持つエージェントを構築し、
// 小説内容を取得するツール関数を登録する。
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,
    "TruncationCompactionStrategy"
);

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

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

実行すると何が見えるか

この例では 5 冊の小説を連続で検索します。ツールは毎回かなり長い内容を返すため、コンテキストはすぐに 1000 Token を超えます。制限を超えると、最も古いメッセージグループが除外対象としてマークされ、以後のモデルリクエストには含まれなくなります。

プログラムの最後ではセッション内のメッセージグループを出力します。赤は有効コンテキストから外れたもの、緑は現在も保持されているものを示します。これらの古いメッセージはセッション状態には残っている可能性がありますが、モデルへは送信されなくなります。

トランケーション圧縮が適している場面

トランケーション圧縮の最大の利点は、シンプルで低コストなことです。要約生成のために別のモデルを追加で呼び出す必要がありません。欠点も明確で、削除した後はモデルがその古い内容を知ることはできません。

そのため、比較的独立した問い合わせタスク、あるいはコンテキスト溢れを防ぐ最後の保険として適しています。

古い会話にユーザーの好み、業務ルール、長期的に記憶すべき情報が含まれている場合は、まず要約圧縮を検討し、最後のフォールバックとしてトランケーション圧縮を使うべきです。

最後に

トランケーション圧縮は、すべての内容を覚えておくことを目指しません。解決するのは、より現実的な別の問題です。コンテキストが入りきらなくなりそうなとき、まず対話が継続できることを保証するのです。

Token 上限を超えたら最も古い内容から整理しつつ、直近のいくつかの完全なメッセージグループは守る。シンプルで、直接的で、十分に実用的です。

(Translated by GPT)

元のリンク:https://mp.weixin.qq.com/s/-lFBvNIZiytLj1Zf6POSVw?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?