Claude Media
Compaction blocksを会話に戻す実装パターン

Compaction blocksを会話に戻す実装パターン

Claude APIのcompactionが返すcompactionブロックは、次のリクエストへ積み戻さないと会話が続きません。基本の実装手順とストリーミング時の扱いまで示します。

compactionブロックを渡さないと会話が壊れる

Claude APIのcompactionは、会話が長くなると自動で要約を作りcompactionブロックとしてレスポンスに含めます。ここで次のリクエストにこのブロックを積み戻す実装を忘れると、要約された会話の続きが成立しません。API自体は要約を作るところまでしかやらず、その要約を次のmessages配列に載せて送り返すのは呼び出し側の責任です。積み戻しの実装は単純ですが、複数回のcompaction・ストリーミング・thinkingブロックの扱いなど、細部でつまずきやすい箇所がいくつかあります。

前提条件は2つです。リクエストにanthropic-beta: compact-2026-01-12ヘッダーを付けること、context_management.editscompact_20260112を指定することです。しきい値そのものの選び方は本記事末尾で別記事に譲り、ここでは返ってきたブロックをどう次のリクエストへ運ぶかだけを扱います。

対応モデルはClaude Fable 5.1・Mythos 5.1・Fable 5・Mythos 5・Mythos preview・Opus 5・Opus 4.8・4.7・4.6・Sonnet 5・Sonnet 4.6です。提供面はClaude API・AWS上のClaude Platform・Amazon Bedrock・Google Cloud・Microsoft Foundryのそれぞれでベータ提供されています。いずれもベータのため、本番導入前に対象モデル・対象面での動作を個別に確認しておく必要があります。

compactionが起きると何が返ってくるか

しきい値を超えると、APIは内部で4段階の処理をします。入力トークンがしきい値に達したことを検知し、その時点までの会話の要約を生成し、要約をcompactionブロックとして組み立て、そのまま同じレスポンスの中で本来の応答テキストの生成を続けます。呼び出し側から見えるのは、レスポンスのcontent配列の先頭にcompactionブロックが入り、その後ろに通常のテキスト応答が続く、という結果だけです。

{
  "content": [
    { "type": "compaction", "content": "Summary of the conversation: ..." },
    { "type": "text", "text": "Based on our conversation so far..." }
  ]
}

長い会話では複数回compactionが起きることもあります。その場合は最後のcompactionブロックだけが有効で、それより前の内容はすべて要約に置き換わったものとして扱われます。

レスポンス全体をそのままassistantターンとして積み戻す

もっとも単純な実装は、レスポンスのcontentをまるごと次のリクエストのmessagesassistantターンとして追加することです。

client = anthropic.Anthropic()
messages = [{"role": "user", "content": "Hello, Claude"}]
 
response = client.beta.messages.create(
    betas=["compact-2026-01-12"],
    model="claude-opus-5",
    max_tokens=4096,
    messages=messages,
    context_management={"edits": [{"type": "compact_20260112"}]},
)
 
# compactionブロックを含むレスポンスをそのまま積み戻す
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Now add error handling"})
 
response = client.beta.messages.create(
    betas=["compact-2026-01-12"],
    model="claude-opus-5",
    max_tokens=4096,
    messages=messages,
    context_management={"edits": [{"type": "compact_20260112"}]},
)

compactionブロックを個別に取り出して加工する必要はありません。response.contentをそのままassistantメッセージの中身にするだけで、API側が要約より前のコンテンツブロックを無視してくれます。CLIやTypeScript版でも考え方は同じで、レスポンスのcontentをそのまま次のメッセージ配列へ追加する点は言語やクライアントが変わっても変わりません。

ここで説明しているのはpause_after_compactionを指定しない既定の動作です。既定では、要約ブロックとそれに続く本来の応答テキストが同じレスポンスの中に両方含まれます。要約が終わった直後で処理をいったん止め、追加のメッセージを差し込んでから応答を継続させたい場合は、pause_after_compactionを有効にする別の設計が必要になります。その場合レスポンスのcontentにはcompactionブロックだけが入り、stop_reasoncompactionになります。

複数回compactionが起きても最後のブロックだけが有効になる

APIはcompactionブロックを受け取ると、それより前の全コンテンツブロックを無視します。これは実装側に選択肢を与えます。

  • 元のメッセージ履歴をそのままmessagesに残し、無視される処理をAPI側に任せる
  • 圧縮済みのメッセージを自分で間引き、compactionブロック以降だけを持つ

どちらでも動作は同じですが、後者はクライアント側のメモリ・ペイロードサイズを抑えられます。頻繁に長時間セッションを扱うなら、間引く実装のほうが送信データ量の面で有利です。

積み戻すたびに追加コストがかかるのではないかという不安は不要です。一度作られたcompactionブロックを再度リクエストへ含めるだけなら、新しい要約ステップは走らず、追加のcompactionコストは発生しません。レスポンスのusage.iterations配列には、実際にcompactionが発生したリクエストでだけtype: "compaction"のエントリが追加され、それ以外のリクエストではmessageイテレーションだけが並びます。web検索のようなサーバーツールを使っている場合は、1回のリクエストの中でしきい値判定がサンプリングの反復ごとに走るため、出力量によっては1リクエスト内で複数回compactionが起きることもあります。

ストリーミングではcompaction_deltaを1回で受け取る

stream: trueでリクエストすると、compactionブロックは通常のテキストブロックとは違うイベント順で届きます。content_block_startイベントの後、要約全文を含むcontent_block_deltaが1回だけ届き(逐次的に少しずつ届くのではなく、要約全体が一度に来ます)、最後にcontent_block_stopイベントで終わります。

with client.beta.messages.stream(
    betas=["compact-2026-01-12"],
    model="claude-opus-5",
    max_tokens=4096,
    messages=messages,
    context_management={"edits": [{"type": "compact_20260112"}]},
) as stream:
    for event in stream:
        if event.type == "content_block_start" and event.content_block.type == "compaction":
            print("Compaction started...")
        elif event.type == "content_block_delta" and event.delta.type == "compaction_delta":
            print(f"Compaction complete: {len(event.delta.content or '')} chars")
 
    message = stream.get_final_message()
    messages.append({"role": "assistant", "content": message.content})

逐次的に届くテキストブロックと同じ扱いでバッファリングしようとすると、要約が1チャンクで丸ごと来る前提を持たないコードで不具合の原因になります。 stream.get_final_message()のような組み立て済みのメッセージを使えば、この違いを意識せずに積み戻せます。

プロンプトキャッシュはcompactionブロックにcache_controlを置く

compactionが起きると、要約というまとまった新しいコンテンツがキャッシュに書き込まれます。compactionブロックにcache_controlのブレークポイントを追加すると、この要約部分をキャッシュできます。

{
  "type": "compaction",
  "content": "[summary text]",
  "cache_control": { "type": "ephemeral" }
}

system promptの末尾にも別途ブレークポイントを置いておくと、compaction発生時にsystem promptのキャッシュは無効化されず、要約部分だけが新規キャッシュ書き込みの対象になります。ブレークポイントを分けずにsystem promptとcompactionブロックを同じキャッシュ範囲にまとめてしまうと、要約が生成されるたびにsystem prompt側もキャッシュミスとして再書き込みされ、削減できたはずのコストが戻ってきてしまいます。キャッシュ設計の細部はCompactionとプロンプトキャッシュを両立させるcache_controlの置き方で扱っています。

よくあるつまずき — Claude Fable 5.1でthinkingブロックを持ち越すと400エラーになる

Claude Fable 5.1では、compactionブロックより前のassistantターンにあったthinkingredacted_thinkingブロックを、そのまま積み戻すエラーが起きやすい点に注意します。これらのブロックは会話全体が見えていた時点で生成されたものなので、compaction後の会話チェックに通らず、400エラーで拒否されます。

対処は2つあります。compactionブロック以降に再挿入するassistantターンからthinkingredacted_thinkingブロックを取り除くか、thinking.block_binding.prefix_mismatch_behavior: "drop_block"をベータヘッダー付きで送ることです。それ以前のターンを再挿入せず、APIにすべて要約させる実装であれば、このエラー自体を回避できます。

よくあるつまずき — ツール定義があるとcontent: nullで返ることがある

toolsを指定したリクエストでcompactionが発動すると、内部の要約ステップでモデルが要約文を書く代わりにツールを呼び出そうとすることがあります。このとき返ってくるcompactionブロックはcontent: nullになり、積み戻しても要約内容が空のまま次のリクエストに渡ってしまいます。

対処は、instructionsパラメータに「要約中はツールを呼ばず、テキストだけで応答する」よう明示する指示を渡すことです。積み戻し処理側でも、compaction.contentnullでないことを確認してから次のリクエストへ進める防御的なチェックを入れておくと、空の要約が気づかれないまま会話に残る事故を防げます。この確認は、複数回compactionが起きる長時間セッションほど重要になります。

しきい値調整・一時停止・カスタム要約プロンプトは別の実装判断

本記事で扱ったのは、返ってきたcompactionブロックを次のリクエストへ積み戻す基本形です。ここから先の実装判断は、目的ごとに専門記事に分かれています。しきい値をどこに置くかはCompactionの発動しきい値の決め方、要約が終わった直後に処理を止めて人間承認を挟みたい場合はpause_after_compactionを扱うCompactionの一時停止で合計トークン予算を強制する実装パターン、デフォルトの要約プロンプトを差し替えたい場合はCompactionのカスタム要約プロンプトを書く実装ガイドがそれぞれ扱っています。

本記事で説明した積み戻しの基本形は、これらどの応用パターンでも共通の土台になります。しきい値をどう調整しても、要約プロンプトをどうカスタマイズしても、最終的に返ってくるcompactionブロックを次のリクエストへ運ぶ手順自体は変わりません。

まとめ

compactionブロックは、レスポンスのcontentをそのまま次のassistantターンとして積み戻すだけで会話を継続できます。複数回のcompactionでは最後のブロックだけが有効になり、ストリーミングではcompaction_deltaが1回で届く点、Claude Fable 5.1ではthinkingブロックの持ち越しが400エラーになりうる点、tools指定時にcontent: nullが返ることがある点の3つが実装上の主なつまずきどころです。積み戻し自体に追加コストはかからないため、迷ったらレスポンス全体をそのまま次のリクエストへ運ぶ実装から始めるのが安全です。

この記事を共有:XはてブLinkedIn