Compactionで直近ターンを残す — 切れ目の選び方とtool_useの分断
要約対象から末尾ターンを外し、要約ブロックの後ろに原文のまま送るkeep-tail方式の手順。切れ目の位置、tool callを分けたときの400エラー、thinkingの扱いをまとめます。
Compactionで直近ターンを残す — 切れ目の選び方とtool_useの分断
オンデマンドcompactionは、会話全体を1つの要約ブロックに置き換えます。直近のやり取りまで要約に溶かしたくないときは、末尾の数ターンを外して要約ブロックの後ろへ原文のまま送ります。これがkeep-tail方式です。
決めることは1つだけです。履歴のどこで切るか。切れ目を選ぶパラメータはAPIに無く、自分のコードで決めます。ここを誤ると、tool callが宙に浮いてAPIが要求を拒否します。
keep-tailは何を変えて何を変えないか
keep-tailが変えるのは2点です。compactionリクエストに入れるメッセージと、要約ブロックの後ろに送るメッセージ。ブロックを返して履歴を差し替える流れは、通常のオンデマンドcompactionと同じです。
前提は次のとおりです。
- ベータ機能で、ベータヘッダー
compact-2026-09-04が要る - 対応プラットフォームはベータのClaude API、Claude Platform on AWS、Google Cloud、Microsoft Foundry。Amazon Bedrockは対象外
- 対応モデルにOpus 5.5 / Sonnet 5.5 / Fable 5.1などが含まれる
しきい値方式でブロックを会話へ戻す実装はCompaction blocksを会話に戻す実装パターンで扱っています。ここでは、切れ目の設計に絞ります。
切れ目の選び方 — どこに切ればよいか
切れ目より前のメッセージがcompactionリクエストに入り、切れ目以降が「残すターン」になります。APIは送られたメッセージをすべて要約するので、残したい分を要約リクエストに混ぜてはいけません。
公式の例では、1ターンをuserメッセージ1件とその返答1件と数えています。2ターン残すなら、履歴を末尾から4メッセージの位置で割ります。残す側は必ずuserメッセージから始まります。
KEEP_TURNS = 2
split = -2 * KEEP_TURNS
older, recent = history[:split], history[split:]この方法が成り立つのは、ツールを使わない単純な往復だからです。ツールを呼ぶ会話では1ターンが何メッセージにもなるため、末尾から固定数で割ると切れ目がtool callの途中に落ちます。
切れ目の判定は1つの規則にまとまる
公式が示す規則は1つです。tool callを開いたままにせず、tool callとその結果を同じ側に置きます。
| 切れ目の位置 | 判定 |
|---|---|
| assistantの返答の直後、次のuserメッセージの前 | 判定置ける |
| tool callを含むassistantメッセージと、その結果のuserメッセージの間 | 判定置けない |
| 要約側の末尾が、結果の付いていないtool callで終わる | 判定APIが拒否する |
三行目が、公式に明記された失敗です。compactionリクエストのメッセージがassistantターンで終わり、そのtool callに結果がまだ無いと、APIはリクエストを拒否します。
ツール呼び出しを含む履歴での切れ目の探し方
ここからは公式の記述を前提にした、記事側の実装例です。tool_resultはuserロールで返るため、「userメッセージなら切れ目にできる」とは言えません。人間の入力(テキスト)が先頭に来るuserメッセージだけを切れ目の候補にします。
def is_turn_start(msg) -> bool:
"""人間の入力で始まる user メッセージか(tool_result だけの
user メッセージは、直前の tool_use と対になるので除く)。"""
if msg["role"] != "user":
return False
content = msg["content"]
if isinstance(content, str):
return True
return any(block.get("type") != "tool_result" for block in content)
def find_split(history, keep_turns: int):
starts = [i for i, m in enumerate(history) if is_turn_start(m)]
if len(starts) <= keep_turns:
return None # 要約する側が空になるので compaction しない
return starts[-keep_turns]この実装では、残す側が必ず人間の入力から始まり、その直前の要約側はassistantの返答で終わります。tool_useとtool_resultの組は、途中で分かれません。
None を返す分岐は、公式サンプルの条件(会話のターン数が残すターン数を超えること)と同じ意図です。olderが空のままcompactionリクエストを送ると、送る意味がありません。
要約側と残す側を別々に送る
切れ目が決まったら、olderだけをcompactionリクエストに入れます。残す側は要約リクエストに含めません。
older, recent = history[:split], history[split:]
summary = client.beta.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
system=SYSTEM,
betas=["compact-2026-09-04"],
messages=older,
compaction={"type": "summarize"},
)
if summary.stop_reason == "compaction":
history = [{"role": "assistant", "content": summary.content}, *recent]stop_reason の確認は省けません。要約が作られるのは、要約の呼び出しがテキストで正常終了し、tool callを含まないときだけです。それ以外は200のまま content が空で返ります。
次のリクエストは、返ってきたブロック、残したターン、新しいuserメッセージの順に並べます。残したターンは履歴にあるとおりに送り、thinkingブロックも含めます。編集や再構成はしません。
要約を書くのは要約用のモデル(summarizer)です。ツール定義は読みますが、ツールは実行しません。要約の観点を変えたいときはCompactionの要約指示をカスタマイズする方法が参考になります。
残した分だけ空きは減る
残したターンは原文の長さのままClaudeに戻ります。多く残すほど、compactionで空く枠は小さくなります。
たとえば、ツールの出力が長い会話で3ターン残すと、要約で削ったつもりの分を末尾が食い返します。残すターン数は、次の2つを見て決めます。
- 直近のやり取りを原文で持たせる価値(直前の指示、書きかけのコード、途中の判断)
- 要約後に確保したい空き
要約を走らせる閾値の決め方はCompactionのトークン閾値の設計で扱っています。閾値と残すターン数は組で調整する値です。残す側が大きければ、閾値を下げても要約後の空きは狭いままです。
分断が起きたとき何が返るか
compactionリクエストの側は明確です。末尾のassistantターンが結果なしのtool callで終わっていれば、リクエストは400で拒否されます。対処は、そのターンのtool_resultを先に送ることです。
残す側の先頭がtool_resultだけのuserメッセージで、対応するtool_useが要約側に入ってしまう場合について、keep-tailのページは個別のエラーを説明していません。公式の指示は「tool callと結果を同じ側に置く」であり、この記事の切れ目探しも同じ方針で書いています。tool_useとtool_resultの整合性の一般規則は、tool_resultとtool_useの400エラー — 正しい整形ルールにまとまっています。
thinkingを使う会話で切れ目に加わる制約
preserved thinkingに対応するモデルでthinkingブロックを送り返している場合、残したターンのthinkingが有効であり続けるには条件があります。切れ目に関わるのは次の点です。
- 残すターンが、要約したメッセージの直後に続き、変更なしで送られること
- 残す先頭のメッセージのロールが、要約側の最後のメッセージと異なること
- 途中の
role: "system"メッセージで始まらないこと systemと、defer_loading: trueでないtoolsが変わらないこと- compactionリクエストが、preserved thinkingに対応するモデルで実行されること
守れていなくても、compaction自体は失敗しません。失敗は、そのthinkingを送る後続のリクエストで、既定では400エラーとして現れます。thinking.block_binding.prefix_mismatch_behavior を "drop_block" にすると、エラーの代わりにthinkingブロックが落とされます。
公式は、すでに送ったリクエストの messages をそのままcompactionし、その後に増えた分を残す方法を挙げています。切れ目が「返答の直後、次のuserメッセージの前」になるので、上の条件に当てはまります。
確認は、ベータヘッダー thinking-binding-controls-2026-08-01 を足し、prefix_mismatch_behavior を "error" にして次のリクエストを送ります。200で input_transformations が空なら、残したthinkingは有効でした。
system や tools を変えたい場合は、先に会話全体をcompactionして、残すターンをなくします。その次のリクエストで変更します。
もう一度compactionするときの扱い
keep-tailを使った会話も、再度compactionできます。新しいブロックは、古い要約と、compactionリクエストに入れたそれ以降のメッセージをまとめて覆います。そのリクエストから外したターンが、新しいブロックにとっての「残すターン」です。
前回残したターンを要約に溶かすか、さらに残すかは、そのつど切れ目で決まります。thinkingを残したまま2回compactionする場合は、両方のcompactionリクエストがpreserved thinkingに対応するモデルで走っている必要があります。
要約と一緒に消えるもの
残さなかった側にあるものは、ブロックへの置き換えで失われます。
- 画像、ドキュメント、
container_uploadブロック、取得したURLは、要約側に含まれていると消える - 要約側の
role: "system"メッセージの指示は、テキストとしては効かなくなる。まだ必要なら、次の新しいuserターンの直後にrole: "system"で再掲する
残す側の1ターン前後に画像や指示が固まっているなら、切れ目をそこに合わせる手があります。要約の質に頼るより確実です。
keep-tailを使う判断の目安
| 状況 | 残す量 |
|---|---|
| 直前の指示の言い回しが、次の作業に効く | 残す量1〜2ターン |
| ツール出力が長く、原文を残すと空きが足りない | 残す量0〜1ターン |
| thinkingを送り返しており、system / toolsを変える予定がある | 残す量0ターン(全体を要約してから変更) |
残す量に固定の正解は無く、上の表は考え方の整理です。実際の値は、自分の会話のターン当たりのトークン数を測って決めます。プロンプトキャッシュとの併用はCompactionとPrompt cachingの併用を参照してください。
まとめ
keep-tailは、切れ目を自分のコードで決める方式です。tool callを使う会話では、人間の入力で始まるuserメッセージを切れ目にすれば、tool_useとtool_resultが分かれません。直前の指示や書きかけの作業を原文で持たせたいエージェントで効き、ツール出力が長い会話では残す量を0〜1ターンに絞る選択肢があります。