Function Calling

AI の関数呼び出し

Function Calling を分かりやすく

Function Calling(関数呼び出し)は、AI に「特殊能力」を与える技術です。

例え話をしましょう。あなたが秘書を雇ったとします:

通常の秘書: 知識と経験だけで答える

  • 「今日の天気は晴れだと思います」(記憶や推測に基づく)
  • 「株価は上がっているはずです」(不確実)

Function Calling を使う秘書: 必要に応じてツールを使う

  • 「天気 API を確認します...今日の天気は晴れで、気温は 25 度です」(正確)
  • 「株価 API を確認します...現在の株価は 1,234 円です」(リアルタイム)

Function Calling を使うと、AI は単なる「知識の塊」から、「実際に行動できるアシスタント」に変わります。データベースを検索したり、計算をしたり、メールを送ったり、API を呼び出したり――人間のアシスタントができることを、AI もできるようになるのです。

Function Calling の仕組み

Function Calling は以下のフローで動作します:

  1. 関数の定義: AI が使えるツール(関数)を定義
  2. AI が判断: ユーザーの質問に対して、どの関数を使うべきか AI が決定
  3. パラメータ抽出: 関数に渡すべきパラメータを AI が抽出
  4. 関数実行: アプリケーション側で実際に関数を実行
  5. 結果を返す: 実行結果を AI に渡して、最終的な回答を生成

Next.js × TypeScript で実装する

ステップ 1: 関数の定義

// lib/functions.ts
import { z } from 'zod'

// 天気情報を取得する関数
export const getWeatherFunction = {
  name: 'get_weather',
  description: '指定された都市の現在の天気情報を取得します',
  parameters: {
    type: 'object',
    properties: {
      city: {
        type: 'string',
        description: '都市名(例: 東京、大阪、福岡)',
      },
      unit: {
        type: 'string',
        enum: ['celsius', 'fahrenheit'],
        description: '温度の単位',
      },
    },
    required: ['city'],
  },
}

// データベースから製品を検索する関数
export const searchProductsFunction = {
  name: 'search_products',
  description: '製品データベースからキーワードで製品を検索します',
  parameters: {
    type: 'object',
    properties: {
      keyword: {
        type: 'string',
        description: '検索キーワード',
      },
      category: {
        type: 'string',
        description: '製品カテゴリ(オプション)',
        enum: ['electronics', 'clothing', 'food', 'books'],
      },
      maxResults: {
        type: 'number',
        description: '最大検索結果数',
        default: 10,
      },
    },
    required: ['keyword'],
  },
}

// 計算を実行する関数
export const calculateFunction = {
  name: 'calculate',
  description: '数式を計算します(加減乗除、累乗など)',
  parameters: {
    type: 'object',
    properties: {
      expression: {
        type: 'string',
        description: '計算式(例: "2 + 2", "10 * 5 + 3")',
      },
    },
    required: ['expression'],
  },
}

// 全ての関数を配列にまとめる
export const availableFunctions = [getWeatherFunction, searchProductsFunction, calculateFunction]

ステップ 2: 実際の関数実装

// lib/function-implementations.ts

type WeatherData = {
  city: string
  temperature: number
  condition: string
  humidity: number
}

// 天気情報取得(ダミー実装、実際は外部 API を使用)
export async function getWeather(city: string, unit: string = 'celsius'): Promise<WeatherData> {
  // 実際には OpenWeatherMap などの API を呼び出す
  return {
    city,
    temperature: unit === 'celsius' ? 25 : 77,
    condition: '晴れ',
    humidity: 60,
  }
}

type Product = {
  id: string
  name: string
  price: number
  category: string
}

// 製品検索
export async function searchProducts(
  keyword: string,
  category?: string,
  maxResults: number = 10
): Promise<Product[]> {
  // 実際にはデータベースやElasticsearchを使用
  const mockProducts: Product[] = [
    { id: '1', name: 'iPhone 15', price: 120000, category: 'electronics' },
    { id: '2', name: 'MacBook Pro', price: 250000, category: 'electronics' },
    { id: '3', name: 'AirPods Pro', price: 35000, category: 'electronics' },
  ]

  let results = mockProducts.filter((p) => p.name.toLowerCase().includes(keyword.toLowerCase()))

  if (category) {
    results = results.filter((p) => p.category === category)
  }

  return results.slice(0, maxResults)
}

// 計算実行
export function calculate(expression: string): number {
  // 安全な計算(eval は使わない)
  try {
    // 実際には math.js などのライブラリを使用
    const result = Function('"use strict"; return (' + expression + ')')()
    return result
  } catch (error) {
    throw new Error('無効な計算式です')
  }
}

// 関数名から実装へのマッピング
export async function executeFunctionCall(functionName: string, args: any): Promise<any> {
  switch (functionName) {
    case 'get_weather':
      return await getWeather(args.city, args.unit)

    case 'search_products':
      return await searchProducts(args.keyword, args.category, args.maxResults)

    case 'calculate':
      return calculate(args.expression)

    default:
      throw new Error(`Unknown function: ${functionName}`)
  }
}

ステップ 3: Function Calling API の実装

// app/api/chat/function-calling/route.ts
import OpenAI from 'openai'
import { availableFunctions } from '@/lib/functions'
import { executeFunctionCall } from '@/lib/function-implementations'

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})

export async function POST(req: Request) {
  try {
    const { messages } = await req.json()

    // 最初のAI呼び出し(関数を使うかどうか判断)
    let response = await openai.chat.completions.create({
      model: 'gpt-4',
      messages,
      functions: availableFunctions,
      function_call: 'auto', // AI が自動で判断
    })

    let responseMessage = response.choices[0].message

    // Function Calling が必要な場合
    if (responseMessage.function_call) {
      const functionName = responseMessage.function_call.name
      const functionArgs = JSON.parse(responseMessage.function_call.arguments)

      console.log(`AI が関数を呼び出しました: ${functionName}`, functionArgs)

      // 関数を実行
      const functionResult = await executeFunctionCall(functionName, functionArgs)

      console.log(`関数の実行結果:`, functionResult)

      // 関数の結果を AI に渡して最終的な回答を生成
      const secondResponse = await openai.chat.completions.create({
        model: 'gpt-4',
        messages: [
          ...messages,
          responseMessage,
          {
            role: 'function',
            name: functionName,
            content: JSON.stringify(functionResult),
          },
        ],
      })

      return Response.json({
        message: secondResponse.choices[0].message.content,
        functionCalled: functionName,
        functionArgs,
        functionResult,
      })
    }

    // Function Calling が不要な場合は通常の回答
    return Response.json({
      message: responseMessage.content,
      functionCalled: null,
    })
  } catch (error) {
    console.error('Function Calling Error:', error)
    return new Response('エラーが発生しました', { status: 500 })
  }
}

ステップ 4: フロントエンドの実装

// app/function-calling/page.tsx
'use client'

import { useState } from 'react'

type Message = {
  role: 'user' | 'assistant' | 'system'
  content: string
  functionCalled?: string
  functionResult?: any
}

export default function FunctionCallingPage() {
  const [messages, setMessages] = useState<Message[]>([])
  const [input, setInput] = useState('')
  const [isLoading, setIsLoading] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!input.trim() || isLoading) return

    const userMessage: Message = {
      role: 'user',
      content: input,
    }

    setMessages((prev) => [...prev, userMessage])
    setInput('')
    setIsLoading(true)

    try {
      const response = await fetch('/api/chat/function-calling', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          messages: [...messages, userMessage].map((m) => ({
            role: m.role,
            content: m.content,
          })),
        }),
      })

      const data = await response.json()

      const assistantMessage: Message = {
        role: 'assistant',
        content: data.message,
        functionCalled: data.functionCalled,
        functionResult: data.functionResult,
      }

      setMessages((prev) => [...prev, assistantMessage])
    } catch (error) {
      console.error('Error:', error)
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <div className="container mx-auto max-w-4xl p-4">
      <h1 className="text-3xl font-bold mb-6">Function Calling デモ</h1>

      {/* 使用可能な機能の説明 */}
      <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
        <h2 className="font-semibold mb-2">AI が使える機能:</h2>
        <ul className="list-disc list-inside space-y-1 text-sm">
          <li>天気情報の取得(例: 「東京の天気を教えて」)</li>
          <li>製品検索(例: 「iPhone を検索して」)</li>
          <li>計算(例:125 × 47 を計算して」)</li>
        </ul>
      </div>

      {/* メッセージ表示 */}
      <div className="bg-gray-50 rounded-lg p-4 h-[500px] overflow-y-auto mb-4">
        {messages.map((message, index) => (
          <div
            key={index}
            className={`mb-4 ${message.role === 'user' ? 'text-right' : 'text-left'}`}
          >
            <div
              className={`inline-block rounded-lg px-4 py-2 max-w-[80%] ${
                message.role === 'user'
                  ? 'bg-blue-500 text-white'
                  : 'bg-white border border-gray-200'
              }`}
            >
              <p className="text-xs mb-1">{message.role === 'user' ? 'あなた' : 'AI'}</p>
              <p className="whitespace-pre-wrap">{message.content}</p>

              {/* 関数呼び出し情報を表示 */}
              {message.functionCalled && (
                <div className="mt-2 pt-2 border-t border-gray-200">
                  <p className="text-xs text-gray-500">
                    🔧 使用した機能: <code>{message.functionCalled}</code>
                  </p>
                  <details className="mt-1">
                    <summary className="text-xs text-gray-500 cursor-pointer">実行結果を表示</summary>
                    <pre className="text-xs bg-gray-100 p-2 rounded mt-1 overflow-x-auto">
                      {JSON.stringify(message.functionResult, null, 2)}
                    </pre>
                  </details>
                </div>
              )}
            </div>
          </div>
        ))}
      </div>

      {/* 入力フォーム */}
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="メッセージを入力..."
          className="flex-1 border border-gray-300 rounded-lg px-4 py-2"
          disabled={isLoading}
        />
        <button
          type="submit"
          disabled={isLoading || !input.trim()}
          className="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 disabled:bg-gray-300"
        >
          {isLoading ? '送信中...' : '送信'}
        </button>
      </form>
    </div>
  )
}

高度な Function Calling パターン

複数の関数を連続で呼び出す

// AI が複数の関数を順番に呼び出すケース
export async function POST(req: Request) {
  const { messages } = await req.json()
  const conversationMessages = [...messages]

  let iterations = 0
  const maxIterations = 5 // 無限ループ防止

  while (iterations < maxIterations) {
    const response = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: conversationMessages,
      functions: availableFunctions,
      function_call: 'auto',
    })

    const responseMessage = response.choices[0].message

    // Function Calling がない場合は終了
    if (!responseMessage.function_call) {
      return Response.json({
        message: responseMessage.content,
        iterations,
      })
    }

    // Function Calling を実行
    const functionName = responseMessage.function_call.name
    const functionArgs = JSON.parse(responseMessage.function_call.arguments)
    const functionResult = await executeFunctionCall(functionName, functionArgs)

    // 会話履歴に追加
    conversationMessages.push(responseMessage)
    conversationMessages.push({
      role: 'function',
      name: functionName,
      content: JSON.stringify(functionResult),
    })

    iterations++
  }

  return new Response('最大反復回数に達しました', { status: 500 })
}

まとめ

Function Calling は、AI を「実際に行動できるアシスタント」に変える強力な技術です。Next.js と TypeScript を組み合わせることで、型安全で拡張可能な Function Calling システムを構築できます。

重要なポイント:

  1. 関数定義の明確化: 各関数の目的とパラメータを詳細に記述
  2. エラーハンドリング: 関数実行の失敗を適切に処理
  3. セキュリティ: 危険な関数の実行を制限
  4. ログ記録: どの関数がいつ呼ばれたかを記録
  5. 無限ループ防止: 関数呼び出しの回数を制限

Function Calling を活用することで、データベース統合、外部 API 連携、計算処理など、AI アプリケーションの可能性が大きく広がります。