Function Calling入門|LLMからツール呼出

Claude Code活用

※ この記事にはアフィリエイトリンクが含まれています。詳細はプライバシーポリシーをご確認ください。

「LLMにDB検索や計算をさせたいが、毎回ハルシネーションで困る」という悩みはエンジニアなら誰しも通る道です。解決策の主役がFunction Calling(Tool Use)です。

この記事は、コードが書けるエンジニア向けにFunction Callingの仕組みをコード付きで整理します。JSON Schemaでのツール定義、単発呼出、並列呼出、ループによる自律処理までを順に追います。OpenAIとAnthropicの実装差も並べて比較します。

僕は普段、Claude APIを業務スクリプトに組み込んでツール連携を実装しています。最初はループ設計を雑にして無限呼出で課金が跳ねた経験もありました。本記事はそのハマりどころを潰した実装ガイドです。

本記事はAI学び直し|エンジニアの実務スキル4階層シリーズ第5回(第3階層:AIアプリ開発)にあたります。前回のRAG入門と合わせて読むと、LLMの外部連携の全体像がつかめます。

こんな方に読んでほしい

  • LLMから外部APIやDBを呼び出す仕組みを実装で理解したい方
  • OpenAI/Anthropic APIのツール呼出を比較検討している方
  • AIエージェントの基礎となる「ツール使用ループ」を体系的に学びたい方
  • Function Callingでハマりやすい設計ミスを事前に潰したい方

Function Calling(Tool Use)とは

結論:LLMが関数呼出を判断し、引数を生成する仕組み

Function Callingとは、LLMに「使える関数の一覧」を渡し、必要なときに関数名と引数をJSONで返してもらう仕組みです。実際の関数実行は開発者側のコードで行います。

OpenAIは「Function Calling」、Anthropicは「Tool Use」と呼びますが、本質は同じです。LLMはテキスト生成だけでなく、構造化されたツール呼出指示を返せるようになります。

なぜ必要か:LLM単体の限界を超えるため

LLMは学習時点までの知識しか持たず、最新情報やDB内データには直接アクセスできません。ハルシネーションも避けられません。

Function Callingを使うと、「天気APIを叩く」「DBを検索する」「メールを送る」といった実世界の操作をLLMから安全にトリガできます。LLMは判断と引数生成に集中し、実行は型のあるコードに任せる、という役割分担が明確になります。

典型的なユースケースは次の3つです。どれも「LLMだけでは絶対にできない」処理を補完する形になっています。

  • リアルタイム情報取得:天気・株価・為替のように刻々変わるデータをAPI経由で取得
  • 社内データ照会:ユーザーDB、案件管理、在庫情報のように学習データに含まれない閉域情報を検索
  • 副作用のある操作:メール送信、Slack通知、ファイル更新のように実世界に影響を与える処理

最新の仕様は公式ドキュメントが一次情報です。OpenAI Function calling guideAnthropic Tool use ドキュメントを併読すると、各社の差分が見えてきます。

ツール定義と呼出の基本

Function Callingの全体像をシーケンスで把握しておくと、各APIのドキュメントが読みやすくなります。

Function Callingのシーケンス|ツール定義→LLM呼出→引数生成→実行→結果フィードバック→最終応答

JSON Schemaでツールを定義する

ツールはJSON Schemaで定義します。関数名・説明・引数の型の3点が最重要です。

const tools = [
  {
    name: "get_weather",
    description: "指定した都市の現在の天気を取得します。" +
                 "都市名は日本語、英語どちらでも受け付けます。",
    input_schema: {
      type: "object",
      properties: {
        city: {
          type: "string",
          description: "天気を取得したい都市名(例:東京、Tokyo)"
        },
        unit: {
          type: "string",
          enum: ["celsius", "fahrenheit"],
          description: "温度の単位"
        }
      },
      required: ["city"]
    }
  }
];

descriptionはLLMがツール選択の判断材料にする最重要フィールドです。曖昧に書くと誤呼出が増えます。「いつ使うか」「何を返すか」を1〜2文で具体的に書きます。

僕の経験では、descriptionの一行追加で誤呼出率が体感で半分以下になります。コードレビューと同じく、「未来の自分」と「LLM」の両方が読むつもりで書くと自然と精度が上がります。

LLMと開発者の役割分担

役割分担を整理すると次の通りです。両者の境界を意識すると設計が安定します。

  • LLM側:ツールを使うかの判断、どのツールを使うかの選択、引数のJSON生成
  • 開発者側:実際の関数実行、結果のLLMへのフィードバック、ループの制御

実装パターン(単発/並列/ループ)

単発呼出:1ツールを1回呼ぶ最小構成

最小構成は「LLMに質問→ツール呼出指示→実行→結果を返す→最終応答」の流れです。Anthropic SDKで書くと次のようになります。

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();

// 1回目:LLMがツール使用を指示
const res1 = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools,
  messages: [{ role: "user", content: "東京の天気を教えて" }],
});

// 2回目:ツール実行結果を渡して最終応答を得る
const toolUse = res1.content.find(b => b.type === "tool_use");
const result = await getWeather(toolUse.input.city);

const res2 = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools,
  messages: [
    { role: "user", content: "東京の天気を教えて" },
    { role: "assistant", content: res1.content },
    { role: "user", content: [{
      type: "tool_result",
      tool_use_id: toolUse.id,
      content: JSON.stringify(result),
    }]},
  ],
});

ポイントは2回目のリクエストで1回目のassistantメッセージをそのまま含めることです。ここを省くとtool_use_idの整合性が取れずエラーになります。

このエラー、僕も最初はハマりました。assistantメッセージを省略してuserとtool_resultだけ送ると、Anthropic側で「対応するtool_useブロックがない」と弾かれます。会話履歴はLLMの思考プロセスを含めて完全に再現するのがFunction Callingの基本です。

並列ツール呼出:複数ツールを同時に呼ぶ

「東京と大阪の天気を同時に教えて」のような指示には、LLMが複数のtool_useブロックを1度に返します。これを並列実行で捌くと効率的です。

const toolUses = res1.content.filter(b => b.type === "tool_use");

// 並列実行
const results = await Promise.all(
  toolUses.map(async (tu) => ({
    type: "tool_result",
    tool_use_id: tu.id,
    content: JSON.stringify(await dispatch(tu.name, tu.input)),
  }))
);

// すべての結果をまとめてLLMに返す
const res2 = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools,
  messages: [
    { role: "user", content: "東京と大阪の天気を比較して" },
    { role: "assistant", content: res1.content },
    { role: "user", content: results },
  ],
});

OpenAIもparallel_tool_calls: true(既定値)で同様の挙動になります。順次実行と比べてレイテンシが大きく下がります。

並列実行で注意したいのはツール間の依存関係です。「都市Aの天気を見て、Bと比較する」のように後段が前段の結果に依存する場合、LLMは自動的に並列ではなく逐次で呼び出します。並列が効くのは独立したタスクに限る、という前提を頭に入れておきます。

ループでの自律処理:簡易Agent的な使い方

1往復で終わらないタスク(検索→絞込→詳細取得など)は、「stop_reasonがtool_useでなくなるまでループ」する設計が基本です。

async function runAgent(userInput: string, maxSteps = 10) {
  const messages = [{ role: "user", content: userInput }];

  for (let step = 0; step < maxSteps; step++) {
    const res = await client.messages.create({
      model: "claude-opus-4-7",
      max_tokens: 2048,
      tools,
      messages,
    });

    messages.push({ role: "assistant", content: res.content });

    if (res.stop_reason !== "tool_use") {
      return res.content; // 最終応答
    }

    // ツール実行結果を追加
    const toolUses = res.content.filter(b => b.type === "tool_use");
    const toolResults = await Promise.all(
      toolUses.map(async (tu) => ({
        type: "tool_result",
        tool_use_id: tu.id,
        content: JSON.stringify(await dispatch(tu.name, tu.input)),
      }))
    );
    messages.push({ role: "user", content: toolResults });
  }

  throw new Error(`Max steps (${maxSteps}) reached`);
}

maxStepsの設定は必須です。これを忘れると、LLMが延々とツールを呼び続けて課金が跳ねます。実装初期は5〜10程度に絞り、運用しながら調整するのが安全です。

僕は最初の検証で停止条件を雑にして、1スクリプトで20ドル分のトークンを溶かしたことがあります。maxStepsはコード側のサーキットブレーカーと捉えて、LLMの判断ミスや誤プロンプトに耐える設計にしておくのが現実解です。

OpenAI/Anthropic比較

主要2APIの仕様差を表で整理します。コードを移植するときの参考にしてください。

項目 OpenAI Anthropic
呼称 Function Calling Tool Use
スキーマ定義 parameters(JSON Schema) input_schema(JSON Schema)
並列呼出 parallel_tool_calls既定ON 既定で複数tool_use返却可
Strict mode strict: trueでスキーマ厳格 非対応(自動でほぼ厳格)
停止判定 finish_reason === “tool_calls” stop_reason === “tool_use”

使い分けの目安

選定基準は次の通りです。両方使えるよう抽象化しておくのが現実的です。

  • OpenAI:Strict modeでスキーマ違反を構造的に防ぎたい/既存のOpenAI資産が多い場合
  • Anthropic:長い推論を伴うAgent的処理/コードや構造化出力の精度を最優先する場合

Strict modeとJSON modeの関係

OpenAIのStrict modeは、引数JSONがスキーマに完全準拠することを保証する機能です。JSON mode(出力全体をJSON固定)とは別物で、Function Callingに特化しています。

Anthropicは公式のStrict modeはありませんが、実運用上は引数のスキーマ違反がほぼ起きません。ただしrequiredに書いた必須項目の漏れチェックは、開発者側でも入れておくのが安全です。

よくある失敗パターン

実装初期にハマりやすいポイントを3つに絞って整理します。事前に知っておくとデバッグ時間が大幅に減ります。

失敗1:descriptionが曖昧で誤呼出が起きる

「データを取得する」のような曖昧なdescriptionだと、LLMが似たツール間で混乱します。「いつ使うか/何を返すか/使うべきでない場面」を具体的に書くのが定石です。

// ❌ 曖昧
description: "ユーザー情報を取得"

// ✅ 具体的
description: "ユーザーIDを指定して、氏名・メール・登録日を返します。" +
             "メールアドレスからの逆引きはできません。" +
             "退会済みユーザーの場合は404を返します。"

失敗2:JSON Schemaが緩く型不整合が起きる

引数の型をstringでひとくくりにすると、LLMが日付や数値を文字列で返してきて後段でパースエラーになります。enumformatを活用して制約を強めるのが有効です。

  • 列挙型enum: ["pending", "approved", "rejected"]
  • 日付type: "string", format: "date-time"
  • 数値範囲type: "integer", minimum: 1, maximum: 100

スキーマを厳しくしすぎると、LLMがツール選択自体を諦めるケースもあります。最初は緩めから始め、誤呼出が起きた箇所だけ絞り込む順序が安全です。

失敗3:ループの停止条件が未設計で無限ループ

maxStepsを設けずwhile (toolUse) { ... }のように書くと、LLMが同じツールを何度も呼び続けて停止しないケースがあります。

停止条件は二重に張るのが安全です。ステップ数上限同一ツール連続呼出の上限を組み合わせると暴走を防げます。

// 同一ツール連続呼出の検知例
const lastTwoTools = recentToolNames.slice(-2);
if (lastTwoTools[0] === lastTwoTools[1]
    && JSON.stringify(lastTwoArgs[0]) === JSON.stringify(lastTwoArgs[1])) {
  throw new Error("同一引数の連続呼出を検知");
}

本番投入前のチェックリスト

失敗パターンを潰したうえで、本番投入の前に僕がいつも確認している5項目を共有します。

  • idempotency:同じ引数で2回呼ばれても安全か(重複メール・二重課金の防止)
  • 権限スコープ:LLMに渡すAPIキーやDB接続が、必要最小権限になっているか
  • ログ:tool_useブロックと実行結果をすべて構造化ログに残しているか
  • レート制限:外部APIのrate limitに引っかかったときの再試行戦略があるか
  • 失敗時の応答:ツール実行失敗をLLMに伝える形式が統一されているか

5項目とも「あって当然」のレベルですが、検証コードからそのまま本番に流れると抜けがちです。コードレビューのチェックリストに加えておくと事故が減ります。特にidempotency権限スコープは、LLMに任せる範囲が広がるほど効いてきます。

Function Callingの設計はここまで押さえれば実用に耐えます。次のステップとしてAgentパターン(自律ループ)に進む前段階として、Function Calling単体での運用経験を積むのが順番として安全です。

もっと深く学ぶなら

そもそもPythonの素振りが足りないなら、いきなりLLM連携に飛び込むより、月額制で課題ドリブンに進めるテックジムが選択肢になります。無料カウンセリングとPython入門講座の体験があるので、合うかの判断もできます。

  • デメリット:教材は動画ではなく課題ドリブンで自走型
  • メリット:月額制で短期離脱可能/Python基礎を実装で学べる

テックジムの無料カウンセリングを予約する

シリーズの位置づけと次回予告

本記事はAI学び直し|エンジニアの実務スキル4階層シリーズ第5回です。第3階層「AIアプリ開発」のうち、ツール連携の基礎にあたるFunction Callingを取り上げました。

前回はRAG入門で外部知識の参照方法を扱いました。Function Callingが「実世界の操作」、RAGが「知識の参照」と整理すると役割分担が見えやすくなります。

次回(5月7日公開予定)はAIエージェントを扱います。本記事のループ実装をベースに、計画立案・自己修正・複数ツール統合まで踏み込みます。

まとめ|判断はLLM、実行はコードに任せる

Function Callingの本質は、「判断と引数生成はLLM、実行は型のあるコード」という役割分担です。JSON Schemaでツールを定義し、tool_useブロックを受け取ったら実行して結果を返す、というシンプルな往復を理解すれば、応用は大きく広がります。

並列呼出でレイテンシを削り、ループ設計で複雑タスクに対応し、descriptionとスキーマを丁寧に書く。この3点を押さえるだけで、業務スクリプトに組み込んだときの安定性が大きく変わります。maxStepsの設定だけは絶対に忘れないでください。

明日からの実装では、まず1ツールの単発呼出を動かし、次に並列、最後にループの順で広げると遠回りしません。ツール定義のdescriptionだけ妥協せず書き込むのがコツです。

進展があったらこのブログで共有します。Agent的な複数ツール統合の実例についても、できたら定点観測の記事を書いてみたいと考えています。

コメント

タイトルとURLをコピーしました