Anthropic SDK Tool Helpers実践ガイド — betaZodToolとtoolRunnerで型安全なツール呼び出しを最小コードで実現する
Claude APIのtool_use実装、JSONスキーマの手書きとswitch文の羅列にうんざりしていませんか? Anthropic公式SDKが提供するTool Helpers(betaZodTool + toolRunner)を使えば、Zodスキーマから型推論が効いたツール定義と、ループ不要の自動実行が手に入ります。本記事では従来パターンとの同一タスク比較実装を通じて、コード量・型安全性・エラーハンドリングの差を実測で示します。
従来のtool_use実装 — 何が冗長なのか
手書きJSON Schemaとswitch文ルーティングの典型パターン
まず、天気取得と都市検索の2ツールを従来パターンで実装したコードを見てみましょう。
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.inputはunknownであり、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からインポートします。
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_reasonがtool_useである限り「run関数実行 → tool_resultを付けて再リクエスト」を自動で繰り返します。
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を渡すだけです。
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するだけでモデルにエラー内容が自動伝搬されます。
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からスキーマが自動生成されます。
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の設定忘れ。 toolRunnerにmax_iterationsを設定しないと、モデルがツールを呼び続ける限りループが終了しません。開発中は3〜5程度に設定しておくと安心です。
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は十分安定しており、新規プロジェクトでは積極的に採用を検討する価値があります。既存コードからの移行もツール定義の書き換えだけで済むため、段階的な導入が可能です。
