検索を挟むエージェントを一日中走らせていて、ある朝 usage のログを並べ直したときに手が止まりました。web_search_requests の回数はほとんど変わっていないのに、output_tokens だけが前週の倍近くに伸びていたのです。
原因はすぐには腑に落ちませんでした。dynamic filtering を有効にしていたので、検索結果はモデルが書いたコードで絞り込まれ、context window には必要な分しか載らないはずだと思い込んでいたからです。けれど実際に応答の content を一つずつ数えてみると、絞り込みに使われたあとの生の web_search_tool_result ブロックが、そのまま応答の出力として乗り続けていました。フィルタ済みの情報をモデルが持っているのに、フィルタ前の原文がもう一度、出力トークンとして運ばれていたわけです。
2026 年 3 月の web_search_20260318(および web_fetch_20260318)で入った response_inclusion は、ちょうどこの「二度運び」を断つためのパラメータです。地味な追加ですが、検索を含むエージェントを継続運用している身には出力コストに直結します。ここでは、私が 4 サイトの自動運用で実際に踏んだこの落とし穴を起点に、excluded を安全に使える境界線を実装と判断表で整理していきます。
検索結果は「入力」と「出力」の両方でトークンを食う
まず、検索結果がどこでトークンを消費するのかを正確に押さえておきます。ここが曖昧だと、削減策を一つ打っても効いた感触が得られません。
Web 検索の課金は、検索 1,000 回あたり 10 ドルの従量に加え、検索で取得した本文がトークンとして 乗ります。公式ドキュメントは「検索結果は、同一ターン内の検索反復でも、後続の会話ターンでも入力トークンとして数えられる」と明記しています。つまり取得した本文は、
取得したターンで、モデルが読むための入力 として一度、
そのターンの応答 content に web_search_tool_result ブロックとして乗る出力 として一度、
カウントされ得ます。dynamic filtering は前者の入力側を賢く絞る仕組みです。モデルが code execution の中でコードを書き、検索結果を context window に載せる前に選別する。だから入力側は確かに軽くなります。
ところが後者の出力側、すなわち応答に echo される生の結果ブロックは、dynamic filtering だけでは消えません。フィルタに使い終わった原文が、クライアントに送り返す応答の中に残り続けるのです。私の自動運用のように「最終的な要約だけ受け取れればよく、検索原文をユーザーに見せない」ワークフローでは、この出力分は丸ごと無駄でした。
response_inclusion が外せるのは「完了した code execution が消費した結果」だけ
response_inclusion の既定値は "full" です。"excluded" を指定すると、同一ターン内で完了した code execution の呼び出しが消費した 検索結果について、入れ子になった server_tool_use と結果ブロックの対を応答から丸ごと落とします。
ここで効いてくる条件が二つあります。読み飛ばすと事故になる部分なので、はっきり書いておきます。
第一に、外れるのは「完了した code execution が消費した結果」に限られます。dynamic filtering はコード実行を伴うので、検索結果はコードに消費されます。そのコード実行がそのターン内で完走したなら、モデルは既にフィルタ後の情報を手にしている。だから生ブロックは応答に echo されるだけの存在になり、安全に落とせる、という理屈です。
第二に、direct call の結果 (dynamic filtering を介さない素の検索)と、pause_turn で中断した code execution の結果は、excluded を指定しても常に full で返ります。これらは次のターンに送り返して引用や継続に使う必要があるためで、API 側が落とさないよう守ってくれています。response_inclusion は「もう次ターンに要らないと確定したブロックだけを外す」設計になっている、と理解すると腑に落ちます。
なお、引用(citations)の cited_text・title・url は、そもそも入力にも出力にもトークンとして数えられません。excluded で落ちるのは原文側の重い web_search_tool_result ブロックであって、引用そのものはテキストブロック側に残ります。ここを混同して「excluded にすると引用が消える」と早合点しないことが大切です。
excluded を有効にする最小構成
実装は、ツール定義に一行足すだけです。dynamic filtering は code execution の有効化が前提なので、両方を渡します。検索・取得と併用する場合、code execution の課金は無料になる点も運用上ありがたいところです。
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model = "claude-opus-4-8" ,
max_tokens = 4096 ,
messages = [{
"role" : "user" ,
"content" : (
"主要クラウドのAI推論料金の最新値を調べ、"
"1Mトークンあたりの相対比だけを表にして返してください。"
"取得したページの生本文は返さなくて構いません。"
),
}],
tools = [
{ "type" : "code_execution_20250825" , "name" : "code_execution" },
{
"type" : "web_search_20260318" ,
"name" : "web_search" ,
"max_uses" : 8 ,
"response_inclusion" : "excluded" ,
},
],
)
print (resp.usage)
このリクエストでは、モデルが検索を反復し、code execution の中で結果を絞り込み、最後に相対比の表だけを返します。excluded を付けているので、絞り込みに消費された検索原文は応答から落ち、output_tokens には最終的な表とそこに至る思考だけが乗ります。
削減が効いているかは、応答 content の中身を数えると一目で分かります。次のヘルパーは、echo された生の結果ブロック数と usage を並べて表示します。
def summarize (resp) -> None :
echoed = sum (
1 for b in resp.content
if b.type == "web_search_tool_result"
)
sts = resp.usage.server_tool_use
print ( f "output_tokens = { resp.usage.output_tokens } " )
print ( f "web_search_requests = { sts.web_search_requests if sts else 0 } " )
print ( f "echoed_result_blocks = { echoed } " )
full で走らせたときと excluded で走らせたときを同じプロンプトで比べると、web_search_requests は同じまま echoed_result_blocks が 0 に落ち、それに連動して output_tokens が下がります。検索回数は変わらないのに出力だけが軽くなる、というこの形が見えれば、設定が正しく効いている証拠です。
安全に外せる場合・full を保つべき場合
excluded は万能スイッチではありません。応答の使い道によっては full を保たねばならない場面があります。私が運用で線引きしている基準を表にまとめます。
状況 推奨設定 理由
同一ターンで完了した code execution が消費した検索結果 excluded で外せる モデルは既にフィルタ後の情報を保持。生ブロックは出力に echo されるだけで次ターンに不要
dynamic filtering を介さない direct call の結果 常に full (指定しても外れない)次ターンに送り返して引用・継続に使うため API が保持する
pause_turn で中断した code execution の結果 常に full (指定しても外れない)再開時にそのまま送り返す必要があるため
検索原文をエンドユーザーに提示・監査する full を維持 引用元の原文表示や根拠の追跡に生ブロックが要る
要約・スコア・判定だけを下流に渡すサーバー側エージェント excluded を推奨 原文を返す価値がなく、出力トークンの純粋な削減になる
判断の軸は単純です。「その生ブロックを、このターンの応答を受け取った後にもう一度使うか 」。使うなら full、使わないなら excluded。私の自動投稿パイプラインの調査エージェントは、最終的に構造化した所見だけを次の工程へ渡すので、ほとんどの検索を excluded に倒しています。一方、読者に出典リンクを見せる用途のエージェントだけは full のまま残しています。
clear_tool_uses・クライアント側圧縮との使い分け
ツール返り値でコンテキストが膨らむ問題には、response_inclusion の他にも手があります。混同して同じ場所に二重で対策を打たないよう、効く場所が違うことを押さえておきます。
手法 削るトークン 効くタイミング 主な対象
response_inclusion: excluded 出力 (echo される消費済み結果ブロック)同一ターン内で完結 web_search / web_fetch × dynamic filtering
clear_tool_uses(context editing) 入力 (過去ターンに溜まった tool_use / tool_result)複数ターンにまたがる蓄積に対して 任意のツール返り値の履歴
クライアント側圧縮(スキーマ縮約・参照渡し) 入力 (自前ツールの巨大返り値)ツールを返却する瞬間 DB 行・スクレイプ HTML 等の自前ツール
この三つは競合しません。response_inclusion は今このターンの出力 を軽くする。context editing の clear_tool_uses は、ターンをまたいで溜まった過去の tool 履歴を入力 から削る。自前ツールのクライアント側圧縮 は、サーバーツールではない自分で書いたツールの返り値を返す瞬間に絞り込む。
私の運用では、サーバーツール(検索・取得)には response_inclusion、自前の MCP ツールにはクライアント側圧縮、そして長く回り続けるループ全体には context editing、という三層で住み分けています。どれか一つで全部を賄おうとすると、必ずどこかに取りこぼしが残ります。
出力トークンの削減を push 前に usage で確かめる
最後に、設定を本番へ入れる前の確認手順です。体感ではなく数字で裏を取ります。私自身は、次の順で確かめています。
同じプロンプト・同じ max_uses で full と excluded を一度ずつ走らせ、output_tokens の差分を記録します。
excluded 側で最終応答の中身(要約・表・判定)が劣化していないことを確認します。
引用をユーザーに見せる経路だけは full のまま据え置くと決めます。
検索の量が多いタスクほど差は大きく出ます。私の調査エージェントの一例では、web_search_requests が 6 回前後のタスクで、excluded にしたときの output_tokens がおおむね 3〜4 割落ちました。検索回数が少ない単発タスクでは差は小さく、ループが深く検索が多いほど効く、という素直な傾向です。
def compare (make_request) -> None :
"""make_request(inclusion) は response を返す関数"""
for mode in ( "full" , "excluded" ):
r = make_request(mode)
echoed = sum ( 1 for b in r.content if b.type == "web_search_tool_result" )
print ( f "[ { mode :8 } ] out= { r.usage.output_tokens :>6 } echoed_blocks= { echoed } " )
出力が軽くなっても結論がぶれていては本末転倒なので、トークン差分そのものよりも、応答の質が保たれているかを最終的な判断基準にしています。
個人開発の自動運用での落としどころ
response_inclusion は、課金体系を眺めていて「検索は入力でも出力でも金を取られる」と気づいたときに、初めて価値が腑に落ちるたぐいの機能です。dynamic filtering で入力を絞っただけで満足していると、出力側に残った原文の二度運びを見落とします。
一人で複数サイトのパイプラインを回していると、こうした地味な漏れが毎日少しずつ積み上がります。私は調査・絞り込みが主目的で原文を下流に渡さないエージェントを既定で excluded にし、読者向けに出典を見せる経路だけを full で残す、という運用に落ち着きました。
まず手元の一番検索の多いエージェントで、full と excluded を一度ずつ走らせ、output_tokens の差を見てみてください。差が出るなら、それは毎回の実行で静かに払っていたコストです。お読みいただきありがとうございました。