1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenAI と Anthropic(Claude)の JSON スキーマ/ツール呼び出し/マルチモーダル/SSE 比較

Last updated at Posted at 2025-09-14

OpenAI と Anthropic(Claude)それぞれの LLM API における JSON スキーマ/構造化出力(structured JSON, function/tool calling, schema‐enforcement etc.)の違いを比較し用途や特徴なども含めて整理しました。

共通点

まず、両者に共通する特徴:

  • プロンプト内で期待する JSON スキーマを指定できる。
  • 「ツール呼び出し(tool use / function calling)」機能があり、モデルに「この関数(ツール)を使ってください/使う可能性があります」と伝えて、それに従った入力構造を生成させられる。
  • 複数のメッセージ履歴(user/assistant/system)を送れるチャット形式がある。
  • 出力の整合性を上げるためのモードやオプションが存在する。

主な違い

  • JSON スキーマ厳格性と Structured Outputs

    • OpenAI:2024 年以降「Structured Outputs」を導入。指定した JSON Schema に「完全一致」させるオプションがある。
    • Anthropic:スキーマ指定は可能だが、完全一致の保証は OpenAI ほど強くない。プロンプト設計の工夫が必要。
  • function / tool calling

    • OpenAI:tools(旧 functions)で parameters に JSON Schema。アシスタントが function 呼び出しを提案し、ツール結果は role:"tool" メッセージで返す。
    • Anthropic:toolsinput_schema を渡し、アシスタントは tool_use ブロックを返す。ツール結果は tool_result ブロックで返す。
  • 出力保証モード

    • OpenAI:strict な Structured Outputs、response_format などで厳密化。
    • Anthropic:JSON mode/prefill 等で整合性を高める設計。
  • 並列・選択

    • 両者とも複数ツール定義が可能で、モデルが選択可能。
  • 制約・注意

    • OpenAI:サポートする JSON Schema のサブセットや制約あり。
    • Anthropic:余分な前置き・テキストが混ざることがあるため、設計・プロンプト工夫が必要。
  • メッセージ構造

    • OpenAI:system/user/assistant ロール中心。assistant メッセージに tool_calls
    • Anthropic:コンテントブロック(text/image/tool_use/tool_result)中心。

OpenAI の例(Structured Outputs + function 呼び出し, strict)

POST /v1/chat/completions
{
  "model": "gpt-4o-2024-08-06",
  "messages": [
    { "role": "system", "content": "…あなたは有用なアシスタントです…" },
    { "role": "user", "content": "5月の注文で納品が遅れたものを全部見せてください" }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "query_orders",
        "description": "注文データベースから注文を検索する",
        "strict": true,
        "parameters": {
          "type": "object",
          "properties": {
            "month": { "type": "string", "enum": ["January", "February", ] },
            "year": { "type": "integer" }
          },
          "required": ["month","year"],
          "additionalProperties": false
        }
      }
    }
  ]
}

Anthropic の例(tool use, input_schema)

POST /v1/messages
{
  "model": "claude-3-5-sonnet-20240620",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "指定された場所の現在の天気を取得する",
      "input_schema": {
        "type": "object",
        "properties": {
          "location": { "type": "string", "description": "都市名など" },
          "unit": { "type": "string", "enum": ["celsius","fahrenheit"] }
        },
        "required": ["location","unit"],
        "additionalProperties": false
      }
    }
  ],
  "messages": [
    { "role": "user", "content": "東京の天気を摂氏で教えて" }
  ]
}

1) ツール実行結果通知用の API JSON(OpenAI / Anthropic)

まず結論(違いの一言メモ)

  • OpenAI(Chat Completions系): モデルが tool_calls[].id を出す → 開発者は 次のメッセージrole:"tool"tool_call_id を付けて結果を返す。厳密スキーマ強め(Structured Outputs)。
  • Anthropic(Messages API): モデルが content 内に {"type":"tool_use","id":...} を出す → 開発者は 次のメッセージ{"type":"tool_result","tool_use_id":...} ブロックをコンテントとして返す。ブロック型の設計で流れが明確。

OpenAI(Chat Completions API)版:tool_call → toolメッセージで結果返却

1.モデルがツール呼び出しを出す(抜粋・受信例)

{
  "id": "chatcmpl-xxx",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"location\":\"Tokyo\",\"unit\":\"celsius\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

2.開発者がツールを実行して、次のリクエストでツール結果を返す(Chat Completionsの messages に積む)

POST /v1/chat/completions
{
  "model": "gpt-4o-2024-08-06",
  "messages": [
    { "role": "system", "content": "Assistant that uses tools." },
    { "role": "user", "content": "東京の天気は?" },

    /* 直前の応答(assistant)が tool_calls を出していた前提で */
    {
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "{\"temp_c\":28,\"condition\":\"Cloudy\"}"
    }
  ]
}

ポイント

  • role: "tool" のメッセージで 必ず tool_call_id を一致させる
  • content は文字列(JSON文字列でもOK)。この後、最終回答用にもう一度 role:"assistant" の返答を出させるのが一般的
  • 厳密スキーマで最終回答もJSONにしたい場合は Structured Outputs(response_format: {"type":"json_schema", ... , "strict": true})を併用

Anthropic(Claude Messages API)版:tool_use → tool_resultブロックで結果返却

1.モデルがツール呼び出しを出す(抜粋・受信例)

{
  "id": "msg_abc",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_bdrk_012Jw4Ltu...",
      "name": "get_weather",
      "input": { "location": "Tokyo", "unit": "celsius" }
    }
  ],
  "stop_reason": "tool_use"
}

2.開発者がツールを実行して、次のリクエストtool_result ブロックを返す

POST /v1/messages
{
  "model": "claude-3-5-sonnet-20240620",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "指定された場所の現在の天気を取得する",
      "input_schema": {
        "type": "object",
        "properties": {
          "location": { "type": "string", "description": "都市名など" },
          "unit": { "type": "string", "enum": ["celsius","fahrenheit"] }
        },
        "required": ["location","unit"],
        "additionalProperties": false
      }
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "toolu_bdrk_012Jw4Ltu...",
          "content": [
            { "type": "text", "text": "{\"temp_c\":28,\"condition\":\"Cloudy\"}" }
          ],
          "is_error": false
        }
      ]
    }
  ]
}

ポイント

  • tool_use_id必ず前段の tool_use.id と一致させる
  • 結果は content の配列(テキスト以外のブロックも可)。必要なら is_error:true でエラーも返せる
  • Claudeは ツールを使ったら必ず対応する tool_result を返す必要があり、漏れるとエラーになる

2) マルチモーダル(画像など)も含められるか?

OpenAI も Anthropic も マルチモーダル(テキスト+画像など)をツール実行や JSON スキーマ連携に含めることが可能 です。ただし扱い方に違いがあります。

OpenAI の場合

  • 入力: messages[].content{"type":"text"}{"type":"image_url"} を混在可能。
  • ツール実行: 画像解析をツールに切り出し、JSON Schema で引数定義可能。Structured Outputs と併用で厳密化。

Anthropic の場合

  • 入力: messages[].content{"type":"image"} ブロックを含める(source:{type:"url"|"base64"})。
  • ツール実行: tool_useinput に画像 URL や Base64 を載せ、tool_result で結果(OCR テキストなど)を返却。

3) 画像を含む完全サンプル JSON(メッセージ履歴すべて含む)

どちらも「写真内の文字をOCRツールで抽出→要約して答える」想定

OpenAI(Chat Completions API)版

リクエスト #1(ユーザーが画像を送る、ツール定義つき)

POST /v1/chat/completions
{
  "model": "gpt-4o-2024-08-06",
  "temperature": 0,
  "messages": [
    { "role": "system", "content": "あなたは画像の内容を読み取り、必要に応じてツールを使います。" },
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "この写真のポスターに書いてある内容を要約してください。" },
        {
          "type": "image_url",
          "image_url": { "url": "https://example.com/poster.jpg" }
        }
      ]
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "ocr_image",
        "description": "画像URLを受け取り、OCRしたテキストを返す",
        "parameters": {
          "type": "object",
          "properties": {
            "image_url": { "type": "string", "description": "画像のURL" },
            "lang": { "type": "string", "enum": ["ja","en"], "description": "OCR対象言語" }
          },
          "required": ["image_url","lang"],
          "additionalProperties": false
        }
      }
    }
  ]
}

レスポンス #1(アシスタントがツール呼び出しを要求)

{
  "id": "chatcmpl-xxx",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "finish_reason": "tool_calls",
      "message": {
        "role": "assistant",
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "ocr_image",
              "arguments": "{\"image_url\":\"https://example.com/poster.jpg\",\"lang\":\"ja\"}"
            }
          }
        ]
      }
    }
  ]
}

リクエスト #2(ツールを実行して結果を tool ロールで通知 → 最終回答を促す)

POST /v1/chat/completions
{
  "model": "gpt-4o-2024-08-06",
  "temperature": 0,
  "messages": [
    { "role": "system", "content": "あなたは画像の内容を読み取り、必要に応じてツールを使います。" },
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "この写真のポスターに書いてある内容を要約してください。" },
        { "type": "image_url", "image_url": { "url": "https://example.com/poster.jpg" } }
      ]
    },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "call_abc123",
          "type": "function",
          "function": {
            "name": "ocr_image",
            "arguments": "{\"image_url\":\"https://example.com/poster.jpg\",\"lang\":\"ja\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "{\"text\":\"2025年技術カンファレンス開催。場所:東京ビッグサイト。日時:9/20-9/22。主なテーマ:AI、ロボティクス、データ基盤。事前登録で入場無料。\"}"
    }
  ]
}

レスポンス #2(最終回答)

{
  "id": "chatcmpl-yyy",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "要約:9/20〜9/22に東京ビッグサイトで技術カンファレンス開催。主なテーマはAI・ロボティクス・データ基盤。事前登録で入場無料です。"
      }
    }
  ]
}

Anthropic(Claude Messages API)版

リクエスト #1(ユーザーが画像を送る、ツール定義つき)

POST /v1/messages
{
  "model": "claude-3-5-sonnet-20240620",
  "max_tokens": 1024,
  "temperature": 0,
  "tools": [
    {
      "name": "ocr_image",
      "description": "画像URLを受け取り、OCRしたテキストを返す",
      "input_schema": {
        "type": "object",
        "properties": {
          "image_url": { "type": "string", "description": "画像のURL" },
          "lang": { "type": "string", "enum": ["ja","en"], "description": "OCR対象言語" }
        },
        "required": ["image_url","lang"],
        "additionalProperties": false
      }
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "この写真のポスターに書いてある内容を要約してください。" },
        {
          "type": "image",
          "source": { "type": "url", "url": "https://example.com/poster.jpg" }
        }
      ]
    }
  ]
}

レスポンス #1(アシスタントがツール呼び出しを要求:tool_use ブロック)

{
  "id": "msg_123",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_bdrk_012Jw4Lt",
      "name": "ocr_image",
      "input": {
        "image_url": "https://example.com/poster.jpg",
        "lang": "ja"
      }
    }
  ],
  "stop_reason": "tool_use"
}

リクエスト #2(ツールを実行して結果を tool_result ブロックで通知 → その後の回答を促す)

POST /v1/messages
{
  "model": "claude-3-5-sonnet-20240620",
  "max_tokens": 1024,
  "temperature": 0,
  "tools": [
    {
      "name": "ocr_image",
      "description": "画像URLを受け取り、OCRしたテキストを返す",
      "input_schema": {
        "type": "object",
        "properties": {
          "image_url": { "type": "string" },
          "lang": { "type": "string", "enum": ["ja","en"] }
        },
        "required": ["image_url","lang"],
        "additionalProperties": false
      }
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "この写真のポスターに書いてある内容を要約してください。" },
        { "type": "image", "source": { "type": "url", "url": "https://example.com/poster.jpg" } }
      ]
    },
    {
      "role": "assistant",
      "content": [
        {
          "type": "tool_use",
          "id": "toolu_bdrk_012Jw4Lt",
          "name": "ocr_image",
          "input": { "image_url": "https://example.com/poster.jpg", "lang": "ja" }
        }
      ]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "toolu_bdrk_012Jw4Lt",
          "content": [
            {
              "type": "text",
              "text": "{\"text\":\"2025年技術カンファレンス開催。場所:東京ビッグサイト。日時:9/20-9/22。主なテーマ:AI、ロボティクス、データ基盤。事前登録で入場無料。\"}"
            }
          ],
          "is_error": false
        }
      ]
    }
  ]
}

レスポンス #2(最終回答)

{
  "id": "msg_456",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "要約:9/20〜9/22に東京ビッグサイトで技術カンファレンス開催。主なテーマはAI・ロボティクス・データ基盤。事前登録で入場無料です。"
    }
  ],
  "stop_reason": "end_turn"
}

4) SSE(delta/差分)の結合サンプル

ストリーミング時に “tool_call / tool_use をデルタ結合して復元 → 実行 → 結果を再送” までを、OpenAI と Anthropic それぞれ TypeScript で実装した例。

OpenAI(Chat Completions / stream: true)

1) ツール呼び出しの受信(delta連結)→ 復元

OpenAIは choices[0].delta.tool_calls[] が配列インデックス付きで断片的に届きます。

id・function.name・function.arguments がバラバラに来るので、index をキーに結合します。

// openai-stream-toolcalls.ts
type OpenAIToolCallDelta = {
  index: number;
  id?: string;
  type?: "function";
  function?: { name?: string; arguments?: string }; // arguments は “部分文字列” が累積で来る
};

type OpenAIDeltaChunk =
  | { choices: [{ delta: { tool_calls?: OpenAIToolCallDelta[]; content?: string }, finish_reason?: string }] }
  | { choices: [{ delta: { role?: string } }] }
  | { choices: [{ delta: {} }]};

async function streamOpenAIAndCollectToolCalls() {
  const resp = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY!}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gpt-4o-2024-08-06",
      stream: true,
      messages: [
        { role: "system", content: "You are a tool-using assistant." },
        {
          role: "user",
          content: [
            { type: "text", text: "この写真のポスターの要約をして。必要ならOCRツールを使って。" },
            { type: "image_url", image_url: { url: "https://example.com/poster.jpg" } },
          ],
        },
      ],
      tools: [
        {
          type: "function",
          function: {
            name: "ocr_image",
            description: "OCR for an image url",
            parameters: {
              type: "object",
              properties: {
                image_url: { type: "string" },
                lang: { type: "string", enum: ["ja", "en"] },
              },
              required: ["image_url", "lang"],
              additionalProperties: false,
            },
          },
        },
      ],
    }),
  });

  if (!resp.ok || !resp.body) throw new Error(`OpenAI stream failed: ${resp.status}`);

  // index をキーに部分を蓄積
  const toolBuf = new Map<number, { id?: string; name?: string; args: string }>();
  let assistantText = "";

  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let sseBuffer = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    sseBuffer += decoder.decode(value, { stream: true });

    // SSEは \n\n 区切り。1イベントずつ処理
    let idx: number;
    while ((idx = sseBuffer.indexOf("\n\n")) !== -1) {
      const rawEvent = sseBuffer.slice(0, idx);
      sseBuffer = sseBuffer.slice(idx + 2);

      // "data: {json}" の行を抽出
      const dataLine = rawEvent
        .split("\n")
        .find((l) => l.startsWith("data: "));
      if (!dataLine) continue;

      const data = dataLine.slice("data: ".length).trim();
      if (data === "[DONE]") break;

      let json: OpenAIDeltaChunk;
      try {
        json = JSON.parse(data);
      } catch {
        continue;
      }

      const choice = (json as any).choices?.[0];
      const delta = choice?.delta ?? {};
      const finishReason = choice?.finish_reason;

      // テキスト回答(通常のcontent)のデルタ
      if (delta.content) assistantText += delta.content;

      // tool_calls デルタ
      if (delta.tool_calls) {
        for (const d of delta.tool_calls as OpenAIToolCallDelta[]) {
          const entry = toolBuf.get(d.index) ?? { args: "" };
          if (d.id) entry.id = d.id;
          if (d.function?.name) entry.name = d.function.name;
          if (typeof d.function?.arguments === "string") {
            entry.args += d.function.arguments; // ★ 連結が重要(部分文字列で来る)
          }
          toolBuf.set(d.index, entry);
        }
      }

      // tool_calls 完了シグナル
      if (finishReason === "tool_calls") {
        // ここで toolBuf の中身が “1回分のツール呼び出し群” として完成
        const toolCalls = [...toolBuf.entries()]
          .sort((a, b) => a[0] - b[0])
          .map(([, v]) => ({ id: v.id!, name: v.name!, argsJson: v.args }));

        return { toolCalls, assistantTextSoFar: assistantText };
      }
    }
  }

  // ツール不要で完了した場合
  return { toolCalls: [], assistantTextSoFar: assistantText };
}

// 使い方例
(async () => {
  const { toolCalls, assistantTextSoFar } = await streamOpenAIAndCollectToolCalls();
  if (toolCalls.length === 0) {
    console.log("ASSISTANT:", assistantTextSoFar);
    return;
  }

  // ここで実ツール実行(例: OCR)
  const results = await Promise.all(
    toolCalls.map(async (t) => {
      const parsed = JSON.parse(t.argsJson);
      // ダミーOCR結果
      const text = "2025年技術カンファレンス…(OCR結果)";
      return { tool_call_id: t.id, content: JSON.stringify({ text }) };
    })
  );

  // 次リクエストで tool 結果を返して最終回答を取得
  const final = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY!}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gpt-4o-2024-08-06",
      messages: [
        { role: "system", content: "You are a tool-using assistant." },
        {
          role: "user",
          content: [
            { type: "text", text: "この写真の要約を。" },
            { type: "image_url", image_url: { url: "https://example.com/poster.jpg" } },
          ],
        },
        // “ツール呼び出しを出した直前の assistant メッセージ” を最低限再現
        {
          role: "assistant",
          tool_calls: toolCalls.map((t) => ({
            id: t.id,
            type: "function",
            function: { name: t.name!, arguments: t.argsJson },
          })),
        },
        // ツール実行結果を通知
        ...results.map((r) => ({ role: "tool" as const, tool_call_id: r.tool_call_id, content: r.content })),
      ],
      temperature: 0,
    }),
  }).then((r) => r.json());

  console.log("FINAL:", final.choices?.[0]?.message?.content);
})();

ポイント

  • tool_calls[].index ごとに {id, name, args(partial)} を結合。
  • finish_reason: "tool_calls" を受け取ったら1ラウンド分が完成。
  • 以後、role:"tool" メッセージで tool_call_id を対応させて返送。

Anthropic(Messages / stream: true)

AnthropicのSSEはイベント名が細分化されます(例: message_start / content_block_start / content_block_delta / content_block_stop / message_stop など)。

type:"tool_use" ブロックの “input(JSON)” が partial_json として断片で届くので、ブロックIDごとに結合します。

1) ツール呼び出し(tool_use)の復元

// anthropic-stream-tooluse.ts
type AnthropicEvent =
  | { type: "message_start"; message: any }
  | { type: "content_block_start"; index: number; content_block: { type: string; id: string; name?: string } }
  | { type: "content_block_delta"; index: number; delta: { type: string; text?: string; partial_json?: string } }
  | { type: "content_block_stop"; index: number }
  | { type: "message_delta"; delta: any }
  | { type: "message_stop" }
  | { type: "error"; error: { message: string } };

async function streamClaudeAndCollectToolUses() {
  const resp = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": process.env.ANTHROPIC_API_KEY!,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-3-5-sonnet-20240620",
      temperature: 0,
      max_tokens: 1024,
      stream: true,
      tools: [
        {
          name: "ocr_image",
          description: "OCR for an image url",
          input_schema: {
            type: "object",
            properties: {
              image_url: { type: "string" },
              lang: { type: "string", enum: ["ja", "en"] },
            },
            required: ["image_url", "lang"],
            additionalProperties: false,
          },
        },
      ],
      messages: [
        {
          role: "user",
          content: [
            { type: "text", text: "この写真のポスターを要約して。必要ならOCRツールを使って。" },
            { type: "image", source: { type: "url", url: "https://example.com/poster.jpg" } },
          ],
        },
      ],
    }),
  });

  if (!resp.ok || !resp.body) throw new Error(`Claude stream failed: ${resp.status}`);

  // tool_use content block の id -> { name, jsonStr }
  const toolUseBuf = new Map<string, { name?: string; json: string }>();
  // content_block.id の逆引き(何番インデックスのブロックがどの id を持つか)
  const indexToBlockId = new Map<number, string>();
  let assistantText = "";

  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let sseBuffer = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    sseBuffer += decoder.decode(value, { stream: true });

    let idx: number;
    while ((idx = sseBuffer.indexOf("\n\n")) !== -1) {
      const rawEvent = sseBuffer.slice(0, idx);
      sseBuffer = sseBuffer.slice(idx + 2);

      // 複数行の SSE: "event: xxx" と "data: {...}"
      const lines = rawEvent.split("\n");
      const dataLine = lines.find((l) => l.startsWith("data: "));
      if (!dataLine) continue;

      const jsonStr = dataLine.slice("data: ".length).trim();
      if (jsonStr === "[DONE]") break;

      let ev: AnthropicEvent;
      try {
        ev = JSON.parse(jsonStr);
      } catch {
        continue;
      }

      switch (ev.type) {
        case "content_block_start": {
          // tool_use ブロック開始?
          const cb = ev.content_block as any;
          if (cb.type === "tool_use") {
            indexToBlockId.set(ev.index, cb.id);
            toolUseBuf.set(cb.id, { name: cb.name, json: "" }); // input は後続 delta で来る
          }
          break;
        }
        case "content_block_delta": {
          const id = indexToBlockId.get(ev.index);
          if (!id) {
            // テキストブロックの場合は text が入る
            if ((ev.delta as any).text) assistantText += (ev.delta as any).text;
            break;
          }
          const entry = toolUseBuf.get(id)!;
          // tool_use の input は partial_json として断片で来る
          if (ev.delta.partial_json) entry.json += ev.delta.partial_json;
          toolUseBuf.set(id, entry);
          break;
        }
        case "content_block_stop": {
          // ブロック閉じ。特に処理不要(完成は message_stop で判断)
          break;
        }
        case "message_stop": {
          // 1ターンぶんの tool_use 群を返す
          const calls = [...toolUseBuf.entries()].map(([id, v]) => ({
            id,
            name: v.name!,
            inputJson: v.json || "{}",
          }));
          return { toolUses: calls, assistantTextSoFar: assistantText };
        }
        case "error": {
          throw new Error(ev.error.message);
        }
      }
    }
  }

  return { toolUses: [], assistantTextSoFar: assistantText };
}

// 使い方例
(async () => {
  const { toolUses, assistantTextSoFar } = await streamClaudeAndCollectToolUses();
  if (toolUses.length === 0) {
    console.log("ASSISTANT:", assistantTextSoFar);
    return;
  }

  // ツール実行
  const results = await Promise.all(
    toolUses.map(async (u) => {
      const parsed = JSON.parse(u.inputJson);
      // ダミーOCR
      const text = "2025年技術カンファレンス…(OCR結果 by Claude flow)";
      return {
        tool_use_id: u.id,
        content: [{ type: "text" as const, text: JSON.stringify({ text }) }],
        is_error: false,
      };
    })
  );

  // 次リクエストで tool_result ブロックを返して最終応答
  const finalResp = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": process.env.ANTHROPIC_API_KEY!,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-3-5-sonnet-20240620",
      max_tokens: 1024,
      temperature: 0,
      tools: [
        {
          name: "ocr_image",
          input_schema: {
            type: "object",
            properties: { image_url: { type: "string" }, lang: { type: "string", enum: ["ja", "en"] } },
            required: ["image_url", "lang"],
          },
        },
      ],
      messages: [
        {
          role: "user",
          content: [
            { type: "text", text: "この写真の要約を。" },
            { type: "image", source: { type: "url", url: "https://example.com/poster.jpg" } },
          ],
        },
        {
          role: "assistant",
          content: toolUses.map((u) => ({
            type: "tool_use" as const,
            id: u.id,
            name: u.name,
            input: JSON.parse(u.inputJson),
          })),
        },
        {
          role: "user",
          content: results.map((r) => ({
            type: "tool_result" as const,
            tool_use_id: r.tool_use_id,
            content: r.content,
            is_error: r.is_error,
          })),
        },
      ],
    }),
  }).then((r) => r.json());

  const finalText = finalResp?.content?.map((b: any) => (b.type === "text" ? b.text : "")).join("");
  console.log("FINAL:", finalText);
})();

ポイント

  • content_block_start で tool_useブロックの id と name を取得。
  • content_block_delta.partial_json を追記結合して input を復元。
  • 後続リクエストで role:"user" の tool_result ブロックを返送。

5) 主な違い一覧

観点 OpenAI Anthropic(Claude)
メッセージ構造 role: system/user/assistantcontent(マルチパート:text, image_url など)。assistant.message.tool_calls[] にツール呼び出しが現れる messages[].contentブロック配列type: "text" / "image" / "tool_use" / "tool_result" など)。assistant.content[] 内の tool_use ブロックでツール呼び出し
ツール定義 tools: [{ type: "function", function: { name, description, parameters(JSON Schema) }}] tools: [{ name, description, input_schema(JSON Schema)}]
ツール結果の通知 次リクエストで role:"tool" メッセージを追加し、tool_call_id直前の assistant.tool_calls[].id に一致させる 次リクエストで role:"user"type:"tool_result" ブロックを入れ、tool_use_id直前の tool_use.id に一致させる
スキーマ準拠の厳格性 Structured Outputs により、strict: true で JSON Schema に厳密一致させやすい スキーマ遵守はできるが、厳密一致は設計・プロンプト工夫が必要
ストリーミング(SSE)でのツール呼び出しデルタ choices[0].delta.tool_calls[]index ごとに id/name/arguments 断片を連結) content_block_* イベント群。tool_use.inputpartial_jsonblock.id ごとに連結
マルチモーダル入力 content: [{type:"text"}, {type:"image_url"}] 混在 content: [{type:"text"}, {type:"image"}] 混在
失敗時の扱い [DONE] で終端。finish_reason 監視 error イベントをハンドル。message_stop で1ターン終了

6) 実装の落とし穴と対策(両API共通)

  1. 部分文字列の順序保証
  • OpenAI: tool_calls[].index で安定。indexごとに文字列連結
  • Anthropic: content_block.index → block.id をマップし、idごとに partial_json を連結
  1. JSON の途中断片での JSON.parse は厳禁
  • 最後にまとめて parse する(途中は文字列連結のみ)。
  1. ツール結果の再送は “直前の会話履歴を含める”
  • OpenAI: assistant(tool_calls)tool → 最終 assistant
  • Anthropic: assistant(tool_use)user(tool_result) → 最終 assistant
  1. ストリーム中に通常テキストが混じる
  • OpenAI: delta.content を別途バッファ。
  • Anthropic: content_block_delta.text を別途バッファ。
  1. エラー分岐
  • Anthropic: error イベントをハンドリング必須。
  • OpenAI: [DONE] で終了。finish_reason を監視。

この記事はChatGPTによって生成されたものです。

以上。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?