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