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

Anthropic SDK Tool Helpers実践ガイド — betaZodToolとtoolRunnerで型安全なツール呼び出しを最小コードで実現する

(更新: 2026年03月15日)
Claude APIAnthropic SDKTypeScriptツール呼び出し

Claude APIのtool_use実装、JSONスキーマの手書きとswitch文の羅列にうんざりしていませんか? Anthropic公式SDKが提供するTool Helpers(betaZodTool + toolRunner)を使えば、Zodスキーマから型推論が効いたツール定義と、ループ不要の自動実行が手に入ります。本記事では従来パターンとの同一タスク比較実装を通じて、コード量・型安全性・エラーハンドリングの差を実測で示します。

従来のtool_use実装 — 何が冗長なのか

手書きJSON Schemaとswitch文ルーティングの典型パターン

まず、天気取得と都市検索の2ツールを従来パターンで実装したコードを見てみましょう。

typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const tools: Anthropic.Tool[] = [
  {
    name: "get_weather",
    description: "指定都市の現在の天気を取得する",
    input_schema: {
      type: "object" as const,
      properties: {
        city: { type: "string", description: "都市名" },
        unit: { type: "string", enum: ["celsius", "fahrenheit"] },
      },
      required: ["city"],
    },
  },
  {
    name: "search_cities",
    description: "キーワードで都市を検索する",
    input_schema: {
      type: "object" as const,
      properties: {
        query: { type: "string", description: "検索キーワード" },
        limit: { type: "number", description: "最大件数" },
      },
      required: ["query"],
    },
  },
];

async function run(userMessage: string) {
  let messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];

  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-5-20250929",
      max_tokens: 1024,
      tools,
      messages,
    });

    if (response.stop_reason === "end_turn") {
      return response.content;
    }

    const toolResults: Anthropic.MessageParam = {
      role: "user",
      content: response.content
        .filter((block): block is Anthropic.ToolUseBlock => block.type === "tool_use")
        .map((toolUse) => {
          const input = toolUse.input as Record<string, unknown>;
          let result: string;
          switch (toolUse.name) {
            case "get_weather":
              result = JSON.stringify({ temp: 22, condition: "晴れ", city: input.city });
              break;
            case "search_cities":
              result = JSON.stringify([{ name: "東京" }, { name: "大阪" }]);
              break;
            default:
              result = "Unknown tool";
          }
          return { type: "tool_result" as const, tool_use_id: toolUse.id, content: result };
        }),
    };

    messages = [...messages, { role: "assistant", content: response.content }, toolResults];
  }
}

3つの課題:スキーマと型の二重定義、手動ループ、エラー伝搬の煩雑さ

このコードには構造的な問題が3つあります。

1. スキーマと型の二重定義。 input_schemaはただのオブジェクトリテラルなので、TypeScriptの型システムとは完全に断絶しています。toolUse.inputunknownであり、as Record<string, unknown>でキャストするしかありません。プロパティ名のtypoは実行時まで気づけません。

2. 手動ループ。 while (true) + stop_reason判定は、ツール呼び出しがある限り毎回書く定型コードです。ツールが結果を返した後にモデルがさらに別のツールを呼ぶケースにも対応する必要があり、ループの終了条件を間違えるとバグになります。

3. エラー伝搬の煩雑さ。 ツール実行中にエラーが起きた場合、is_error: trueを付けたtool_resultを手動で構築してモデルに返す必要があります。try-catchとエラーフォーマットのボイラープレートがツールごとに増殖します。

ツールが5個、10個と増えたときのswitch文の肥大化は想像に難くないでしょう。

betaZodTool — Zodスキーマから型安全なツール定義を一発生成

インポートとツール定義の書き方

betaZodTool@anthropic-ai/sdk/helpers/beta/zodからインポートします。

typescript
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const getWeather = betaZodTool({
  name: "get_weather",
  description: "指定都市の現在の天気を取得する",
  inputSchema: z.object({
    city: z.string().describe("都市名"),
    unit: z.enum(["celsius", "fahrenheit"]).optional(),
  }),
  // input の型は自動的に { city: string; unit?: "celsius" | "fahrenheit" } に推論される
  run: async (input) => {
    return JSON.stringify({ temp: 22, condition: "晴れ", city: input.city });
  },
});

const searchCities = betaZodTool({
  name: "search_cities",
  description: "キーワードで都市を検索する",
  inputSchema: z.object({
    query: z.string().describe("検索キーワード"),
    limit: z.number().optional().describe("最大件数"),
  }),
  run: async (input) => {
    // input.query は string 型。typoするとコンパイルエラー
    return JSON.stringify([{ name: "東京" }, { name: "大阪" }]);
  },
});

run関数の型推論が効く仕組み

ポイントはinputSchemaにZodオブジェクトを渡すと、run関数の引数inputが自動的にz.infer<typeof schema>相当の型になることです。input.cityと書けば補完が効き、input.ctyと書けばコンパイルエラーになります。JSON Schemaとの二重定義が完全に不要になります。

Zodを依存に入れたくないプロジェクトには、JSON SchemaベースのbetaTool@anthropic-ai/sdk/helpers/beta/json-schemaからインポート)も用意されています。こちらはJSON Schemaリテラルからの型推論が効きますが、個人的にはZod版のほうが書き心地がよく気に入っています。

toolRunner — ツール呼び出しループを自動化する

client.beta.messages.toolRunner()の基本

toolRunnerは、stop_reasontool_useである限り「run関数実行 → tool_resultを付けて再リクエスト」を自動で繰り返します。

typescript
const client = new Anthropic();

const runner = client.beta.messages.toolRunner({
  model: "claude-sonnet-4-5-20250929",
  max_tokens: 1024,
  messages: [{ role: "user", content: "東京の天気を教えて" }],
  tools: [getWeather, searchCities],
  max_iterations: 5, // 無限ループ防止
});

// awaitするだけで最終メッセージが得られる
const finalMessage = await runner;
console.log(finalMessage.content);

従来パターンではwhileループ・stop_reason判定・switch文ルーティング・メッセージ配列の手動管理が必要でした。Tool Helpersではそれがすべて不要です。正直、初めて動かしたときは「これだけ?」と拍子抜けしました。

ストリーミング対応

ストリーミングはstream: trueを渡すだけです。

typescript
const runner = client.beta.messages.toolRunner({
  model: "claude-sonnet-4-5-20250929",
  max_tokens: 1024,
  messages: [{ role: "user", content: "東京の天気を教えて" }],
  tools: [getWeather, searchCities],
  stream: true,
});

for await (const messageStream of runner) {
  for await (const event of messageStream) {
    if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
      process.stdout.write(event.delta.text);
    }
  }
}

max_iterationsオプションは必ず設定しましょう。未設定だとモデルがツールを呼び続ける限りループが止まりません。

Before/After比較 — 同一タスクでの実測差

同じ2ツール構成で、従来パターンとTool Helpersを比較した結果です。

観点 従来パターン Tool Helpers
スキーマ定義 JSON Schema手書き Zodスキーマ(型推論付き)
ルーティング switch文で手動分岐 run関数で定義済み、自動ディスパッチ
ループ管理 while + stop_reason判定 toolRunnerが自動管理
型安全性 unknown → 手動キャスト コンパイル時に型チェック完了
エラー伝搬 is_error: trueを手動構築 ToolErrorをthrowするだけ
コード行数(実測) 約55行 約25行

エラーハンドリングは特に差が大きいポイントです。従来パターンではtry-catch内でis_error: true付きのtool_resultを手動構築する必要がありましたが、Tool HelpersではToolErrorをthrowするだけでモデルにエラー内容が自動伝搬されます。

typescript
import { ToolError } from "@anthropic-ai/sdk/lib/tools/BetaRunnableTool";

const getWeather = betaZodTool({
  name: "get_weather",
  description: "指定都市の天気を取得する",
  inputSchema: z.object({ city: z.string() }),
  run: async (input) => {
    const data = await fetchWeather(input.city);
    if (!data) {
      // モデルに is_error: true の tool_result として自動送信される
      throw new ToolError(`都市 "${input.city}" の天気データが見つかりません`);
    }
    return JSON.stringify(data);
  },
});

Python版との違いとAgent SDKのtool()との使い分け

Python:@beta_toolデコレータと型ヒントベースの自動スキーマ生成

Python版はデコレータ方式で、関数の型ヒントとdocstringからスキーマが自動生成されます。

python
import anthropic
from anthropic import beta_tool

client = anthropic.Anthropic()

@beta_tool
async def get_weather(city: str, unit: str = "celsius") -> str:
    """指定都市の現在の天気を取得する"""
    return json.dumps({"temp": 22, "condition": "晴れ", "city": city})

runner = client.beta.messages.tool_runner(
    model="claude-sonnet-4-5-20250929",
    max_tokens=1024,
    tools=[get_weather],
    messages=[{"role": "user", "content": "東京の天気は?"}],
)
for message in runner:
    print(message)

async関数には@anthropic.beta_async_toolを使います。Pydantic v2が必要ですが、Zodと同様に型ヒントからJSON Schemaへの変換と型チェックが同時に行われます。

Agent SDKのtool()ヘルパーとの境界線

Claude Agent SDKにもtool()ヘルパーがありますが、用途が異なります。

  • Messages APIのbetaZodTool: 単発のAPI呼び出しやカスタムループで使う。自分でオーケストレーションを組みたい場合に最適
  • Agent SDKのtool(): エージェントフレームワーク内で使う。ハンドオフやガードレールなどAgent SDK固有の機能と連携する前提

新規開発でAgent SDKを採用するならAgent SDKのtool()を、Messages APIを直接叩く構成ならbetaZodToolを使うのが自然な選択です。

なお、MCPツールをMessages APIのツール形式に変換するヘルパーも存在します。@anthropic-ai/sdk/helpers/beta/mcpからmcpTools等をインポートすることで、MCPサーバーのツールをそのままtoolRunnerに渡せます。

実装時のハマりポイントと対処法

betaプレフィックスの意味。 現時点でbeta APIであり、将来的にインポートパスやAPI形状が変更される可能性があります。プロダクション利用時はSDKのバージョンを固定(package.jsonでexact version指定)しておくのが安全です。

プロパティ名はcamelCase。 betaZodToolのプロパティはinputSchema(camelCase)です。Messages APIのinput_schema(snake_case)と混同しやすいので注意してください。

max_iterationsの設定忘れ。 toolRunnermax_iterationsを設定しないと、モデルがツールを呼び続ける限りループが終了しません。開発中は35程度に設定しておくと安心です。

z.enumの変換。 Zodのz.enum(["a", "b"])はJSON Schemaの{ "type": "string", "enum": ["a", "b"] }に正しく変換されます。ただしz.nativeEnumは期待通りに変換されない場合があるため、z.enumを使うのが無難です。

ストリーミング時のブロッキング。 run関数が同期的に重い処理を行うと、ストリーミングイベントの配信がブロックされます。外部API呼び出しやDB操作は必ずasyncで実装しましょう。

まとめ

SDK Tool Helpersを使えば、tool_use実装の3大定型作業 — スキーマ手書き・ルーティングswitch文・whileループ — がすべて不要になります。特にbetaZodToolによる型安全なスキーマ定義とtoolRunnerによる自動ループは、ツール数が増えるほど恩恵が大きくなります。

まだbetaですがAPIは十分安定しており、新規プロジェクトでは積極的に採用を検討する価値があります。既存コードからの移行もツール定義の書き換えだけで済むため、段階的な導入が可能です。

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