メインコンテンツへスキップ
ブログ一覧

Claude API サーバーサイドツール実践ガイド — Web Search・Code Execution・Tool Searchで「検索→分析→回答」を1リクエストに凝縮する

Claude APIサーバーサイドツールWeb SearchCode ExecutionAI開発

Claude APIのtool_useを使ったことがあるなら、あの「ツール呼び出し → 結果を返す → また呼び出し」のクライアントサイドループに覚えがあるはずだ。だが2026年2月にGAとなったサーバーサイドツールは、そのループ自体が不要になる全く別のパラダイムだ。Web検索もPythonコード実行も1万ツールからの絞り込みも、Anthropicのサーバー側で自動完結する。

本記事では、3つのサーバーサイドツール(web_search / code_execution / tool_search)を体系的に整理し、Dynamic Filteringによるトークン削減、無料枠の活用法、そして「リサーチブリーフ自動生成」の実装例まで、APIアプリケーション開発者が今日から使える実践パターンを解説する。

サーバーサイドツールとは何か — クライアントサイドtool_useとの根本的な違い

従来のtool_use: 開発者がループを回す世界

従来のクライアントサイドtool_useでは、Claudeがツールを呼びたいときにstop_reason: "tool_use"でレスポンスが中断される。開発者はそのツール呼び出しを解析し、実際にツールを実行し、tool_resultとして結果を返し、再度APIを呼ぶ——というループを自前で実装する必要があった。

python
# クライアントサイドtool_use: 開発者がループを回す
import anthropic

client = anthropic.Anthropic()
messages = [{"role": "user", "content": "東京の天気を調べて"}]

while True:
    response = client.messages.create(
        model="claude-sonnet-4-5-20250514",
        max_tokens=1024,
        tools=[{"name": "get_weather", "description": "天気を取得",
                "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}}}],
        messages=messages
    )
    if response.stop_reason == "tool_use":
        tool_block = next(b for b in response.content if b.type == "tool_use")
        # 開発者が自分でツールを実行
        result = call_weather_api(tool_block.input["city"])
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": tool_block.id, "content": result}
        ]})
    else:
        break  # end_turnで完了

サーバーサイドツール: Anthropicが実行を完結させる世界

サーバーサイドツールでは、このループが丸ごと不要になる。toolsにサーバーサイドツールを指定するだけで、Anthropicのサーバー側でツール実行が完結し、結果が含まれた状態でレスポンスが返る。

python
# サーバーサイドツール: 1リクエストで完結
response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=[{"type": "web_search_20260209", "name": "web_search", "max_uses": 3}],
    messages=[{"role": "user", "content": "Claude APIのサーバーサイドツールについて教えて"}]
)
# ループ不要。responseに検索結果を踏まえた回答が含まれている

IDプレフィックスで見分ける — toolu vs srvtoolu

レスポンスに含まれるツール呼び出しIDを見れば、どちらの実行形態か一目で判別できる。クライアントサイドはtoolu_プレフィックス、サーバーサイドはsrvtoolu_プレフィックスだ。レスポンス解析のロジックを組む際にこの区別は重要になる。

なお、長時間の処理ではstop_reason: "pause_turn"が返ることがある。この場合はレスポンスのcontentをそのまま次のリクエストのmessagesに追加して継続すればよい。

2026年2月17日のGA化により、anthropic-betaヘッダーは不要になった。ただしAmazon BedrockおよびGoogle Vertex AIではcode_executionが非対応である点に注意してほしい。

Web Search — Dynamic Filteringで精度とコストを両立する

基本の使い方: toolsにwebsearch20260209を追加するだけ

最新バージョンはweb_search_20260209だ。旧版web_search_20250305との最大の違いはDynamic Filtering対応にある。混同しやすいのでバージョンIDは正確に指定しよう。

python
response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=2048,
    tools=[{
        "type": "web_search_20260209",
        "name": "web_search",
        "max_uses": 5,
        "allowed_domains": ["docs.anthropic.com", "github.com"],  # 検索スコープ制御
    }],
    messages=[{"role": "user", "content": "Claude APIのweb searchツールの料金体系を教えて"}]
)

# citationsの抽出
for block in response.content:
    if block.type == "text":
        print(block.text)
        if hasattr(block, "citations") and block.citations:
            for cite in block.citations:
                print(f"  出典: {cite.url} - {cite.title}")

allowed_domainsblocked_domainsで検索対象をドメイン単位で制御できる。RAGアプリで特定のドキュメントサイトだけを検索ソースにしたい場合に便利だ。

Dynamic Filtering: 内部code_executionで検索結果をフィルタリング

Dynamic Filteringはweb_search_20260209で追加された目玉機能だ。仕組みとしては、検索結果をサーバー内部のcode_execution(Python)で解析・フィルタリングしてから回答生成に渡す。これにより、不要な検索結果がコンテキストに入らなくなる。

ベンチマーク結果は印象的で、BrowseCompスコアがSonnetで33.3% → 46.6%(+13.3pt)、Opusで45.3% → 61.6%(+16.3pt)に向上し、入力トークンも24%削減されている。

ただし注意点がある。Opusモデルではより詳細な分析を行う傾向があり、出力トークンが増加してコスト増に転じる場合がある。Sonnetでの利用が最もコストパフォーマンスに優れる。

料金設計とcited_textのトークンカウント外の恩恵

Web Searchの料金は$10/1,000検索 + 標準トークン料金だ。ここで見落としがちなのが、レスポンスに含まれるcited_texttitleurlフィールドはトークンカウントの対象外という仕様だ。出典情報をリッチに返してもトークン料金に上乗せされない——正直、これを知ったときはかなり得した気分になった。

Code Execution — 無料で使えるサンドボックスPython環境

web_search併用で完全無料になる条件

code_execution_20260120が最新バージョンで、対応モデルはOpus 4.5+とSonnet 4.5+に限られる。

最大の注目ポイントは、web_search_20260209またはweb_fetch_20260209と併用するとcode_executionが完全無料になることだ。これはDynamic Filteringと同じインフラを共有しているためで、検索系ツールを使うアプリでは追加コストなしでPython実行環境を手に入れられる。

スタンドアロン利用の場合は月1,550時間の無料枠があり、超過分は$0.05/時間だ。

REPL永続化とProgrammatic Tool Callingの活用

同一ターン内ではREPLの状態が保持される。つまり最初のコード実行で定義した変数やインポートしたライブラリを、後続のコード実行で参照できる。データの段階的な加工に適した設計だ。

python
# web_search + code_execution: 検索→分析を1リクエストで
response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=4096,
    tools=[
        {"type": "web_search_20260209", "name": "web_search", "max_uses": 5},
        {"type": "code_execution_20260120", "name": "code_execution"},
    ],
    messages=[{"role": "user",
               "content": "主要LLMプロバイダのAPI料金を検索し、入出力トークン単価を比較表にまとめて"}]
)
# Claudeが自動的にWeb検索→データ抽出→Python分析→表形式出力を実行
# code_execution料金はweb_search併用により無料

allowed_callers設定を使えば、code_executionが自動トリガーされる条件をプログラマティックに制御することも可能だ。

Tool Search — 1万ツールから必要なものだけを選ばせる

regexとBM25: 2つのバリアントの使い分け

大量のツール定義をtoolsに渡すとコンテキスト消費が膨大になる。100個のツールを定義するだけで数万トークンを消費し、毎リクエストのコストに直結する。Tool Searchはこの問題をサーバーサイドで解決する。

2つのバリアントがある:

  • regex版tool_search_tool_regex_20251119): ツール名・説明のパターンマッチ。高速だが柔軟性に劣る
  • BM25版tool_search_tool_bm25_20251119): 意味的な関連度スコアリング。多数ツールからの選択に強い
python
# BM25版: 100個のツールから必要なものだけを選択
tools = [generate_tool_definition(i) for i in range(100)]  # 大量のツール定義
tools.append({
    "type": "tool_search_tool_bm25_20251119",
    "name": "tool_search",
})

response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "ユーザーの注文履歴を取得して"}]
)
# Claudeがtool_searchで関連ツールを絞り込み、必要なものだけを呼び出す

トークン削減効果

実測で55Kトークン → 8.7Kトークン(85%削減)という数値が報告されている。最大10,000ツールまで対応しており、MCPサーバー統合やプラグインシステムなど、ツール数が動的に増えるアーキテクチャでの恩恵が大きい。

設計判断フロー — サーバーサイド vs クライアントサイド、どちらを選ぶか

判断フローチャート: 4つの分岐条件

すべてをサーバーサイドツールに置き換えるべきかというと、そうではない。以下の判断基準で使い分けるとよい。

  1. Web検索が必要か?web_search(サーバーサイド)
  2. データ分析・計算が必要か?code_execution(サーバーサイド)
  3. ツール定義が50個以上か?tool_search(サーバーサイド)
  4. 自社DB・外部API・ファイルシステムとの連携が必要か? → カスタムtool_use(クライアントサイド)

自社システムとの連携は、当然ながらAnthropicのサーバーからは実行できない。ここは引き続きクライアントサイドのtool_useが必要だ。

ハイブリッド構成: サーバーサイド + クライアントサイドの併用パターン

重要なのは、両者は同一リクエスト内で共存できることだ。

python
response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=4096,
    tools=[
        # サーバーサイド: Web検索
        {"type": "web_search_20260209", "name": "web_search", "max_uses": 3},
        # サーバーサイド: コード実行
        {"type": "code_execution_20260120", "name": "code_execution"},
        # クライアントサイド: 自社DB検索
        {"name": "search_products", "description": "自社商品DBを検索",
         "input_schema": {"type": "object",
                          "properties": {"query": {"type": "string"}},
                          "required": ["query"]}},
    ],
    messages=[{"role": "user",
               "content": "競合他社の最新価格をWeb検索し、うちの商品DBと比較して"}]
)
# web_searchはサーバーサイドで自動実行
# search_productsはstop_reason: tool_useで返るので開発者が処理

Claudeがサーバーサイドツールを先に実行し、その結果を踏まえてクライアントサイドのツール呼び出しを判断する——という流れが自然に成立する。ZDR(Zero Data Retention)環境ではallowed_callersの設定が追加で必要になる点だけ注意しておこう。

実装例: リサーチブリーフ自動生成エンドポイント

アーキテクチャ: 1リクエストで検索→分析→構造化出力

最後に、ここまでの知識を統合した実践的な実装例を示す。トピックを入力するとWeb検索 → 情報抽出 → Python分析 → 構造化レポートを1 APIコールで返すエンドポイントだ。

完全なコードとレスポンス例

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import anthropic
import json

app = FastAPI()
client = anthropic.Anthropic()

class ResearchRequest(BaseModel):
    topic: str
    max_searches: int = 5

class ResearchBrief(BaseModel):
    summary: str
    key_findings: list[str]
    sources: list[dict]

SYSTEM_PROMPT = """あなたはリサーチアナリストです。与えられたトピックについて:
1. Web検索で最新情報を収集する
2. 収集した情報をPythonで整理・分析する
3. 以下のJSON形式で構造化レポートを出力する

出力形式:
{"summary": "要約(200字以内)", "key_findings": ["発見1", "発見2", ...], "data_analysis": "分析結果"}
"""

@app.post("/research", response_model=ResearchBrief)
async def generate_research_brief(req: ResearchRequest):
    try:
        response = client.messages.create(
            model="claude-sonnet-4-5-20250514",
            max_tokens=8096,
            system=SYSTEM_PROMPT,
            tools=[
                {"type": "web_search_20260209", "name": "web_search",
                 "max_uses": req.max_searches},
                {"type": "code_execution_20260120", "name": "code_execution"},
            ],
            messages=[{"role": "user",
                       "content": f"以下のトピックについてリサーチブリーフを作成してください: {req.topic}"}]
        )

        # pause_turn対応: 長時間実行時の継続処理
        messages = [{"role": "user",
                     "content": f"以下のトピックについてリサーチブリーフを作成してください: {req.topic}"}]
        while response.stop_reason == "pause_turn":
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": [{"type": "text", "text": "続けてください"}]})
            response = client.messages.create(
                model="claude-sonnet-4-5-20250514",
                max_tokens=8096,
                system=SYSTEM_PROMPT,
                tools=[
                    {"type": "web_search_20260209", "name": "web_search",
                     "max_uses": req.max_searches},
                    {"type": "code_execution_20260120", "name": "code_execution"},
                ],
                messages=messages
            )

        # テキストブロックと出典の抽出
        text_content = ""
        sources = []
        for block in response.content:
            if block.type == "text":
                text_content += block.text
                if hasattr(block, "citations") and block.citations:
                    for cite in block.citations:
                        sources.append({
                            "url": cite.url,
                            "title": cite.title,
                            "cited_text": cite.cited_text
                        })

        # JSON部分のパース
        brief_data = json.loads(
            text_content[text_content.index("{"):text_content.rindex("}") + 1]
        )

        return ResearchBrief(
            summary=brief_data["summary"],
            key_findings=brief_data["key_findings"],
            sources=sources
        )

    except anthropic.APIError as e:
        raise HTTPException(status_code=502, detail=f"Claude API error: {e.message}")

コスト面では、検索料金($10/1,000検索)+ 入出力トークン料金のみ。code_executionweb_search併用により無料だ。本番運用では、max_tokensを余裕をもって設定すること、タイムアウトを長めに取ること(サーバーサイドツールの実行時間を含むため)を意識しておくとよい。


サーバーサイドツールは「APIの使い方が少し変わった」という話ではない。開発者がループを書いてツール結果を中継する設計パターンそのものが、検索・分析・大規模ツール選択という3つの頻出ユースケースで不要になった。特にweb_search + code_executionの併用は、コード実行が無料になるだけでなく、Dynamic Filteringによるトークン削減と精度向上を同時に得られる。

まだtool_useのクライアントサイドループだけでアプリを構築しているなら、まずはweb_searchの1行追加から試してほしい。検索 → 分析 → 回答が1リクエストに凝縮される体験は、APIアプリケーションの設計観を変えるはずだ。

もっと読む他の技術記事も読む