長時間動かすエージェントを Claude API で組むと、必ず「コンテキストが溢れる」壁にぶつかる。ツールを何十回も呼ぶうちに、過去のツール実行結果が会話履歴に積み上がって入力トークンを食い尽くすやつだ。
この記事では、その対策として用意されている Context Editing(コンテキスト編集) を Python から実装する手順と、自分が実際に踏んだハマりどころをまとめる。
対象と前提
- 想定読者:Claude API でツール(function calling)を使ったエージェントを実装している人
- 前提知識:Messages API とツール定義の基本、
tool_use/tool_resultブロックの往復が分かること - 環境:Python 3.13 /
anthropicSDK 0.6x 系 / Anthropic Messages API
ベータ機能なので、ヘッダ名やストラテジの版数文字列(
-2025-XX-XXの部分)は更新されることがある。実装前に公式ドキュメントで現行値を確認すること。
TL;DR
-
context_managementパラメータで、古いtool_resultをサーバー側で自動的に間引ける - 入力トークンが閾値を超えたら発火 → 直近 N 回分のツール往復だけ残す、という設定を宣言的に書くだけ
- ただし プロンプトキャッシュとの相性が最悪。何も考えずに入れるとコストが下がるどころか上がる
実装
最小構成はこれだけ。ベータヘッダを付けて context_management を渡す。
import anthropic
client = anthropic.Anthropic()
CONTEXT_MGMT = {
"edits": [
{
"type": "clear_tool_uses_20250919",
# 入力が 3 万トークンを超えたら発火
"trigger": {"type": "input_tokens", "value": 30000},
# 直近 3 往復のツール結果は必ず残す
"keep": {"type": "tool_uses", "value": 3},
# 一度発火したら最低 5000 トークン分はまとめて消す
"clear_at_least": {"type": "input_tokens", "value": 5000},
# ツールの「引数」も消すか(既定は False = 引数は残る)
"clear_tool_inputs": False,
}
]
}
resp = client.beta.messages.create(
model="claude-sonnet-5",
max_tokens=4096,
betas=["context-management-2025-06-27"],
tools=TOOLS,
messages=messages,
context_management=CONTEXT_MGMT,
)
消された分は本文が [cleared] 相当のプレースホルダに置き換わり、モデルには「ここは消した」という事実が伝わる。何回・何トークン消えたかはレスポンス側で取れる。
info = getattr(resp, "context_management", None)
if info and info.applied_edits:
for e in info.applied_edits:
print("cleared:", e.type, getattr(e, "cleared_tool_uses", None))
自分はこれをループの毎ターンでログに落として、どのタイミングで刈り取りが走ったかを追えるようにしている。
ハマりどころ 1:プロンプトキャッシュが毎回壊れる
これが一番痛かった。プロンプトキャッシュは「プレフィックスが完全一致していること」が条件で、会話の途中にある古い tool_result を消すと、そこから後ろのキャッシュが全部無効になる。
つまり刈り取りが走るたびにキャッシュミスが発生し、キャッシュ書き込み料金(通常の 1.25 倍)を払い直すことになる。小刻みに発火させると、削減した入力トークン以上に書き込みコストが乗る。
対策は clear_at_least を大きめに取ること。「発火したら最低これだけまとめて消す」という下限を設けて、刈り取り回数そのものを減らす。自分は trigger を高め(3 万〜5 万)・clear_at_least を 5000 以上にして、1 セッションで数回しか走らない設定に落ち着いた。ちまちま消すのが一番損。
ハマりどころ 2:残したいツール結果まで消える
keep は「直近 N 往復」という時間軸の指定でしかない。エージェントの序盤で読んだ仕様書やタスク定義が、いちばん重要なのに真っ先に消える、という事故が起きる。
{
"type": "clear_tool_uses_20250919",
"trigger": {"type": "input_tokens", "value": 30000},
"keep": {"type": "tool_uses", "value": 3},
# このツールの結果は刈り取り対象から除外する
"exclude_tools": ["read_spec", "get_task_definition"],
}
exclude_tools にツール名を並べると、そのツールの tool_result は何ターン前だろうと残る。設計としては「消えても後から取り直せるツール」と「消えたら詰むツール」を最初に仕分けしておくのが正解だった。ファイル読み込み系は消してよく、ユーザーの指示や外部から一度きりで取得した値は残す。
ハマりどころ 3:extended thinking と併用すると挙動が読みにくい
thinking を有効にしていると、ツール往復の間に thinking ブロックが挟まる。ここで注意なのは、thinking ブロックは clear_tool_uses の対象ではないこと。消えるのはあくまで tool_result(と設定次第で tool_input)で、思考ブロックはそのまま残る。
「トークンが減るはずなのに思ったほど減らない」と悩んだら、内訳を疑ったほうがいい。自分は count_tokens で刈り取り前後を実測して、thinking が残っていることに気付いた。
# 刈り取り前後の入力トークンを比較する
python3 measure_context.py --before --after
thinking 側を減らしたいなら thinking.budget_tokens を絞るか、ツール往復が長引く設計自体を見直すことになる。別レイヤーの問題として切り分けるのが早い。
動作確認
検証は「わざと溢れさせる」のが手っ取り早い。数 KB のダミーテキストを返すだけのツールを 1 つ用意して、それを 10 回連続で呼ばせるループを回す。
-
triggerを 5000 くらいに下げておく - 毎ターン
resp.usage.input_tokensを出力する -
applied_editsが出たターンで入力トークンが階段状に落ちれば成功
自分の環境では、7 ターン目で発火して入力が約 4 割落ちた。閾値を跨いだ瞬間にガクッと下がるグラフが出れば、設定は効いている。
まとめ
- Context Editing は
context_managementを渡すだけで、古いツール結果をサーバー側で自動的に間引ける - プロンプトキャッシュと同時に使うなら
clear_at_leastを大きく取り、刈り取り回数を減らす。小刻みな発火は逆効果 -
keepは時間軸でしか守れないので、消えたら困るツールはexclude_toolsで明示的に除外する - thinking ブロックは刈り取られない。減らないときは内訳を実測して切り分ける
- 検証はダミーツールで意図的に溢れさせ、
applied_editsとinput_tokensの階段を見るのが確実