Compactionをバックグラウンドで走らせる非同期の実装手順
要約リクエストを送った時点のメッセージ数を記録し、返ってきたら先頭のその件数だけを差し替える。バックグラウンドcompactionの手順、同時2リクエストの注意点、thinkingを有効に保つ条件を示します。
要約を待たずに会話を続け、届いたら先頭だけ差し替える
バックグラウンドcompaction(非同期compaction)は、要約リクエストを会話の裏で走らせる方式です。要約が届くまで、会話は要約前の完全な履歴のまま進みます。届いたら、要約リクエストに含めたメッセージだけを履歴の先頭から取り除き、返ってきたcompactionブロックを代わりに置きます。要約中に増えたターンは、ブロックの後ろにそのまま残ります。
通常のオンデマンドcompactionでは、呼び出し側が要約の完了を待ってから次のターンを送ります。その待ち時間を会話から外すのが、この方式の狙いです。代わりに、履歴の管理と2件同時のリクエストを自分のコードで持つことになります。
この記事では、差し替えの手順、公式サンプルの構造、数え間違いで起きる失敗、要約が返らないときの扱い、thinkingを有効に保つ3条件までを順に示します。
通常のループと何が変わるのか
オンデマンドcompactionは、会話をそのままcompactionパラメータ付きで送ると、返答を生成せずにcompactionブロックだけが返る仕組みです。ブロックは以後、要約した元のメッセージの代わりにmessagesの先頭へ置かれます。バックグラウンド版は、この「要約を依頼する」と「結果を使う」の間に会話が進む点だけが違います。
| 観点 | 通常のループ | バックグラウンド版 |
|---|---|---|
| 要約リクエスト中の会話 | 通常のループ要約の完了を待つ | バックグラウンド版完全な履歴のまま続ける |
| 履歴の置換 | 通常のループ返ったメッセージで履歴全体を置き換える | バックグラウンド版送った件数だけを先頭から置き換える |
| 同時リクエスト | 通常のループ1件 | バックグラウンド版会話と要約の2件 |
| 要約が返らなかったとき | 通常のループ完全な履歴を保ち、後で再挑戦 | バックグラウンド版同じ(次の要約リクエストも投げられる) |
「要約が返らなかったときの扱い」と「ブロックを受け取った後の続け方」は通常のループと変わりません。差分は、履歴のどこまでを差し替えるかという1点に集約されます。
差し替えの4手順
公式の手順は次のとおりです。
- 履歴をその時点のまま要約リクエストに送り、そのときのメッセージ数を記録する
- 要約が走っている間も、完全な履歴で会話を続ける。新しいターンは末尾へ追加するだけにして、既存の履歴は編集しない。この要約が差し替わるか失敗するまで、別の要約リクエストは始めない
stop_reasonが"compaction"のレスポンスが届いたら、送ったメッセージの分だけを履歴の先頭から取り除き、返ってきたメッセージをその位置に置く。要約リクエスト後に追加したターンはすべてブロックの後ろに残る- ブロックが届いた後の最初のリクエストでは、差し替え後の履歴を送る。要約の作成中に生まれた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条件が保たれる間だけです。
- 要約リクエストがpreserved thinkingを持つモデルで走ること。これは最新の要約だけでなく、thinkingが作られて以降のすべての要約リクエストに当てはまります。会話に使っているモデルへ、すべての要約リクエストを送るのが一つの満たし方です
- 残すターンが要約したメッセージの直後に続き、変更せずに送ること。最後に要約したメッセージと最初に残すメッセージの間に、メッセージを挟まず、飛ばしません。最初に残すメッセージのロールは、最後に要約したメッセージと異なる必要があり、途中に挟む
role: "system"メッセージも不可です。守らないと、APIが最初に残すメッセージを最後に要約したメッセージへ結合します。すでに送ったリクエストのmessagesをそのまま要約すれば、これは自然に満たされます 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の数え間違いは、要約が正常に返っても会話を壊します。差し替えの関数は、件数を引数で受け取る小さな形にしておくと、単体テストで境界を確かめられます。