個人開発で回している自動投稿のパイプラインで、リサーチ系のエージェントを一度動かしっぱなしにしてみたところ、20 回ほどツールを呼んだあたりで入力トークンが 7 万を超え、1 ターンごとの課金が無視できない額になっていました。中身を覗くと、原因の大半は「もう使い終わった web_search の結果」が会話履歴にそのまま残り続けていることでした。エージェントが長く動くほど、過去のツール結果がコンテキストを圧迫していく——これは私だけの話ではなく、ツールを多用するワークフロー全般に共通する課題だと思います。
Claude API には、この肥大化を二方向から扱う仕組みが用意されています。古いツール結果をサーバー側で消す context editing と、残したい情報をファイルに退避する memory tool です。役割が正反対なので、片方だけでなく組み合わせて使うのが実用的でした。以下では、この 2 つを最小構成から本番構成まで段階的に組み上げ、削減量を count_tokens で事前に測るところまで、自前実装のコードとともに追っていきます。
「消す」context editing と「残す」memory tool は役割が逆
最初に整理しておきたいのは、この 2 機能が解決する問題がまったく違うという点です。
context editing(clear_tool_uses_20250919)は、会話履歴が一定のしきい値を超えたときに、古いツール結果をサーバー側で 自動的に削除します。削除された箇所にはプレースホルダーが差し込まれ、Claude は「ここに結果があったが消えた」と認識できます。つまり、もう参照しないであろう過去の検索結果やファイル読み込み結果を、こちらが手で切り詰めることなく落とせます。
一方 memory tool(memory_20250818)は、Claude が自分で ファイルに書き込み・読み出しを行うためのクライアントサイドツールです。/memories ディレクトリに学んだことを保存し、次のセッションや context editing で履歴が消えた後でも、必要なときに読み戻せます。
この対比が肝心です。context editing だけだと、消したツール結果に含まれていた重要な発見も一緒に失われます。そこで「これは後で要る」という情報だけ memory に書いておけば、履歴は軽く保ちつつ知識は残せる、という両取りができます。どちらも beta ヘッダー context-management-2025-06-27 で有効化します。
clear_tool_uses を最小構成で有効にする
まずは context editing だけを、いちばん素直な形で入れてみます。context_management に edit を 1 つ渡すだけです。
import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model = "claude-opus-4-8" ,
max_tokens = 4096 ,
messages = [{ "role" : "user" , "content" : "最近の AI エージェント研究を調べて要約してください" }],
tools = [{ "type" : "web_search_20250305" , "name" : "web_search" }],
betas = [ "context-management-2025-06-27" ],
context_management = { "edits" : [{ "type" : "clear_tool_uses_20250919" }]},
)
実際に消去が走ったかどうかは、レスポンスの context_management.applied_edits を見れば分かります。ここを確認せずに「効いているはず」と思い込むと、しきい値に届いていないだけで一度も発火していなかった、という見落としをしがちです。
applied = getattr (response, "context_management" , None )
if applied and applied.applied_edits:
for edit in applied.applied_edits:
print ( f "消去したツール呼び出し: { edit.cleared_tool_uses } 件 / "
f "削減トークン: { edit.cleared_input_tokens } " )
else :
print ( "今回は消去なし(しきい値未達)" )
最小構成では trigger を省略しているのでデフォルトのしきい値で動きます。短い会話では一度も発火しないのが正常です。発火しないこと自体は異常ではないので、ログで件数を見ながら次のチューニングに進みます。
trigger・keep・clear_at_least・exclude_tools をどう決めるか
実運用では、デフォルト任せではなく挙動を明示的に握ったほうが安定します。私が落ち着いた設定は次の形です。
context_management = {
"edits" : [
{
"type" : "clear_tool_uses_20250919" ,
# この入力トークン数を超えたら消去を始める
"trigger" : { "type" : "input_tokens" , "value" : 30000 },
# 直近 3 件のツール結果は残す
"keep" : { "type" : "tool_uses" , "value" : 3 },
# 一度の消去で最低これだけのトークンを削る
"clear_at_least" : { "type" : "input_tokens" , "value" : 5000 },
# この道具の結果は消さない
"exclude_tools" : [ "web_search" ],
}
]
}
それぞれの勘所を、実際に詰まった順で挙げます。
trigger は「いつ消し始めるか」です。低くしすぎると毎ターン消去が走ってプロンプトキャッシュが頻繁に無効化され、かえってコストが増えます。私はコンテキスト上限の 20〜30% を目安に置いています。keep は「直近いくつのツール結果を残すか」です。エージェントが直前の結果を参照して次の手を決める設計なら、ここを 1 や 2 にすると思考が途切れます。3 前後が無難でした。
clear_at_least は、せっかく消すなら一定量はまとめて削るための下限です。これがないと、しきい値ぎりぎりで「数百トークンだけ消して即また超過」を繰り返し、消去が小刻みに走ってキャッシュを壊し続けることがあります。exclude_tools は逆に「消したくない道具」を守る指定です。たとえば結果が後続の判断に効き続ける検索結果は残し、使い捨てのファイル一覧だけ消す、といった出し分けに使います。
memory tool のバックエンドを自前で実装する
memory tool はクライアントサイドなので、ツールを宣言するだけでは動きません。view / create / str_replace / insert / delete / rename の各コマンドを、こちら側で実処理する必要があります。ここで最も重要なのがパストラバーサル対策 です。
公式の SDK には BetaAbstractMemoryTool(Python)のようなヘルパーがありますが、保存先を完全に自分で握りたい場合は、薄いバックエンドを書いておくと挙動を把握しやすくなります。私自身、最初に書いたのもこの形でした。まず、つい書いてしまいがちな危ない実装 から見てみます。
# Before(危険): 受け取った path をそのまま結合している
from pathlib import Path
ROOT = Path( "./memories" )
def view (path: str ) -> str :
target = ROOT / path.lstrip( "/" ) # "../../etc/passwd" を防げない
return target.read_text( encoding = "utf-8" )
path に /memories/../../etc/passwd のような値が渡されると、/memories の外を読み書きできてしまいます。Claude が悪意を持つわけではなくても、誤動作や予期しない入力で外部のファイルに触れる経路を残してはいけません。次が保護を入れた実装 です。
# After(保護あり): canonical 解決後に /memories 配下か検証する
from pathlib import Path
ROOT = Path( "./memories" ).resolve()
def _safe (path: str ) -> Path:
# 先頭の /memories を実ディレクトリへ写像し、正規化して配下チェック
rel = path.replace( "/memories" , "" , 1 ).lstrip( "/" )
target = ( ROOT / rel).resolve()
try :
target.relative_to( ROOT ) # 配下でなければ ValueError
except ValueError :
raise PermissionError ( f "パスが /memories の外を指しています: { path } " )
return target
def handle_memory (cmd: dict ) -> str :
c = cmd[ "command" ]
if c == "view" :
p = _safe(cmd[ "path" ])
if p.is_dir():
items = " \n " .join( f " { f.stat().st_size }\t /memories/ { f.relative_to( ROOT ) } "
for f in sorted (p.rglob( "*" )) if f.is_file())
return f "Files under /memories: \n{ items } "
lines = p.read_text( encoding = "utf-8" ).splitlines()
return " \n " .join( f " { i + 1 :>6 }\t{ ln } " for i, ln in enumerate (lines))
if c == "create" :
p = _safe(cmd[ "path" ])
if p.exists():
return f "Error: File { cmd[ 'path' ] } already exists"
p.parent.mkdir( parents = True , exist_ok = True )
p.write_text(cmd[ "file_text" ], encoding = "utf-8" )
return f "File created successfully at: { cmd[ 'path' ] } "
if c == "str_replace" :
p = _safe(cmd[ "path" ])
body = p.read_text( encoding = "utf-8" )
if body.count(cmd[ "old_str" ]) != 1 :
return "No replacement was performed; old_str must appear exactly once."
p.write_text(body.replace(cmd[ "old_str" ], cmd[ "new_str" ]), encoding = "utf-8" )
return "The memory file has been edited."
if c == "delete" :
p = _safe(cmd[ "path" ])
p.unlink( missing_ok = True )
return f "Successfully deleted { cmd[ 'path' ] } "
return f "Error: unsupported command { c } "
Path.resolve() で .. を解消してから relative_to(ROOT) で配下かどうかを確かめる、というのが核です。ここを通らないパスは PermissionError で弾きます。insert と rename は紙幅の都合で省きましたが、同じ _safe を通せば同様に守れます。戻り値の文言は公式が想定する形に寄せておくと、Claude が結果を解釈しやすくなります。
エージェントループに両方を組み込む
context editing と memory tool は、同じ create 呼び出しに同居できます。ツール宣言に memory を加え、context_management で clear を効かせるだけです。
def run_turn (messages):
response = client.beta.messages.create(
model = "claude-opus-4-8" ,
max_tokens = 4096 ,
messages = messages,
tools = [
{ "type" : "memory_20250818" , "name" : "memory" },
{ "type" : "web_search_20250305" , "name" : "web_search" },
],
betas = [ "context-management-2025-06-27" ],
context_management = {
"edits" : [{
"type" : "clear_tool_uses_20250919" ,
"trigger" : { "type" : "input_tokens" , "value" : 30000 },
"keep" : { "type" : "tool_uses" , "value" : 3 },
}]
},
)
return response
# memory コマンドが来たらバックエンドで処理して tool_result を返す
def dispatch_tools (response, messages):
results = []
for block in response.content:
if block.type == "tool_use" and block.name == "memory" :
out = handle_memory(block.input)
results.append({ "type" : "tool_result" ,
"tool_use_id" : block.id, "content" : out})
if results:
messages.append({ "role" : "user" , "content" : results})
return messages
設計上のコツは、消えると困る発見だけを memory に書くよう促す ことです。memory tool を有効にすると「作業前に必ず memory ディレクトリを確認せよ」という指示がシステムプロンプトに自動で入りますが、放っておくと細かいメモを大量に作りがちです。「<対象> に関係する情報だけ memory に記録してください」と一言添えるだけで、ファイルの散らかりがかなり減りました。
count_tokens で削減量を push 前に測る
ここがいちばん実務で効いた部分です。context editing の効果は、本番に出す前に count_tokens で見積もれます。messages と同じ context_management を渡すと、消去前後のトークン数を返してくれます。
counted = client.beta.messages.count_tokens(
model = "claude-opus-4-8" ,
messages = long_messages, # 肥大化した実際の履歴
betas = [ "context-management-2025-06-27" ],
context_management = {
"edits" : [{
"type" : "clear_tool_uses_20250919" ,
"trigger" : { "type" : "input_tokens" , "value" : 30000 },
"keep" : { "type" : "tool_uses" , "value" : 5 },
}]
},
)
original = counted.context_management.original_input_tokens
after = counted.input_tokens
print ( f "消去前: { original } / 消去後: { after } / 削減: { original - after } " )
私のリサーチ系エージェントの履歴で測ると、消去前およそ 7 万トークンが消去後およそ 2.5 万トークンまで落ちました。削減幅はツールの使い方に強く依存するので、この数字をそのまま当てにするのではなく、自分の典型的な履歴で必ず測ってから trigger と keep を調整するのが確実です。私の場合も、本番に出す前に一度 count_tokens で当たりを付けることを強く推奨します。課金が発生しない count_tokens で先に当たりを付けられるのは、本番コストを動かす前の安全弁として頼りになりました。
運用で踏みやすい落とし穴
組み込んでから気づいた注意点を 3 つ挙げます。
プロンプトキャッシュとの相互作用
context editing が走るとキャッシュ済みのプレフィックスが変わるため、消去が起きたターンはキャッシュが効きません。trigger を低くしすぎると毎ターン消去でキャッシュが壊れ続け、削減したつもりが逆にコスト増になります。消去はときどき起きるくらいの頻度に保つのが目安です。
memory ファイルの肥大
Claude は放っておくとメモを増やし続けるので、1 ファイルあたりの最大文字数を決め、view が返す量に上限を設けてページングさせる、長期間アクセスのないファイルは定期的に消す、といった運用側のガードを入れておくと安全です。
edit の並び順
thinking のクリア(clear_thinking_20251015)と併用する場合、edits 配列では thinking のクリアを必ず先頭に置く必要があります。順番を逆にすると意図どおりに効きません。
長時間動くエージェントを安定運用したいなら、まず手元の肥大化した履歴を 1 本 count_tokens に通し、消去前後のトークン差を実測してみてください。そこで初めて、自分のワークフローにとって trigger と keep をどこに置くべきかが、数字で見えてきます。
参考: Context editing(Claude API ドキュメント) / Memory tool(Claude API ドキュメント)