ストーリー
ファンクションコーリングの仕組み
基本フロー
1. ツールのスキーマを定義
→ ツール名、説明、パラメータの型を JSON Schema で記述
2. LLM APIにスキーマと共にメッセージを送信
→ messages + tools(スキーマ一覧)を送信
3. LLMがツール呼び出しをJSON形式で返却
→ {tool_name, arguments} の構造化データ
4. アプリケーションがツールを実行
→ LLMの指定通りにツールを呼び出し
5. 結果をLLMに返却
→ ツールの実行結果をメッセージとして追加
6. LLMが最終回答を生成
→ ツールの結果を踏まえてユーザーに回答
OpenAI API での実装
ツールスキーマの定義
import OpenAI from "openai";
const tools: OpenAI.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "search_orders",
description: "注文番号、顧客ID、ステータスで注文情報を検索します。部分一致検索に対応。最大100件を返却します。",
parameters: {
type: "object",
properties: {
order_id: {
type: "string",
description: "注文番号(例: ORD-12345)"
},
customer_id: {
type: "string",
description: "顧客ID(例: CUST-001)"
},
status: {
type: "string",
enum: ["pending", "confirmed", "shipped", "delivered", "cancelled"],
description: "注文ステータス"
}
}
}
}
},
{
type: "function",
function: {
name: "track_shipment",
description: "追跡番号または注文番号で配送状況を追跡します。配送業者、現在地、配達予定日を返します。",
parameters: {
type: "object",
properties: {
tracking_number: {
type: "string",
description: "追跡番号"
},
order_id: {
type: "string",
description: "注文番号(追跡番号が不明な場合に使用)"
}
}
}
}
}
];
API呼び出しとツール実行ループ
async function runAgent(userMessage: string): Promise<string> {
const messages: OpenAI.ChatCompletionMessageParam[] = [
{
role: "system",
content: "あなたはNetShop社のカスタマーサポートエージェントです。"
},
{ role: "user", content: userMessage }
];
const MAX_ITERATIONS = 10;
for (let i = 0; i < MAX_ITERATIONS; i++) {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools,
tool_choice: "auto" // LLMが判断してツール呼び出しを行う
});
const message = response.choices[0].message;
messages.push(message);
// ツール呼び出しがない場合 → 最終回答
if (!message.tool_calls || message.tool_calls.length === 0) {
return message.content ?? "";
}
// ツール呼び出しを実行
for (const toolCall of message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
const result = await executeTool(toolCall.function.name, args);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify(result)
});
}
}
return "処理の最大回数に達しました。";
}
Anthropic Claude API での実装
ツール定義
import Anthropic from "@anthropic-ai/sdk";
const tools: Anthropic.Tool[] = [
{
name: "search_orders",
description: "注文番号、顧客ID、ステータスで注文情報を検索します。部分一致検索に対応。最大100件を返却します。",
input_schema: {
type: "object",
properties: {
order_id: {
type: "string",
description: "注文番号(例: ORD-12345)"
},
customer_id: {
type: "string",
description: "顧客ID(例: CUST-001)"
},
status: {
type: "string",
enum: ["pending", "confirmed", "shipped", "delivered", "cancelled"],
description: "注文ステータス"
}
}
}
}
];
API呼び出し
async function runClaudeAgent(userMessage: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage }
];
const MAX_ITERATIONS = 10;
for (let i = 0; i < MAX_ITERATIONS; i++) {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
system: "あなたはNetShop社のカスタマーサポートエージェントです。",
messages,
tools
});
// stop_reason が "end_turn" → 最終回答
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find(b => b.type === "text");
return textBlock?.text ?? "";
}
// stop_reason が "tool_use" → ツール呼び出し
if (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const result = await executeTool(block.name, block.input);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result)
});
}
}
messages.push({ role: "user", content: toolResults });
}
}
return "処理の最大回数に達しました。";
}
パラメータバリデーション
バリデーション層の実装
LLMが生成するパラメータは常に正しいとは限りません。実行前にバリデーションを行います。
import { z } from "zod";
// Zodスキーマでバリデーション定義
const SearchOrdersSchema = z.object({
order_id: z.string().regex(/^ORD-\d+$/).optional(),
customer_id: z.string().regex(/^CUST-\d+$/).optional(),
status: z.enum(["pending", "confirmed", "shipped", "delivered", "cancelled"]).optional()
});
async function executeTool(
name: string,
args: Record<string, unknown>
): Promise<ToolResponse> {
try {
switch (name) {
case "search_orders": {
const validated = SearchOrdersSchema.parse(args);
return await searchOrders(validated);
}
// ... 他のツール
default:
return { success: false, error: { code: "UNKNOWN_TOOL", message: `不明なツール: ${name}` } };
}
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: {
code: "VALIDATION_ERROR",
message: `パラメータエラー: ${error.errors.map(e => e.message).join(", ")}`,
suggestion: "パラメータの形式を確認して再試行してください"
}
};
}
throw error;
}
}
tool_choice オプション
LLMにツール呼び出しの判断をどこまで任せるかを制御できます。
| オプション | 動作 | 使い所 |
|---|---|---|
auto | LLMが判断(呼ぶ/呼ばない) | 通常はこれを使用 |
required | 必ずいずれかのツールを呼ぶ | ツール使用を強制したい場合 |
none | ツールを呼ばない | テキスト回答のみ欲しい場合 |
| 特定ツール指定 | 指定したツールを必ず呼ぶ | 特定の処理を強制したい場合 |
まとめ
| ポイント | 内容 |
|---|---|
| ファンクションコーリング | LLMがJSON形式で構造化されたツール呼び出しを出力する仕組み |
| スキーマ定義 | JSON Schemaでツールのインターフェースを厳密に定義 |
| 実行ループ | ツール呼び出し → 実行 → 結果返却 → 次の判断のサイクル |
| バリデーション | LLMの出力を信頼せず、実行前にパラメータを検証 |
チェックリスト
- ファンクションコーリングの基本フローを理解した
- OpenAI / Claude APIでのツール定義方法を把握した
- ツール実行ループの実装パターンを理解した
- パラメータバリデーションの重要性と実装方法を理解した
次のステップへ
次は「ツール選択戦略」を学びます。複数のツールからLLMが適切なものを選ぶための説明文設計やコンテキスト管理を理解しましょう。
推定読了時間: 30分