AIエージェントをゼロから作る:Function Callingと実践パターン
AIの世界が大きく変わりました。「質問したら答える」時代から、AIエージェントの時代へ。自分で考え、計画を立て、複雑なタスクを実行してくれるシステムです。
実際に作ってみたことありますか?「API呼ぶだけでしょ」と思っていたら、それが甘かったと気づいたはずです😅 ループ管理、ツール定義、エラーハンドリング...やること盛りだくさんです。
この記事では、本番環境で使えるAIエージェントをゼロから作っていきます。
チャットボットとAIエージェントの違い
シンプルに言うと:
チャットボットは答える。エージェントは行動する。
この違いを生むのがエージェントループです:
- 観察 — 今何を求められてる?
- 思考 — どうアプローチする?
- 実行 — ツールやAPIを呼ぶ!
- 評価 — うまくいった?次は?
- 繰り返し — 完了まで続ける
これにより「競合を調べてレポート作って」「安い航空券探して予約して」のような複雑なタスクが可能になります。
┌─────────────────────────────────────────────────────┐
│ エージェンティックループ │
├─────────────────────────────────────────────────────┤
│ ┌──────────┐ │
│ │ 観察 │ ◄── ユーザー要求 / 環境 │
│ └────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ 思考 │ ◄── LLM推論 │
│ └────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ 行動 │ ◄── ツール実行 │
│ └────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ 振り返り │ ◄── 結果評価 │
│ └────┬─────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ タスク完了? │── はい ──► 結果を返す │
│ └──────┬─────────┘ │
│ │ いいえ │
│ └─────────► 観察に戻る │
└─────────────────────────────────────────────────────┘
Function Callingを理解する:ツール使用の基礎
Function Calling(「ツール使用」とも呼ばれます)は、LLMが外部関数の実行を要求できるようにするメカニズムです。モデルはテキスト応答を生成する代わりに、特定の関数を特定の引数で呼び出すための構造化されたリクエストを出力します。
Function Callingの仕組み
関数定義と共にLLMにリクエストを送ると、モデルは以下のいずれかを選択できます:
- テキストで応答(従来のチャットボット動作)
- 関数呼び出しを要求(エージェンティック動作)
OpenAI APIでの関数定義は以下のようになります:
const tools = [ { type: "function", function: { name: "get_weather", description: "特定の場所の現在の天気を取得します", parameters: { type: "object", properties: { location: { type: "string", description: "都市名、例:'東京都'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"], description: "温度の単位" } }, required: ["location"] } } } ];
descriptionフィールドは非常に重要です。LLMはこの説明を見て、いつ、どのように関数を呼び出すかを決定します。
Claude(Anthropic)でのFunction Calling
Claudeは似ていますが、少し異なる構造を使用します:
const tools = [ { name: "get_weather", description: "特定の場所の現在の天気を取得します", input_schema: { type: "object", properties: { location: { type: "string", description: "都市名、例:'東京都'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"] } }, required: ["location"] } } ];
主な違いはparametersの代わりにinput_schemaを使用することですが、概念自体は同じです。
最初のAIエージェントを構築する:完全な実装
ウェブ検索、ドキュメント読み取り、計算を行える実用的なAIエージェントを構築しましょう。
ステップ1:ツールインターフェースの定義
interface Tool { name: string; description: string; parameters: { type: "object"; properties: Record<string, { type: string; description: string; enum?: string[]; }>; required: string[]; }; execute: (args: Record<string, unknown>) => Promise<string>; }
ステップ2:ツールの実装
const webSearchTool: Tool = { name: "web_search", description: "ウェブで最新情報を検索します。最近のイベント、ニュース、または学習データにない情報に使用してください。", parameters: { type: "object", properties: { query: { type: "string", description: "検索クエリ" } }, required: ["query"] }, execute: async (args) => { const { query } = args as { query: string }; const response = await fetch(`https://api.search.example/search?q=${encodeURIComponent(query)}`); const data = await response.json(); return JSON.stringify(data.results.slice(0, 5)); } }; const calculatorTool: Tool = { name: "calculator", description: "数学計算を実行します。基本的な算術演算とパーセンテージをサポートします。", parameters: { type: "object", properties: { expression: { type: "string", description: "計算する数式、例:'(100 * 1.15) + 50'" } }, required: ["expression"] }, execute: async (args) => { const { expression } = args as { expression: string }; try { const result = Function(`"use strict"; return (${expression})`)(); return `結果: ${result}`; } catch { return `エラー: 無効な式です`; } } }; const readUrlTool: Tool = { name: "read_url", description: "URLからテキストコンテンツを読み取り、抽出します。", parameters: { type: "object", properties: { url: { type: "string", description: "読み取るURL" } }, required: ["url"] }, execute: async (args) => { const { url } = args as { url: string }; try { const response = await fetch(url); const html = await response.text(); const text = html.replace(/<[^>]*>/g, ' ').slice(0, 5000); return text; } catch (error) { return `URL読み取りエラー: ${error}`; } } };
ステップ3:エージェンティックループの実装
ここが魔法の起こる場所です:
import OpenAI from 'openai'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); interface Message { role: "system" | "user" | "assistant" | "tool"; content: string; tool_calls?: Array<{ id: string; type: "function"; function: { name: string; arguments: string }; }>; tool_call_id?: string; } async function runAgent( userMessage: string, tools: Tool[], maxIterations: number = 10 ): Promise<string> { const toolDefinitions = tools.map(tool => ({ type: "function" as const, function: { name: tool.name, description: tool.description, parameters: tool.parameters } })); const messages: Message[] = [ { role: "system", content: `あなたはツールにアクセスできる有用なAIアシスタントです。 質問に正確に答えるために必要に応じてツールを使用してください。 ツールを使用する前に必ず推論過程を説明してください。` }, { role: "user", content: userMessage } ]; for (let i = 0; i < maxIterations; i++) { const response = await openai.chat.completions.create({ model: "gpt-4-turbo-preview", messages: messages, tools: toolDefinitions, tool_choice: "auto" }); const assistantMessage = response.choices[0].message; messages.push(assistantMessage as Message); if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) { return assistantMessage.content || "応答を生成できませんでした。"; } for (const toolCall of assistantMessage.tool_calls) { const tool = tools.find(t => t.name === toolCall.function.name); if (!tool) { messages.push({ role: "tool", tool_call_id: toolCall.id, content: `エラー: ツール '${toolCall.function.name}' が見つかりません` }); continue; } try { const args = JSON.parse(toolCall.function.arguments); const result = await tool.execute(args); messages.push({ role: "tool", tool_call_id: toolCall.id, content: result }); } catch (error) { messages.push({ role: "tool", tool_call_id: toolCall.id, content: `ツール実行エラー: ${error}` }); } } } return "最大反復回数に達しました。"; }
ステップ4:エージェントの使用
const tools = [webSearchTool, calculatorTool, readUrlTool]; const result = await runAgent( "東京の現在の人口はいくらで、日本の総人口の何パーセントですか?", tools ); console.log(result);
ReActパターン:推論と行動
ReAct(Reasoning + Acting)パターンは、エージェントをより信頼性の高いものにするフレームワークです。エージェントは各ステップで明示的に推論を行います:
const REACT_SYSTEM_PROMPT = `あなたはReActパターンに従うアシスタントです。 各ステップで以下を実行してください: 1. THOUGHT: 現在の状況について推論する 2. ACTION: アクションを選択する(ツール使用または最終回答) 3. OBSERVATION: 結果を分析する フォーマット: THOUGHT: [推論内容] ACTION: [ツール名と引数 または "FINAL_ANSWER"] OBSERVATION: [ツールの結果] 回答する準備ができたら: THOUGHT: 十分な情報が揃いました。 ACTION: FINAL_ANSWER [完全な回答]`;
エラー処理と信頼性パターン
1. 指数バックオフによるリトライ
async function executeWithRetry<T>( fn: () => Promise<T>, maxRetries: number = 3, baseDelay: number = 1000 ): Promise<T> { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { if (attempt === maxRetries - 1) throw error; const delay = baseDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error("リトライ失敗"); }
2. 実行タイムアウト
async function executeWithTimeout<T>( fn: () => Promise<T>, timeoutMs: number = 30000 ): Promise<T> { return Promise.race([ fn(), new Promise<T>((_, reject) => setTimeout(() => reject(new Error("タイムアウト")), timeoutMs) ) ]); }
3. グレースフルデグラデーション
async function safeToolExecute(tool: Tool, args: Record<string, unknown>): Promise<string> { try { return await executeWithTimeout( () => executeWithRetry(() => tool.execute(args)), 30000 ); } catch (error) { return `ツール "${tool.name}" が失敗しました: ${error}。別のアプローチを試してください。`; } }
高度なパターン:マルチツールオーケストレーション
interface ToolResult { toolName: string; input: Record<string, unknown>; output: string; timestamp: Date; } class AgentContext { private history: ToolResult[] = []; private cache: Map<string, string> = new Map(); async executeToolWithCache(tool: Tool, args: Record<string, unknown>): Promise<string> { const cacheKey = `${tool.name}:${JSON.stringify(args)}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey)!; } const result = await tool.execute(args); this.cache.set(cacheKey, result); this.history.push({ toolName: tool.name, input: args, output: result, timestamp: new Date() }); return result; } getHistory(): ToolResult[] { return [...this.history]; } }
並列ツール実行
ツールが独立している場合は、並列で実行してパフォーマンスを向上させます:
async function executeToolsInParallel( toolCalls: Array<{ tool: Tool; args: Record<string, unknown> }> ): Promise<Map<string, string>> { const results = new Map<string, string>(); const promises = toolCalls.map(async ({ tool, args }) => { const result = await safeToolExecute(tool, args); results.set(tool.name, result); }); await Promise.all(promises); return results; }
モニタリングと可観測性
interface AgentTrace { traceId: string; startTime: Date; endTime?: Date; steps: Array<{ type: "thought" | "action" | "observation"; content: string; timestamp: Date; }>; status: "running" | "completed" | "failed"; } function createTracer() { const trace: AgentTrace = { traceId: crypto.randomUUID(), startTime: new Date(), steps: [], status: "running" }; return { trace, addStep: (type: "thought" | "action" | "observation", content: string) => { trace.steps.push({ type, content, timestamp: new Date() }); }, complete: () => { trace.endTime = new Date(); trace.status = "completed"; } }; }
よくある落とし穴と回避方法
1. 無限ループ
function detectLoop(messages: Message[], threshold: number = 3): boolean { const recentToolCalls = messages .filter(m => m.tool_calls) .slice(-threshold) .map(m => JSON.stringify(m.tool_calls)); return new Set(recentToolCalls).size === 1 && recentToolCalls.length === threshold; }
2. コンテキストウィンドウのオーバーフロー
async function summarizeHistory(messages: Message[]): Promise<Message[]> { if (messages.length <= 10) return messages; const toSummarize = messages.slice(1, -5); const summary = await openai.chat.completions.create({ model: "gpt-4-turbo-preview", messages: [ { role: "system", content: "この会話を簡潔に要約してください。" }, { role: "user", content: JSON.stringify(toSummarize) } ] }); return [ messages[0], { role: "assistant", content: `前のコンテキスト: ${summary.choices[0].message.content}` }, ...messages.slice(-5) ]; }
3. 曖昧なツール説明
// ❌ 悪い例 description: "天気API" // ✅ 良い例 description: "都市の現在の天気状況を取得します。天気や気温について尋ねられたときに使用してください。都市でのみ機能し、国や地域では機能しません。"
セキュリティの考慮事項
1. 入力検証
function validateToolArgs(schema: Tool['parameters'], args: Record<string, unknown>): boolean { for (const required of schema.required) { if (!(required in args)) return false; } for (const [key, value] of Object.entries(args)) { const propSchema = schema.properties[key]; if (propSchema?.enum && !propSchema.enum.includes(value as string)) { return false; } } return true; }
2. サンドボックス実行
import ivm from 'isolated-vm'; async function safeEval(code: string): Promise<string> { const isolate = new ivm.Isolate({ memoryLimit: 128 }); const context = await isolate.createContext(); try { const result = await context.eval(code, { timeout: 5000 }); return String(result); } finally { isolate.dispose(); } }
3. レート制限
class RateLimiter { private requests: number[] = []; constructor(private limit: number, private windowMs: number) {} async check(): Promise<boolean> { const now = Date.now(); this.requests = this.requests.filter(t => t > now - this.windowMs); if (this.requests.length >= this.limit) return false; this.requests.push(now); return true; } }
まとめ
AIエージェントの核心はループ:観察 → 思考 → 実行 → 評価 → 繰り返し。
- シンプルに始める — まずFunction Callingから
- 説明は明確に — LLMは説明を見て判断する
- エラーは必ず起きる — 備えあれば憂いなし
- ログは必須 — ないとデバッグ地獄
- 制限を設ける — 無限ループ防止
- セキュリティ第一 — LLM出力を鬻呑みにしない
次のステップはマルチエージェント、永続メモリ、学習するエージェント...でも全て今日の内容の上に築かれます。さあ、実装してみましょう!🚀
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう