Claude API Fine-Grained Tool Streaming実践ガイド — ツール呼び出しのレイテンシを最大80%削減する実装パターン
Now I have enough research material. Let me write the article.
Claude API Fine-Grained Tool Streamingとは
Claude APIでツール呼び出し(Tool Use)を利用する際、標準のストリーミングではツールのパラメータがJSON検証のためにバッファリングされる。つまり、Claudeがツールの引数を生成し終わるまで、クライアント側には何も届かない。短いパラメータなら問題にならないが、長文テキストの生成やファイル書き込みなど、パラメータが大きくなるケースでは10〜20秒の無応答時間が発生する。
Fine-Grained Tool Streamingは、このバッファリングを取り除き、パラメータの値をJSON検証なしで即座にストリーミングする機能だ。2026年3月現在、全モデル・全プラットフォーム(Claude API、Amazon Bedrock、Google Vertex AI、Microsoft Foundry)でGA提供されており、ベータヘッダーは不要になっている。
標準ストリーミングとの違い — レイテンシ実測比較
公式ドキュメントに記載されている挙動の違いを整理する。
標準ストリーミング(eagerinputstreaming なし)
最初のチャンクが届くまで約15秒。チャンクは細かく分割され、ワードの途中で分断される。
(15秒待機)
Chunk 1: '{"'
Chunk 2: 'query": "Ty'
Chunk 3: 'peScri'
Chunk 4: 'pt 5.0 5.1 '
Chunk 5: '5.2 5'
Chunk 6: '.3'
Chunk 7: ' new f'
Chunk 8: 'eatur'
...
Fine-Grained Tool Streaming(eagerinputstreaming: true)
最初のチャンクが約3秒で到着。チャンクはより大きく、単語の区切りが保たれやすい。
(3秒待機)
Chunk 1: '{"query": "TypeScript 5.0 5.1 5.2 5.3'
Chunk 2: ' new features comparison'
初期レイテンシが15秒から3秒に短縮される。これは体感として劇的な差で、正直、一度使うと標準ストリーミングには戻れなくなる。
基本実装 — eagerinputstreamingの有効化
有効化は極めてシンプルで、ツール定義に eager_input_streaming: true を追加するだけだ。
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const stream = anthropic.messages.stream({
model: "claude-opus-4-6",
max_tokens: 65536,
tools: [
{
name: "make_file",
description: "Write text to a file",
eager_input_streaming: true, // これだけ
input_schema: {
type: "object",
properties: {
filename: {
type: "string",
description: "The filename to write text to",
},
lines_of_text: {
type: "array",
description: "An array of lines of text to write to the file",
},
},
required: ["filename", "lines_of_text"],
},
},
],
messages: [
{
role: "user",
content: "Can you write a long poem and make a file called poem.txt?",
},
],
});
const message = await stream.finalMessage();
console.log(message.usage);
ポイントは、eager_input_streaming はツール単位で設定できることだ。すべてのツールに一律で有効にする必要はなく、パラメータが大きくなりうるツールだけに適用すればよい。
部分JSONの受信とパース戦略
Fine-Grained Tool Streamingの最大の注意点は、受信するJSONが不完全・無効な可能性があることだ。標準ストリーミングでは完全なJSONが保証されるが、Fine-Grainedモードではバッファリングを省略しているため、途中で切れたJSONが届くことがある。
ストリーミングイベントの処理
SDKの content_block_delta イベントで input_json_delta を受信し、文字列を蓄積していく基本パターンは以下の通り。
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const stream = anthropic.messages.stream({
model: "claude-opus-4-6",
max_tokens: 65536,
tools: [
{
name: "write_code",
description: "Write code to a file",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: {
filename: { type: "string" },
code: { type: "string", description: "The source code" },
},
required: ["filename", "code"],
},
},
],
messages: [{ role: "user", content: "Write a Express.js server" }],
});
let accumulatedJson = "";
stream.on("inputJson", (json, snapshot) => {
// json: 今回の差分文字列
// snapshot: 蓄積済み文字列全体
accumulatedJson = snapshot;
// リアルタイムにUIへ反映したい場合は
// 部分パースを試みる
try {
const partial = JSON.parse(snapshot);
if (partial.code) {
process.stdout.write(partial.code.slice(-50)); // 末尾を表示
}
} catch {
// まだ不完全なJSON — 無視して蓄積を続ける
}
});
stream.on("message", (message) => {
// 完了後、最終的なメッセージから完全なtool_useを取得
for (const block of message.content) {
if (block.type === "tool_use") {
console.log("Final input:", block.input);
}
}
});
await stream.finalMessage();
部分JSONパーサーの活用
ストリーミング中にリアルタイムでUIを更新したい場合、try/catch で JSON.parse を繰り返すのは非効率だ。LLMストリーミング向けの部分JSONパーサーライブラリを使うとよい。
npm install partial-json-parser
import { parse } from "partial-json-parser";
stream.on("inputJson", (json, snapshot) => {
// 不完全なJSONでもパース可能な部分まで解釈してくれる
const partial = parse(snapshot);
if (partial.code) {
renderCodePreview(partial.code);
}
});
partial-json-parser は閉じられていない文字列や配列を最寄りの完全なオブジェクトまで補完してくれるため、ストリーミング中のプレビュー表示に適している。
max_tokens到達時のハンドリング
Fine-Grained Tool Streamingで最もハマりやすいのが max_tokens 到達時の挙動だ。トークン上限に達すると、パラメータの途中でストリームが打ち切られ、不完全なJSONが確定値として残る。
stream.on("message", (message) => {
if (message.stop_reason === "max_tokens") {
// パラメータが途中で切れている可能性が高い
console.warn("max_tokens reached — tool input may be incomplete");
// リトライするか、ユーザーに通知する
return;
}
for (const block of message.content) {
if (block.type === "tool_use") {
// stop_reason が "tool_use" なら正常完了
const input = block.input;
await executeTool(block.name, input);
}
}
});
stop_reason を必ずチェックし、max_tokens の場合は不完全なデータとして処理を分岐させること。
不正JSONをモデルに返す方法
ツール実行時にパースエラーが発生した場合、エラー情報をモデルにフィードバックして再試行させたいことがある。不正なJSONをそのまま tool_result に含めるとAPIエラーになるため、ラッパーオブジェクトで包む。
// 不正なJSONを安全にモデルへ返す
const toolResult = {
type: "tool_result" as const,
tool_use_id: toolUseBlock.id,
is_error: true,
content: JSON.stringify({
INVALID_JSON: invalidJsonString.replace(/"/g, '\\"'),
}),
};
こうすることで、モデルは何が問題だったかを理解し、次のターンで修正したパラメータを再送してくれる。
eagerinputstreamingの適用判断フロー
すべてのツールに eager_input_streaming を付けるべきかというと、そうではない。以下の基準で判断するとよい。
有効にすべきケース:
- ファイル書き込み系(コード生成、テキスト生成)— パラメータが数百行になりうる
- 検索クエリ系 — 体感のレスポンス速度が重要
- 配列パラメータ — リスト項目を逐次表示したい場合
無効のままでよいケース:
- パラメータが常に小さいツール(例:
get_weatherのcityパラメータ) - JSON構造の正確性が最優先で、部分JSONのハンドリングコストを避けたい場合
max_tokens到達時の不完全データ処理を実装する余裕がない場合
個人的には、ユーザー向けのUIがあるツールには積極的に有効化し、バックエンド処理のみのツールはデフォルトのままにする、という使い分けが実用的だと感じている。
PTCやAgent SDKとの組み合わせ
Programmatic Tool Calling(PTC)との関係
PTCはClaudeがコードを書いて複数のツールをまとめて呼び出す機能で、Fine-Grained Tool Streamingとは補完的な関係にある。PTCは「ラウンドトリップ回数の削減」、Fine-Grainedは「1回のツール呼び出し内のレイテンシ削減」という別の軸で最適化を行う。
両方を組み合わせることで、20回のツール呼び出しが必要なタスクでも、PTCで1回のコードブロックにまとめつつ、各ツールのパラメータストリーミングも高速化できる。
Agent SDKとの組み合わせ
Claude Agent SDKを使ってエージェントを構築する場合、ツール定義に eager_input_streaming: true を含めることで、エージェントのツール実行フェーズでもFine-Grained Streamingの恩恵を受けられる。Agent SDKのストリーミングモードでは TextBlock、ThinkingBlock、ToolUseBlock、ToolResultBlock といった型付きメッセージが逐次返されるため、ツールの入力パラメータをリアルタイムに処理するUIを構築しやすい。
まとめ
Fine-Grained Tool Streamingは、ツール定義に eager_input_streaming: true を1行追加するだけで、ツール呼び出しの初期レイテンシを15秒から3秒に短縮できる。導入コストに対してリターンが大きい機能だ。
ただし、部分JSONのハンドリングと max_tokens 到達時の処理は確実に実装しておく必要がある。partial-json-parser のようなライブラリを活用し、stop_reason のチェックを忘れなければ、安全に運用できる。
すべてのツールに一律で適用するのではなく、パラメータサイズとUI要件に応じて選択的に有効化するのが、実運用でのベストプラクティスだ。
