Claude Media
Compactionをバックグラウンドで走らせる非同期の実装手順

Compactionをバックグラウンドで走らせる非同期の実装手順

要約リクエストを送った時点のメッセージ数を記録し、返ってきたら先頭のその件数だけを差し替える。バックグラウンドcompactionの手順、同時2リクエストの注意点、thinkingを有効に保つ条件を示します。

要約を待たずに会話を続け、届いたら先頭だけ差し替える

バックグラウンドcompaction(非同期compaction)は、要約リクエストを会話の裏で走らせる方式です。要約が届くまで、会話は要約前の完全な履歴のまま進みます。届いたら、要約リクエストに含めたメッセージだけを履歴の先頭から取り除き、返ってきたcompactionブロックを代わりに置きます。要約中に増えたターンは、ブロックの後ろにそのまま残ります。

通常のオンデマンドcompactionでは、呼び出し側が要約の完了を待ってから次のターンを送ります。その待ち時間を会話から外すのが、この方式の狙いです。代わりに、履歴の管理と2件同時のリクエストを自分のコードで持つことになります。

この記事では、差し替えの手順、公式サンプルの構造、数え間違いで起きる失敗、要約が返らないときの扱い、thinkingを有効に保つ3条件までを順に示します。

通常のループと何が変わるのか

オンデマンドcompactionは、会話をそのままcompactionパラメータ付きで送ると、返答を生成せずにcompactionブロックだけが返る仕組みです。ブロックは以後、要約した元のメッセージの代わりにmessagesの先頭へ置かれます。バックグラウンド版は、この「要約を依頼する」と「結果を使う」の間に会話が進む点だけが違います。

観点通常のループバックグラウンド版
要約リクエスト中の会話通常のループ要約の完了を待つバックグラウンド版完全な履歴のまま続ける
履歴の置換通常のループ返ったメッセージで履歴全体を置き換えるバックグラウンド版送った件数だけを先頭から置き換える
同時リクエスト通常のループ1件バックグラウンド版会話と要約の2件
要約が返らなかったとき通常のループ完全な履歴を保ち、後で再挑戦バックグラウンド版同じ(次の要約リクエストも投げられる)

「要約が返らなかったときの扱い」と「ブロックを受け取った後の続け方」は通常のループと変わりません。差分は、履歴のどこまでを差し替えるかという1点に集約されます。

差し替えの4手順

公式の手順は次のとおりです。

  1. 履歴をその時点のまま要約リクエストに送り、そのときのメッセージ数を記録する
  2. 要約が走っている間も、完全な履歴で会話を続ける。新しいターンは末尾へ追加するだけにして、既存の履歴は編集しない。この要約が差し替わるか失敗するまで、別の要約リクエストは始めない
  3. stop_reasonが"compaction"のレスポンスが届いたら、送ったメッセージの分だけを履歴の先頭から取り除き、返ってきたメッセージをその位置に置く。要約リクエスト後に追加したターンはすべてブロックの後ろに残る
  4. ブロックが届いた後の最初のリクエストでは、差し替え後の履歴を送る。要約の作成中に生まれたthinkingが有効なまま残るためである

たとえば、要約リクエストがメッセージ1〜5を含み、その間に会話がメッセージ6〜8まで進んだとします。差し替え後の履歴は「ブロック、メッセージ6〜8」です。要約は1〜5だけを覆うので、6〜8の内容は要約にありません。ブロックの後ろに原文のまま残す必要があるのは、そのためです。

Pythonで組む公式サンプルの構造

公式ドキュメントには、Python・TypeScript・C#・Go・Java・Rubyのサンプルがあります。同時に2件のリクエストを走らせる例のため、PHP版はありません。以下はPython版の骨格です。ベータヘッダーcompact-2026-09-04を、要約リクエストにも通常の会話リクエストにも付けています。

def swap_in(history, summary, sent):
    if summary.stop_reason == "compaction":
        # 要約リクエストが含んでいたメッセージだけを置き換える
        history[:sent] = [{"role": "assistant", "content": summary.content}]
 
history, pending, sent = [], None, 0
for turn, question in enumerate(QUESTIONS, start=1):
    # ターンの先頭で、届いていれば差し替える
    if pending is not None and pending.done():
        swap_in(history, pending.result(), sent)
        pending = None
 
    history.append({"role": "user", "content": question})
    response = client.beta.messages.create(
        model="claude-opus-5-5", max_tokens=8192, system=SYSTEM,
        betas=["compact-2026-09-04"], messages=history,
    )
    history.append({"role": "assistant", "content": response.content})
 
    tokens = response.usage.input_tokens + response.usage.output_tokens
    if tokens > COMPACT_AT_TOKENS and turn < len(QUESTIONS) and pending is None:
        sent = len(history)          # 送る時点の件数を記録
        pending = executor.submit(   # 履歴のコピーで要約を依頼
            client.beta.messages.create,
            model="claude-opus-5-5", max_tokens=4096, system=SYSTEM,
            betas=["compact-2026-09-04"], messages=history.copy(),
            compaction={"type": "summarize"},
        )

サンプルは、要約の依頼をThreadPoolExecutorに投げて次のターンへ進みます。ドキュメントは各行を次のように説明しています。

  • 起動条件: トークン数のしきい値に加えて、未完了の要約リクエストが無いことも確かめる
  • 依頼の出し方: 履歴の件数を記録し、履歴のコピーに対して要約を依頼し、待たずに次のターンへ進む
  • 結果の確認: 各ターンの先頭で完了を確かめ、完了していれば、そのターンのリクエストを送る前に差し替える
  • 差し替え: 履歴全体を置き換えず、要約リクエストが含んでいた分だけを先頭から置き換える
  • ループ終了時: 要約がまだ届いていなければ、待って差し替える。保存や会話の再開の前に差し替えを済ませ、届きかけの要約を取りこぼさないためである

サンプルのしきい値COMPACT_AT_TOKENSは2,500と低く、短い会話でもcompactionが起きるようにしてあります。ドキュメントにも「実際の入力予算に近い値にする」と明記されています。

件数の数え間違いで起きる失敗

差し替えのバグは、ほとんどがsentまわりに集まります。3つの型を押さえておくと、原因の切り分けが速くなります。

要約中に履歴を編集する。手順2は、既存の履歴を編集しないことを求めています。先頭側のメッセージを書き換えたり削ったりすると、sentが指す位置がずれ、差し替えが別のメッセージを消します。サンプルが末尾への追加だけで進む構造なのは、この前提のためです。

要約の前のメッセージを残したまま差し替える。要約済みのメッセージがブロックの前に残ると、APIは400compaction_block_misplacedを返します。対処はドキュメントどおり、それらを取り除いてブロックをmessagesの先頭にすることです。先頭からちょうどsent件を取り除く実装なら、この状況にはなりません。

要約が終わる前に、2件目を投げる。手順2は、進行中の要約が差し替わるか失敗するまで、次の要約リクエストを始めないことを求めています。サンプルはpending is Noneの条件でこれを守っています。サンプルが記録するsentは1つだけなので、2件を重ねる実装では件数の管理を別に用意する必要があります。

sentを記録するタイミングにも注意が要ります。サンプルは、アシスタントの返答を履歴に追加した後のlen(history)を記録します。要約リクエストに実際に含めた件数とsentが一致しないと、差し替えの境目がずれます。履歴のコピーを送る直前に、その長さを記録してください。

要約が返らなかったときは完全な履歴に戻る

stop_reasonが"compaction"以外なら、要約は作られていません。この場合は完全な履歴をそのまま保ちます。サンプルのswap_inは、このとき何もせず、pendingを空に戻します。その後は次の要約リクエストを始められます。

要約が作られない原因は、通常のオンデマンドcompactionと共通です。

stop_reason原因対処
"max_tokens"原因要約が途中で切れた対処max_tokensを大きくして再送
"model_context_window_exceeded"原因要約の指示を入れる余地がなかった対処instructionsを短くするか、メッセージを減らして再送
"tool_use"原因要約でなくツール呼び出しを返した対処ツールを呼ばないようinstructionsで伝えて再送
"refusal"原因リクエストが拒否された対処要約なしで続ける
"end_turn"原因テキストが返らなかった対処要約なしで続ける

要約が作られなくても、レスポンスは200でcontentが空になります。呼び出しには課金され、usage.iterationsに記録されます。ブロックを探す前に、必ずstop_reasonを確かめてください。

サンプルは、要約リクエストが例外で終わる場合を扱っていません。Pythonではpending.result()が例外をそのまま投げます。実運用では、差し替えの呼び出しをtryで囲み、失敗時はpendingを空に戻して完全な履歴を保つ形が考えられます。529のcompaction_unavailableは一時的な障害で、ドキュメントは再試行を求めています。

thinkingを有効に保つために揃える3条件

要約中に届いたターンは「残すターン」になります。preserved thinkingを持つモデルでthinkingブロックを送り返している場合、残したターンのthinkingが有効でいられるのは、次の3条件が保たれる間だけです。

  1. 要約リクエストがpreserved thinkingを持つモデルで走ること。これは最新の要約だけでなく、thinkingが作られて以降のすべての要約リクエストに当てはまります。会話に使っているモデルへ、すべての要約リクエストを送るのが一つの満たし方です
  2. 残すターンが要約したメッセージの直後に続き、変更せずに送ること。最後に要約したメッセージと最初に残すメッセージの間に、メッセージを挟まず、飛ばしません。最初に残すメッセージのロールは、最後に要約したメッセージと異なる必要があり、途中に挟むrole: "system"メッセージも不可です。守らないと、APIが最初に残すメッセージを最後に要約したメッセージへ結合します。すでに送ったリクエストのmessagesをそのまま要約すれば、これは自然に満たされます
  3. systemと、defer_loading: trueでないtoolsが変わらないこと。要約リクエストと、残すthinkingを作ったリクエストで同じにし、以後のリクエストでも変えません

条件を外れても、要約の時点では何も失敗しません。ブロックは以後のリクエストでも受け付けられます。失敗が出るのは、残したthinkingを検査するリクエストを初めて送ったときです。既定では400エラーで、thinking.block_binding.prefix_mismatch_behaviorを"drop_block"にしていればthinkingブロックが落とされます。Message Batches APIでは、この項目を指定しない項目は失敗せず、検査が既定で効く箇所ではブロックが落とされます。

バックグラウンド版でこの条件が効く理由は明快です。差し替えでブロックの後ろに残るのは、要約が走っている間に完全な履歴のうえで生まれたターンだからです。手順4が、差し替え後の履歴を最初のリクエストで送るよう求めるのも、この検査を通すためです。要約の作成中にsystemやtoolsを変える設計にした場合は、3つ目の条件を破りやすくなります。

起動のタイミングとレート制限

要約リクエストは、通常のリクエストと同じようにレート制限を消費します。要約が走っている間、アプリケーションは2件のリクエストを同時に開いています。

会話は差し替えまで完全な履歴のまま伸び続けるため、起動はコンテキストウィンドウに余裕があるうちに始めます。余裕が乏しい段階で始めると、要約を待つ間に届くターンを収める場所が足りなくなります。サンプルのしきい値が「実際の入力予算に近い値」とされていても、要約中の増分まで含めて決める必要があります。しきい値の決め方は、Compactionの発動しきい値の決め方で詳しく扱っています。

しきい値の判定に使うトークン数は、直前のレスポンスのinput_tokensとoutput_tokensの合計です。プロンプトキャッシュを使う場合、input_tokensは最後のキャッシュポイント以降の分しか数えません。cache_read_input_tokensとcache_creation_input_tokensも足す必要があります。

使える環境と、併用できない機能

この機能はベータで、提供面によって扱いが分かれます。

  • ベータヘッダー: compact-2026-09-04を、要約を依頼するリクエストと、ブロックを載せるすべての後続リクエストに付ける
  • 対応モデル: Claude Fable 5.1・Mythos 5.1・Fable 5・Mythos 5・Mythos preview・Opus 5.5・Opus 5・Opus 4.8・Opus 4.7・Opus 4.6・Sonnet 5.5・Sonnet 5・Sonnet 4.6
  • 提供面: Claude API・Claude Platform on AWS・Google Cloud・Microsoft Foundryでベータ。Amazon Bedrockでは利用できない
  • 併用不可: compactionとcontext_managementは同じリクエストに載せられない。しきい値型のcompaction(compact_20260112)は、署名付きブロックを載せたリクエストでは動かない
  • task_budget: output_config.task_budget.remainingをcompaction付き、またはブロックを載せたリクエストに送ると400になる。対処はtask_budgetのremainingでcompaction後の予算を引き継ぐ方法を参照
  • 失われる内容: 要約対象の画像・ドキュメント・container_uploadブロック・取得したURLは、ブロックに置き換わると消える。後のターンで要るものは再度渡す

使い分けの判断軸

バックグラウンド版を選ぶ理由は、要約の待ち時間を利用者に見せたくない場面に限られます。ループの単純さと引き換えに、履歴の管理と同時リクエストのレート消費を自分で引き受けるからです。

状況向く方式
要約の待ち時間が許される、または履歴管理を簡素にしたい向く方式通常のオンデマンドループ
待ち時間を会話から外したい向く方式バックグラウンド版
発動の判断をAPI側に任せたい向く方式しきい値型(compact_20260112)

ブロックの積み戻しの基本形はCompaction blocksを会話に戻す実装パターンにあります。ただし同記事が扱うのは、しきい値型のcompactionです。この記事で扱ったオンデマンド型はパラメータもベータヘッダーも違うため、両方を同じリクエストに混ぜません。

まとめ

バックグラウンドcompactionは、待ち時間を消す代わりに、差し替えの正確さを呼び出し側に預ける方式です。守る点は3つに絞れます。

  • 要約リクエストを送る時点の件数sentを記録し、先頭のその件数だけを差し替える
  • 進行中の要約が済むまで、履歴を編集せず、次の要約リクエストも始めない
  • モデル・system・toolsを揃え、差し替え後の履歴を最初のリクエストで送る

sentの数え間違いは、要約が正常に返っても会話を壊します。差し替えの関数は、件数を引数で受け取る小さな形にしておくと、単体テストで境界を確かめられます。

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