Claude Media
Compactionで直近ターンを残す — 切れ目の選び方とtool_useの分断

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ターンに絞る選択肢があります。

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