最終更新:

AI Function Calling実践ガイド: LLMとツール連携でインテリジェントなアプリ構築


AI Function Calling実践ガイド: LLMとツール連携でインテリジェントなアプリ構築

Function Calling(関数呼び出し)は、LLMが外部ツールやAPIを呼び出せるようにする機能です。この記事では、OpenAI、Anthropic Claude、GeminiなどのFunction Calling実装を完全解説します。

Function Callingとは

Function Callingは、LLMが構造化された関数呼び出しを生成し、外部システムと連携できるようにする機能です。これにより、LLMは最新情報の取得、データベース操作、外部APIの実行など、様々なタスクを実行できます。

主な用途

  • データ取得: 天気情報、株価、ニュースなど
  • 操作実行: メール送信、カレンダー登録、タスク作成
  • 計算処理: 複雑な計算、データ分析
  • 外部連携: CRM、データベース、サードパーティAPI

OpenAI Function Calling

基本的な実装

// src/openai/function-calling.ts
import OpenAI from 'openai';

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

// 関数の定義
const tools: OpenAI.ChatCompletionTool[] = [
  {
    type: 'function',
    function: {
      name: 'get_weather',
      description: '指定された場所の現在の天気を取得します',
      parameters: {
        type: 'object',
        properties: {
          location: {
            type: 'string',
            description: '都市名(例: 東京、大阪)',
          },
          unit: {
            type: 'string',
            enum: ['celsius', 'fahrenheit'],
            description: '温度の単位',
          },
        },
        required: ['location'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'search_web',
      description: 'Web検索を実行して最新情報を取得します',
      parameters: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: '検索クエリ',
          },
          max_results: {
            type: 'number',
            description: '取得する結果の最大数',
            default: 5,
          },
        },
        required: ['query'],
      },
    },
  },
];

// 実際の関数実装
async function getWeather(location: string, unit: string = 'celsius'): Promise<string> {
  // 実際のAPIを呼び出す(ここではダミーデータ)
  const weatherData = {
    location,
    temperature: unit === 'celsius' ? 22 : 72,
    condition: 'sunny',
    humidity: 60,
  };

  return JSON.stringify(weatherData);
}

async function searchWeb(query: string, maxResults: number = 5): Promise<string> {
  // 実際の検索APIを呼び出す(ここではダミーデータ)
  const results = Array.from({ length: maxResults }, (_, i) => ({
    title: `Result ${i + 1} for "${query}"`,
    url: `https://example.com/result${i + 1}`,
    snippet: `This is a snippet for result ${i + 1}`,
  }));

  return JSON.stringify(results);
}

// Function Callingの実行
export async function runConversation(userMessage: string): Promise<string> {
  const messages: OpenAI.ChatCompletionMessageParam[] = [
    { role: 'user', content: userMessage },
  ];

  // 最初のAPI呼び出し
  let response = await openai.chat.completions.create({
    model: 'gpt-4-turbo-preview',
    messages,
    tools,
    tool_choice: 'auto',
  });

  let responseMessage = response.choices[0].message;

  // Function Callingのループ
  while (responseMessage.tool_calls) {
    messages.push(responseMessage);

    // 各tool callを実行
    for (const toolCall of responseMessage.tool_calls) {
      const functionName = toolCall.function.name;
      const functionArgs = JSON.parse(toolCall.function.arguments);

      console.log(`Calling function: ${functionName}`, functionArgs);

      let functionResponse: string;

      // 関数を実行
      switch (functionName) {
        case 'get_weather':
          functionResponse = await getWeather(
            functionArgs.location,
            functionArgs.unit
          );
          break;
        case 'search_web':
          functionResponse = await searchWeb(
            functionArgs.query,
            functionArgs.max_results
          );
          break;
        default:
          functionResponse = JSON.stringify({ error: 'Unknown function' });
      }

      // 関数の結果をメッセージに追加
      messages.push({
        role: 'tool',
        tool_call_id: toolCall.id,
        content: functionResponse,
      });
    }

    // 関数の結果を含めて再度API呼び出し
    response = await openai.chat.completions.create({
      model: 'gpt-4-turbo-preview',
      messages,
      tools,
    });

    responseMessage = response.choices[0].message;
  }

  return responseMessage.content || 'No response';
}

// 使用例
const result = await runConversation('東京の天気を教えてください');
console.log(result);

Anthropic Claude Tool Use

Claudeでは「Tool Use」という名前でFunction Calling機能を提供しています。

// src/claude/tool-use.ts
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

// ツールの定義
const tools: Anthropic.Tool[] = [
  {
    name: 'get_weather',
    description: '指定された場所の現在の天気を取得します',
    input_schema: {
      type: 'object',
      properties: {
        location: {
          type: 'string',
          description: '都市名(例: 東京、大阪)',
        },
        unit: {
          type: 'string',
          enum: ['celsius', 'fahrenheit'],
          description: '温度の単位',
        },
      },
      required: ['location'],
    },
  },
  {
    name: 'calculate',
    description: '数式を評価して計算結果を返します',
    input_schema: {
      type: 'object',
      properties: {
        expression: {
          type: 'string',
          description: '計算式(例: "2 + 2 * 3")',
        },
      },
      required: ['expression'],
    },
  },
];

// ツールの実装
async function executeTools(toolName: string, toolInput: any): Promise<string> {
  switch (toolName) {
    case 'get_weather': {
      const { location, unit = 'celsius' } = toolInput;
      const weatherData = {
        location,
        temperature: unit === 'celsius' ? 22 : 72,
        condition: 'sunny',
        humidity: 60,
      };
      return JSON.stringify(weatherData);
    }

    case 'calculate': {
      const { expression } = toolInput;
      try {
        // 注意: evalは危険なので、実際にはmath.jsなどを使用
        const result = eval(expression);
        return JSON.stringify({ result });
      } catch (error) {
        return JSON.stringify({ error: 'Invalid expression' });
      }
    }

    default:
      return JSON.stringify({ error: 'Unknown tool' });
  }
}

// Tool Useの実行
export async function runConversationWithClaude(userMessage: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: 'user', content: userMessage },
  ];

  let response = await anthropic.messages.create({
    model: 'claude-3-5-sonnet-20241022',
    max_tokens: 1024,
    messages,
    tools,
  });

  console.log('Initial response:', response);

  // Tool Useのループ
  while (response.stop_reason === 'tool_use') {
    // ツール使用の結果を収集
    const toolResults: Anthropic.MessageParam[] = [];

    for (const content of response.content) {
      if (content.type === 'tool_use') {
        console.log(`Using tool: ${content.name}`, content.input);

        const toolResult = await executeTools(content.name, content.input);

        toolResults.push({
          role: 'user',
          content: [
            {
              type: 'tool_result',
              tool_use_id: content.id,
              content: toolResult,
            },
          ],
        });
      }
    }

    // アシスタントのレスポンスを追加
    messages.push({
      role: 'assistant',
      content: response.content,
    });

    // ツール結果を追加
    messages.push(...toolResults);

    // 再度API呼び出し
    response = await anthropic.messages.create({
      model: 'claude-3-5-sonnet-20241022',
      max_tokens: 1024,
      messages,
      tools,
    });

    console.log('Follow-up response:', response);
  }

  // 最終的なテキスト応答を抽出
  const textContent = response.content.find(c => c.type === 'text');
  return textContent?.type === 'text' ? textContent.text : 'No response';
}

// 使用例
const result = await runConversationWithClaude('東京の天気を教えて、その気温を華氏に変換して');
console.log(result);

実践的なユースケース

1. データベース操作エージェント

// src/agents/database-agent.ts
import OpenAI from 'openai';
import { createClient } from '@supabase/supabase-js';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_KEY!
);

const tools: OpenAI.ChatCompletionTool[] = [
  {
    type: 'function',
    function: {
      name: 'query_users',
      description: 'ユーザーデータベースを検索します',
      parameters: {
        type: 'object',
        properties: {
          filters: {
            type: 'object',
            description: 'フィルター条件',
            properties: {
              email: { type: 'string' },
              role: { type: 'string' },
              created_after: { type: 'string' },
            },
          },
          limit: {
            type: 'number',
            description: '取得する最大件数',
            default: 10,
          },
        },
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'create_user',
      description: '新しいユーザーを作成します',
      parameters: {
        type: 'object',
        properties: {
          email: { type: 'string' },
          name: { type: 'string' },
          role: { type: 'string', enum: ['admin', 'user'] },
        },
        required: ['email', 'name'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'update_user',
      description: 'ユーザー情報を更新します',
      parameters: {
        type: 'object',
        properties: {
          user_id: { type: 'string' },
          updates: {
            type: 'object',
            properties: {
              name: { type: 'string' },
              role: { type: 'string' },
            },
          },
        },
        required: ['user_id', 'updates'],
      },
    },
  },
];

async function queryUsers(filters: any, limit: number = 10) {
  let query = supabase.from('users').select('*').limit(limit);

  if (filters.email) {
    query = query.eq('email', filters.email);
  }
  if (filters.role) {
    query = query.eq('role', filters.role);
  }
  if (filters.created_after) {
    query = query.gte('created_at', filters.created_after);
  }

  const { data, error } = await query;
  if (error) throw error;

  return JSON.stringify(data);
}

async function createUser(email: string, name: string, role: string = 'user') {
  const { data, error } = await supabase
    .from('users')
    .insert([{ email, name, role }])
    .select();

  if (error) throw error;
  return JSON.stringify(data);
}

async function updateUser(userId: string, updates: any) {
  const { data, error } = await supabase
    .from('users')
    .update(updates)
    .eq('id', userId)
    .select();

  if (error) throw error;
  return JSON.stringify(data);
}

export async function databaseAgent(userQuery: string): Promise<string> {
  const messages: OpenAI.ChatCompletionMessageParam[] = [
    {
      role: 'system',
      content: 'あなたはデータベース操作を支援するアシスタントです。ユーザーの自然言語での要求を適切なデータベース操作に変換します。',
    },
    { role: 'user', content: userQuery },
  ];

  let response = await openai.chat.completions.create({
    model: 'gpt-4-turbo-preview',
    messages,
    tools,
    tool_choice: 'auto',
  });

  let responseMessage = response.choices[0].message;

  while (responseMessage.tool_calls) {
    messages.push(responseMessage);

    for (const toolCall of responseMessage.tool_calls) {
      const functionName = toolCall.function.name;
      const functionArgs = JSON.parse(toolCall.function.arguments);

      let functionResponse: string;

      try {
        switch (functionName) {
          case 'query_users':
            functionResponse = await queryUsers(
              functionArgs.filters || {},
              functionArgs.limit
            );
            break;
          case 'create_user':
            functionResponse = await createUser(
              functionArgs.email,
              functionArgs.name,
              functionArgs.role
            );
            break;
          case 'update_user':
            functionResponse = await updateUser(
              functionArgs.user_id,
              functionArgs.updates
            );
            break;
          default:
            functionResponse = JSON.stringify({ error: 'Unknown function' });
        }
      } catch (error) {
        functionResponse = JSON.stringify({ error: (error as Error).message });
      }

      messages.push({
        role: 'tool',
        tool_call_id: toolCall.id,
        content: functionResponse,
      });
    }

    response = await openai.chat.completions.create({
      model: 'gpt-4-turbo-preview',
      messages,
      tools,
    });

    responseMessage = response.choices[0].message;
  }

  return responseMessage.content || 'No response';
}

// 使用例
const result = await databaseAgent('管理者権限を持つユーザーを全員検索してください');
console.log(result);

2. マルチツールエージェント

// src/agents/multi-tool-agent.ts
import OpenAI from 'openai';

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

const tools: OpenAI.ChatCompletionTool[] = [
  {
    type: 'function',
    function: {
      name: 'send_email',
      description: 'メールを送信します',
      parameters: {
        type: 'object',
        properties: {
          to: { type: 'string', description: '送信先メールアドレス' },
          subject: { type: 'string', description: '件名' },
          body: { type: 'string', description: '本文' },
        },
        required: ['to', 'subject', 'body'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'create_calendar_event',
      description: 'カレンダーにイベントを追加します',
      parameters: {
        type: 'object',
        properties: {
          title: { type: 'string', description: 'イベントタイトル' },
          start_time: { type: 'string', description: '開始時刻(ISO 8601形式)' },
          end_time: { type: 'string', description: '終了時刻(ISO 8601形式)' },
          attendees: {
            type: 'array',
            items: { type: 'string' },
            description: '参加者のメールアドレスリスト',
          },
        },
        required: ['title', 'start_time', 'end_time'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'search_documents',
      description: 'ドキュメントを検索します',
      parameters: {
        type: 'object',
        properties: {
          query: { type: 'string', description: '検索クエリ' },
          filter: {
            type: 'object',
            properties: {
              type: { type: 'string', enum: ['pdf', 'docx', 'txt'] },
              date_from: { type: 'string' },
              date_to: { type: 'string' },
            },
          },
        },
        required: ['query'],
      },
    },
  },
];

async function sendEmail(to: string, subject: string, body: string): Promise<string> {
  // 実際のメール送信処理
  console.log(`Sending email to ${to}: ${subject}`);
  return JSON.stringify({ success: true, message_id: crypto.randomUUID() });
}

async function createCalendarEvent(
  title: string,
  startTime: string,
  endTime: string,
  attendees: string[] = []
): Promise<string> {
  // 実際のカレンダーイベント作成処理
  console.log(`Creating event: ${title} from ${startTime} to ${endTime}`);
  return JSON.stringify({
    success: true,
    event_id: crypto.randomUUID(),
    attendees,
  });
}

async function searchDocuments(query: string, filter?: any): Promise<string> {
  // 実際のドキュメント検索処理
  console.log(`Searching documents: ${query}`, filter);
  return JSON.stringify({
    results: [
      { title: 'Document 1', url: 'https://example.com/doc1' },
      { title: 'Document 2', url: 'https://example.com/doc2' },
    ],
  });
}

export async function multiToolAgent(userRequest: string): Promise<string> {
  const messages: OpenAI.ChatCompletionMessageParam[] = [
    {
      role: 'system',
      content: 'あなたは複数のツールを使いこなすアシスタントです。ユーザーの要求に応じて、メール送信、カレンダー登録、ドキュメント検索などを実行できます。',
    },
    { role: 'user', content: userRequest },
  ];

  let response = await openai.chat.completions.create({
    model: 'gpt-4-turbo-preview',
    messages,
    tools,
    tool_choice: 'auto',
  });

  let responseMessage = response.choices[0].message;

  while (responseMessage.tool_calls) {
    messages.push(responseMessage);

    for (const toolCall of responseMessage.tool_calls) {
      const functionName = toolCall.function.name;
      const functionArgs = JSON.parse(toolCall.function.arguments);

      console.log(`Executing: ${functionName}`, functionArgs);

      let functionResponse: string;

      try {
        switch (functionName) {
          case 'send_email':
            functionResponse = await sendEmail(
              functionArgs.to,
              functionArgs.subject,
              functionArgs.body
            );
            break;
          case 'create_calendar_event':
            functionResponse = await createCalendarEvent(
              functionArgs.title,
              functionArgs.start_time,
              functionArgs.end_time,
              functionArgs.attendees
            );
            break;
          case 'search_documents':
            functionResponse = await searchDocuments(
              functionArgs.query,
              functionArgs.filter
            );
            break;
          default:
            functionResponse = JSON.stringify({ error: 'Unknown function' });
        }
      } catch (error) {
        functionResponse = JSON.stringify({ error: (error as Error).message });
      }

      messages.push({
        role: 'tool',
        tool_call_id: toolCall.id,
        content: functionResponse,
      });
    }

    response = await openai.chat.completions.create({
      model: 'gpt-4-turbo-preview',
      messages,
      tools,
    });

    responseMessage = response.choices[0].message;
  }

  return responseMessage.content || 'No response';
}

// 使用例
const result = await multiToolAgent(
  '明日14時から1時間、チームミーティングをスケジュールして、参加者にメールで通知してください'
);
console.log(result);

エラーハンドリングとベストプラクティス

1. リトライ機能付きFunction Calling

async function executeWithRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  delayMs: number = 1000
): Promise<T> {
  let lastError: Error | undefined;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      console.log(`Retry ${i + 1}/${maxRetries} after error:`, error);
      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, delayMs * (i + 1)));
      }
    }
  }

  throw lastError;
}

2. タイムアウト制御

async function executeWithTimeout<T>(
  fn: () => Promise<T>,
  timeoutMs: number
): Promise<T> {
  return Promise.race([
    fn(),
    new Promise<T>((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), timeoutMs)
    ),
  ]);
}

まとめ

Function Callingを活用することで、LLMは外部システムと連携し、より実用的なアプリケーションを構築できます。主なポイントは以下の通りです。

  • 明確な関数定義: パラメータとその説明を詳細に記述
  • 適切なエラーハンドリング: リトライ、タイムアウト、バリデーション
  • セキュリティ: 実行権限の制御、入力検証
  • ログとモニタリング: 関数呼び出しの追跡と分析

これらの手法を組み合わせることで、インテリジェントで信頼性の高いAIアプリケーションを開発できます。