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:
toolsにinput_schemaを渡し、アシスタントはtool_useブロックを返す。ツール結果はtool_resultブロックで返す。
- OpenAI:
-
出力保証モード
- OpenAI:
strictな Structured Outputs、response_formatなどで厳密化。 - Anthropic:JSON mode/prefill 等で整合性を高める設計。
- OpenAI:
-
並列・選択
- 両者とも複数ツール定義が可能で、モデルが選択可能。
-
制約・注意
- OpenAI:サポートする JSON Schema のサブセットや制約あり。
- Anthropic:余分な前置き・テキストが混ざることがあるため、設計・プロンプト工夫が必要。
-
メッセージ構造
- OpenAI:
system/user/assistantロール中心。assistantメッセージにtool_calls。 - Anthropic:コンテントブロック(
text/image/tool_use/tool_result)中心。
- OpenAI:
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_useのinputに画像 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/assistant+content(マルチパート: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.input は partial_json を block.id ごとに連結 |
| マルチモーダル入力 |
content: [{type:"text"}, {type:"image_url"}] 混在 |
content: [{type:"text"}, {type:"image"}] 混在 |
| 失敗時の扱い |
[DONE] で終端。finish_reason 監視 |
error イベントをハンドル。message_stop で1ターン終了 |
6) 実装の落とし穴と対策(両API共通)
- 部分文字列の順序保証
- OpenAI:
tool_calls[].indexで安定。indexごとに文字列連結。 - Anthropic:
content_block.index → block.idをマップし、idごとにpartial_jsonを連結。
- JSON の途中断片での
JSON.parseは厳禁
- 最後にまとめて parse する(途中は文字列連結のみ)。
- ツール結果の再送は “直前の会話履歴を含める”
- OpenAI:
assistant(tool_calls)→tool→ 最終assistant。 - Anthropic:
assistant(tool_use)→user(tool_result)→ 最終assistant。
- ストリーム中に通常テキストが混じる
- OpenAI:
delta.contentを別途バッファ。 - Anthropic:
content_block_delta.textを別途バッファ。
- エラー分岐
- Anthropic:
errorイベントをハンドリング必須。 - OpenAI:
[DONE]で終了。finish_reasonを監視。
この記事はChatGPTによって生成されたものです。
以上。