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

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

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

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

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

積み戻しの実装は単純ですが、複数回のcompaction・ストリーミング・thinkingブロックの扱いなど、細部でつまずきやすい箇所がいくつかあります。本記事が扱うのは、context_management.editsにcompact_20260112を指定する「しきい値方式」(入力トークンがしきい値に達するとAPIが要約する方式)です。必要なのはanthropic-beta: compact-2026-01-12ヘッダーと、この指定の2つです。

対応モデルはClaude Fable 5.1・Mythos 5.1・Fable 5・Mythos 5・Mythos preview・Opus 5.5・Opus 5・Opus 4.8・4.7・4.6・Sonnet 5.5・Sonnet 5・Sonnet 4.6です。提供面はClaude API・AWS上のClaude Platform・Amazon Bedrock・Google Cloud・Microsoft Foundryで、いずれもベータです。

自分の実装はどちらの方式か

Anthropicのドキュメントには、しきい値方式のほかに、要約を依頼するタイミングを呼び出し側が決める「オンデマンド方式」もあります。方式によって、返ってきたブロックの戻し方が違います。

くらべる

積み戻し方の違い

本記事

しきい値方式

APIが入力トークン数を見て要約します。返ってきたレスポンスを通常どおり末尾に追記すれば、APIがcompactionブロックより前の内容を捨てます。要約はしきい値に達したリクエストの中で走り、終わるまで応答を待ちます。直近のターンを原文で残すには、要約直後に一時停止してから再挿入します。

別記事

オンデマンド方式

呼び出し側がリクエストを送って要約をもらいます。返ってきたブロックは、要約された元のメッセージの代わりにmessagesの先頭へ置きます。ベータヘッダーはcompact-2026-09-04で、トップレベルのcompactionパラメータを使います。バックグラウンド実行や、直近ターンとそのthinkingを残す運用に向きます。

オンデマンド方式の実装はClaude APIのオンデマンドcompactionにあります。しきい値の選び方は本記事末尾で別記事に譲り、ここでは返ってきたブロックをどう次のリクエストへ運ぶかだけを扱います。

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

しきい値を超えると、APIは同じリクエストの中で次の順に処理します。

  1. 1

    しきい値の到達を検知する

    入力トークンが指定したトリガーに達したことを見つけます。

  2. 2

    会話の要約を生成する

    その時点までの会話をAPI側で要約します。

  3. 3

    compactionブロックを組み立てる

    要約をcompactionブロックに入れます。

  4. 4

    応答の続きを生成する

    要約した文脈で、本来の応答テキストを書き続けます。

呼び出し側から見えるのは、content配列の先頭にcompactionブロックが入り、その後ろに通常のテキスト応答が続く結果だけです。

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

長い会話では複数回compactionが起きることもあります。その場合は最後のcompactionブロックだけが有効で、それより前の内容はすべて要約に置き換わったものとして扱われます。直近のターンだけ原文で残したいときは、Compactionで直近ターンを残す方法が参考になります。

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

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

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側が要約より前のコンテンツブロックを無視してくれます。TypeScript版でも考え方は同じで、レスポンスのcontentをそのまま次のメッセージ配列へ追加します。

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

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

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

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

どちらでも動作は同じです。後者は送るメッセージが減るぶん、リクエストのペイロードが小さくなります。

積み戻すたびに追加コストがかかるのではないかという不安は不要です。一度作られたcompactionブロックを再度リクエストへ含めるだけなら、新しい要約ステップは走らず、追加のcompactionコストは発生しません。

一方で、要約を作ったリクエストの課金には注意が要ります。トップレベルのusage.input_tokensとoutput_tokensには、compactionの反復分が入りません。課金対象の合計を出すには、usage.iterationsの全エントリを足します。

{
  "usage": {
    "input_tokens": 23000,
    "output_tokens": 1000,
    "iterations": [
      { "type": "compaction", "input_tokens": 180000, "output_tokens": 3500 },
      { "type": "message", "input_tokens": 23000, "output_tokens": 1000 }
    ]
  }
}

この例の入力側は、トップレベルの23,000ではなく、合計203,000トークン(180,000 + 23,000)として数えます。compactionのベータを有効にすると、要約が起きなかったレスポンスにもusage.iterationsが付きます。type: "compaction"のエントリが加わるのは、そのリクエストで新しく要約が走ったときだけです。

トークン数を事前に数えるエンドポイント/v1/messages/count_tokensは、プロンプトに含まれる既存のcompactionブロックを反映しますが、新しい要約は起動しません。積み戻した後の実効トークン数を確かめるのに使えます。

web検索のようなサーバーツールを使うと、しきい値の判定がサンプリングの反復ごとの先頭で走ります。出力量によっては、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の置き方で扱っています。

よくあるつまずき — Fable 5.1・Opus 5.5・Sonnet 5.5でthinkingを持ち越すと400エラーになる

Claude Fable 5.1・Opus 5.5・Sonnet 5.5では、compactionブロックより後に再挿入するassistantターンにthinking・redacted_thinkingブロックが残っていると、リクエストが400エラーで拒否されることがあります。これらは会話全体が見えていた時点で生成されたブロックで、compaction後の会話チェックに通らないためです。チェックが働く環境でのエラーなので、すべての環境で必ず起きるわけではありません。

対処は2つあります。再挿入するターンからthinking・redacted_thinkingブロックを取り除くか、thinking.block_binding.prefix_mismatch_behavior: "drop_block"をthinking-binding-controls-2026-08-01ベータヘッダー付きで送ります。テキストとツールのブロックはそのまま残せます。

Sonnet 5.5のblock_bindingはthinking: {"type": "adaptive"}のときだけ働きます。between_toolsを使っているなら、ブロックを取り除く方を選びます。それ以前のターンを再挿入せず、APIにすべて要約させる実装なら、このエラー自体が起きません。

また、Fable 5.1・Mythos 5.1・Opus 5.5・Sonnet 5.5では、compactionブロックより前のthinkingは次の文脈に引き継がれません。以前の作業について、モデルが持つのは要約だけです。独自のinstructionsを書くなら、要約に残すべき内容を明示します。直近のターンを残しつつthinkingも有効に保つ条件は、Compactionで残したthinking blockが無効になる条件と検証手順にあります。

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

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

対処は、instructionsパラメータに「要約中はツールを呼ばず、テキストだけで応答する」よう明示する指示を渡すことです。ドキュメントの例は、Summarize the transcript inside <summary></summary> tags.で始め、末尾をDo not call any tools while writing this summary; respond with text only.で締めています。積み戻し処理側でも、compaction.contentがnullでないことを確認してから次のリクエストへ進める防御的なチェックを入れておくと、空の要約が気づかれないまま会話に残る事故を防げます。

よくあるつまずき — Pythonでmodel_dump()すると400エラーになる

client.messagesを使い、レスポンスのブロックを自分で辞書に直列化して積み戻す実装には、もう1つ罠があります。単純なmodel_dump()はcompactionブロックにtext: nullとcitations: nullを足します。

この状態で送ると、APIはExtra inputs are not permittedという400エラーでリクエストを拒否します。to_dict()かmodel_dump(exclude_none=True)で直列化すれば避けられます。前のセクションのようにresponse.contentをそのまま渡す書き方なら、この問題は起きません。

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

本記事で扱ったのは、返ってきたcompactionブロックを次のリクエストへ積み戻す基本形です。ここから先の実装判断は、目的ごとに専門記事に分かれています。しきい値をどこに置くかはCompactionの発動しきい値の決め方、要約が終わった直後に処理を止めて人間承認を挟みたい場合はpause_after_compactionを扱うCompactionの一時停止で合計トークン予算を強制する実装パターン、デフォルトの要約プロンプトを差し替えたい場合はCompactionのカスタム要約プロンプトを書く実装ガイドがそれぞれ扱っています。要約の完了を待たずに会話を続けたい場合は、Compactionをバックグラウンドで走らせる非同期の実装手順が差し替えの手順を示しています。好きなタイミングで要約だけを返させる方式はClaude APIのオンデマンドcompactionにあります。

まとめ

しきい値方式のcompactionは、response.contentをそのままassistantターンに積み戻せば会話が続きます。注意点が増えるのは、ブロックを間引いたり自前で直列化したりする場合です。そのときはthinkingの持ち越しとnullの混入を確かめます。

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