Agent Mentor Learn
エージェントメモリと状態 · 第 2 回 / 全 6 回

レッスン2: 会話履歴の管理: 追加、切り詰め、要約

学習目標:

  • デフォルトで会話履歴が成長するだけで縮小しない理由を説明する
  • 切り詰めが何を捨て、何を保持し、どのような構造を壊す可能性があるかを述べる
  • コンパクションとツール結果クリアリングがそれぞれ解決する問題を区別する
  • ウィンドウ使用量と膨張しているコンテンツの種類に基づいて、どのメカニズムを使用するかを判断する

前提: レッスン1を終え、コンテキストウィンドウが何で構成されているかを理解していること | 前: レッスン1 << | 次: レッスン3 >>

追加がデフォルト: 履歴が成長し続ける理由

レッスン1では、モデルが見る履歴は、ホストアプリが毎ターン再送信するものであるという点を示しました。では、実際にどのように送信されるのでしょうか? 最も単純な実装は追加です: ターンが終了すると、そのターンからの新しいメッセージ(ユーザーの言葉、モデルの返信、ツール呼び出しとその結果)を既存のmessages配列の末尾に追加し、次のターンでは配列全体をそのまま送り返します。

公式ドキュメントは、このデフォルトを明確に述べています: 会話が進むにつれて、各ユーザーメッセージとモデルの返信がコンテキストウィンドウ内に蓄積され、以前のターンはすべて完全に保存されます。"As the conversation advances through turns, each user message and assistant response accumulates within the context window, and previous turns are preserved completely."1 (会話がターンを通じて進むにつれて、各ユーザーメッセージとアシスタントの応答がコンテキストウィンドウ内に蓄積され、以前のターンは完全に保存されます) 誰も積極的に何かを削除していないため、履歴は増加するだけです — 10ターン目には、ウィンドウは10ターン分のコンテンツ全体を保持しており、最新ターンの要約でも自動フィルタリングされたハイライトでもありません。

短い会話では、これは問題になりません。しかし、長時間実行されるエージェントにとって、問題は雪だるま式に大きくなります: すべてのツール呼び出しの完全な引数と完全な戻り値が履歴に詰め込まれ、ファイルを繰り返し読み取りコマンドを実行するタスクは、数十ターン後にmessages配列を簡単に数万トークンまで押し上げることができます。レッスン1では、ウィンドウには容量の上限があり、それが満杯に近づくほど上限に達しやすいことをカバーしました。より微妙なコストはコンテキストロットです — 履歴が長く乱雑になるほど、モデルが今まさに重要な1行を見つけるのが困難になります1。履歴を無制限に成長させると、最終的に両方の代償を払うことになります。

切り詰め: 最も単純で最も粗雑なオプション

最も直接的な対応は切り詰めです: ウィンドウがほぼいっぱいになったら、最も古いメッセージのバッチをそのまま切り取り、最新のNターンだけを保持します。これは最も構築が簡単です — 追加のモデル呼び出しも、設計する要約フォーマットもありません。messages.slice(-N)の1行で完了です。

しかし、切り詰めが削除するものは永久に失われます。切り取ったバッチに、ユーザーがターン3で述べた重要な制約(「予算は$5,000以下に抑える」)が含まれていて、エージェントが今ターン40で注文しようとしている場合、その情報は単に消えます。モデルはそれをかつて「見て」、それから「忘れた」とは認識しません — それが決して伝えられなかったかのように振る舞うだけです。

切り詰めには、より隠れた罠もあります。これは前のコース「エージェントツール呼び出し: エージェントに実際に行動させる」のレッスン2「ツール呼び出しの完全なラウンドトリップ」のラウンドトリッププロトコルに直接関連しています: 単純に「最後のNメッセージ」にスライスすることで切り詰めると、tool_use / tool_resultペアの途中で簡単に切断してしまう可能性があります — 呼び出しを発火したassistantメッセージは保持するが、その直後にあったtool_resultメッセージをスライスオフしてしまいます。その履歴をモデルに送信すると、プロトコル自体が壊れます。ドキュメントは、"Tool result blocks must immediately follow their corresponding tool use blocks in the message history."2 (ツール結果ブロックは、メッセージ履歴内で対応するツール使用ブロックの直後に続かなければなりません) と明示しています。"tool_use ids were found without tool_result blocks immediately after" のようなエラーは、ペアリングが壊れていることを示す信号です2 — モデルは「呼び出しを開始した」ことを見ますが、その呼び出しの結果を受け取ることがなく、次のリクエストは完全に失敗します。

コンパクション: ウィンドウを1つの要約に圧縮する

切り詰めの問題は、全体のストレッチを破棄することです。情報を完全に捨てずにスペースを解放する方法はありますか? それがコンパクションが解決する問題です。公式Cookbookはそれをこう定義しています: "Compaction distills the contents of a context window into a high-fidelity summary, letting the agent continue with minimal performance degradation when the conversation gets long."3 (コンパクションは、コンテキストウィンドウの内容を高忠実度の要約に蒸留し、会話が長くなったときにエージェントが最小限のパフォーマンス低下で継続できるようにします)

切り詰めの「全体のストレッチを削除する」とは異なり、コンパクションは「全体を書き換える」ものです: 以前の会話履歴は、生のメッセージの長い実行を置き換え、ウィンドウの先頭に留まる単一の高忠実度の要約に圧縮されます。要約が保持するのは「何が起こり、何が結論されたか」です; それが削除するのは、一語一語の生の対話の詳細です。

ドキュメントは、このメカニズムのパラメータを詳しく説明しています。デフォルトのトリガー閾値があります — コンパクションは、ウィンドウ使用量が150Kトークンに達したときに自動的に発火します; 閾値は構成可能ですが、50Kトークンを下回ることはできません。これはサーバーで強制される下限です3 4。各トリガーは離散的な置換です: 大きな履歴のストレッチが要約と交換され、新しいメッセージはその後で通常どおり追加され続けます。これは1回限りのイベントではありません — ドキュメントは、長い会話が複数回コンパクトできることを明示しており、"The last compaction block reflects the final state of the prompt, replacing content prior to it with the generated summary."4 (最後のコンパクションブロックは、プロンプトの最終状態を反映し、それ以前のコンテンツを生成された要約に置き換えます) 再びコンパクトすると、以前のコンパクションブロックは履歴の残りとともに新しい要約に折り畳まれます; コンパクションは、"user messages, assistant messages, tool calls, tool results, even prior compaction blocks are all flattened into the summary."3 (ユーザーメッセージ、アシスタントメッセージ、ツール呼び出し、ツール結果、さらには以前のコンパクションブロックまでもがすべて要約に平坦化されます) という全トランスクリプト操作です。

コンパクションは無料ではありません。コンパクトする行為には追加のモデル呼び出しがかかり(要約モデルが実行されます)3、どれほど注意深く書かれても、要約は元の損失のあるバージョンです — "The summary preserves key decisions and facts but may drop specific numbers or exact phrasing."3 (要約は重要な決定と事実を保持しますが、特定の数字や正確な言い回しを削除する可能性があります) 後のステップが、要約されてしまった小さな詳細(たとえば、ある変数の正確なスペル)に依存している場合、その詳細は失われている可能性があります。そのため、コンパクションは「全体的なコンテキストが大きくなりすぎた」という粗粒度の問題に適合しますが、あらゆる種類の履歴膨張の万能薬として機能するわけではありません。

ツール結果クリアリング: 古くなった部分だけをクリアする

履歴膨張の大きな要因は、ツール呼び出し自体です。エージェントがファイルを読み取ったりコマンドを実行したりするたびに、完全な戻り値が履歴に詰め込まれます — 数千行の長さのファイルを読み取ると、誰もその詳細を必要としなくなった10ターン後でも、それらの数千行がmessages配列にそのまま残ります。Cookbookはこれを直接指摘しています: "Tool-result clearing addresses the bloat from tool use itself. As an agent pulls in tools and calls them, the results pile up, and deciding how much of that tool output to keep becomes an increasingly important part of managing context."3 (ツール結果クリアリングは、ツール使用自体からの膨張に対処します。エージェントがツールを引き込んで呼び出すと、結果が積み重なり、そのツール出力のどれだけを保持するかを決定することが、コンテキスト管理のますます重要な部分になります)

ツール結果クリアリングは、これに直接狙いを定めたメカニズムです: それは "drops old, re-fetchable results while keeping the record that the call happened."3 (呼び出しが発生したという記録を保持しながら、古い再取得可能な結果を削除します) これが重要な区別です — クリアリングは、ツールが返した具体的なコンテンツ(それらの数千行のファイルコンテンツ)を削除しますが、「エージェントがこのパスでread_fileを呼び出した」という記録を消去しません。そのコンテンツが後で再び必要になった場合、エージェントはどのツールを呼び出し、どの引数を渡したかを知っており、それを再度呼び出してコンテンツを取り戻すかどうかを決定できます。

そのトリガー閾値と保持ポリシーにも明確なデフォルトがあります: クリアリングはウィンドウ使用量が100Kトークンに達したときに発火し、デフォルトで最新の3つのツール呼び出しの完全な結果を保持し、古いツール結果をクリアします3。100Kトリガーはコンパクションの150Kよりも低く、その役割に適合しています — まず、「最も膨張しやすく、再取得が最も簡単な」ツール出力を処理し、それが十分でない場合は、全体的なウィンドウをコンパクションに引き渡します。

3つの中から選ぶ: メンタルモデル

これで2つのメカニズムが手に入り、レッスン3でカバーする外部メモリを加えると3つになります。Cookbookは、それらの分業を整理するコンパクトなメンタルモデルを提供します: "compaction compresses the whole window when it grows too large, clearing drops stale re-fetchable data inside the window, and memory moves information out of the window so it survives across sessions."3 (コンパクションはウィンドウが大きくなりすぎたときに全体を圧縮し、クリアリングはウィンドウ内の古い再取得可能なデータを削除し、メモリはセッション間で存続するように情報をウィンドウの外に移動します)

それらの優先順位と使用例は競合していません — それらは階層化されています:

  • ツール結果クリアリングは「このコンテンツはまだウィンドウ内にあるが古くなっており、削除しても問題ない。なぜなら再取得できるから」を処理します — 最も的を絞った、最も安価なものです。
  • コンパクションは「ウィンドウ全体が大きくなりすぎた」を処理します。コンテンツがどこから来たかに関係なく、すべてを1つの要約に書き換えます — より広い範囲ですが、損失があり、追加のモデル呼び出しがかかります。
  • メモリ(次のレッスンのトピック)は「この情報はこの1つの会話だけに存在すべきではなく、次のセッションまで持続する必要がある」を処理します — 「ウィンドウがすべてを保持できない」をまったく解決しているのではなく、「この会話が終わると、ウィンドウ内のすべてが消える」を解決しています。

このレッスンが冒頭で開いた質問に戻ります: 履歴が成長するだけなのは、誰も積極的にクリアしていないからです。切り詰め、コンパクション、およびツール結果クリアリングは、異なるコストと異なる状況でクリアする3つの方法です — どれを選ぶかは、何を保持したいか、そしてそれを保持するためにどれだけ支払う意思があるかに依存します。

まとめ

  • デフォルトで会話履歴は成長するだけです: 各ターンのメッセージがウィンドウ内に積み重なり、以前のターンは完全に保持され、誰も積極的にクリアしないため、無制限に上昇します
  • 切り詰めは最も単純ですが、それが削除するものは不可逆的であり、カットポイントがtool_use / tool_resultペアの途中に着地すると、ツール呼び出しのプロトコル構造を壊します
  • コンパクションはウィンドウ全体の履歴を1つの高忠実度要約に書き換え、デフォルトで150Kトークンで発火します(閾値は50K未満にはできません。サーバーで強制されます)。損失があり、1回の追加モデル呼び出しがかかります; 長い会話は複数回コンパクトされる可能性があり、以前の要約ブロックは新しい要約に折り畳まれます3 4
  • ツール結果クリアリングは、呼び出し記録を保持しながら、古い再取得可能なツール出力のみを削除し、デフォルトで100Kトークンで発火し、最後の3つの呼び出しの結果を保持します — コンパクションよりも的を絞っています
  • 3つは異なる仕事を持っています: クリアリングは古い再取得可能なデータを処理し、コンパクションは大きすぎる全体的なウィンドウを処理し、メモリはセッション間で存続することを処理します — どれを選ぶかは、膨張の具体的な原因と詳細を失う余裕があるかどうかに依存します

>> レッスン3: 外部メモリ: ファイルと検索

Footnotes

  1. Context windows — https://platform.claude.com/docs/en/build-with-claude/context-windows 2

  2. Handle tool calls — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

  3. Context engineering: memory, compaction, and tool clearing — https://platform.claude.com/cookbook/tool-use-context-engineering-context-engineering-tools 2 3 4 5 6 7 8 9 10

  4. Compaction — https://platform.claude.com/docs/en/build-with-claude/compaction 2 3

練習

01

以下の3つのシナリオのそれぞれについて、切り詰め、コンパクション、またはツール結果クリアリングのどれが最も適しているかを判断し、理由を説明してください(理由は「膨張の具体的な原因」または「詳細を失う余裕があるか」に基づいている必要があります — 単に「こちらの方が適しているようだ」ではありません)。

レベル1: 3つのシナリオに適したメカニズムを選ぶ
  1. コードレビューエージェントがgrep_codeを繰り返し呼び出してコードベースを検索し、各呼び出しで数十行のマッチを返します。ウィンドウ使用量は上限に近づいていますが、レビュータスクは完了していません。
  2. サポートエージェントがユーザーと60ターンにわたってチャットしています。ウィンドウは全体的に大きくなり、ツール呼び出しと長いユーザーとエージェントの対話の両方を保持しています — コンテンツはあちこちから来ています。
  3. メモ取りアシスタントエージェントは、毎日の新しいセッションの開始時に、ユーザーが先週言及した長期的な好み(「辛い食べ物は避ける」)を知る必要があります。このセッション自体は始まったばかりで、ウィンドウはまったくいっぱいではありません。
完了基準 · ローカルでチェック
02

以下の擬似コードは、ウィンドウ使用量が高すぎるときに履歴をクリアし、最新のメッセージだけを保持しようとします:

レベル2: バグにつながる切り詰めコードを診断する

ある時点でmessages配列がまさにこの順序であるとします: [..., { role: "assistant", content: [tool_use block] }, { role: "user", content: [tool_result block] }, ...]、そしてmaxKeepがこれら2つのメッセージを分割し、tool_resultメッセージだけを保持し、tool_useメッセージを切り取ることになります。

これがモデルに送信されたときにどのような問題を引き起こすかを説明し、それを回避する修正を提示してください(完全なコードは不要です — アイデアを明確にするだけで構いません)。

完了基準 · ローカルでチェック