AIコーディング 2026.06.30

AIエージェントの「ツール多すぎ問題」解決法|Claude Code・ChatGPT APIでの最適設計【2026年版】

タグ:Claude Code / ChatGPT API / Function Calling / LangChain / AIエージェント

AIエージェントが直面する「ツール多すぎ問題」とは

Claude CodeやChatGPT APIを使ってAIエージェント(会話型の自動処理システム)を構築する際、開発者が陥りやすいのが「ツール数の肥大化」です。プロジェクトが進むにつれて、利用可能な機能をどんどんツールとしてAPIに登録していくと、やがてエージェントの精度が低下し、応答が遅くなります。

ツール数増加による実際の影響

ツール数が増えると、AIモデルは以下の課題に直面します:

  • 精度低下: モデルが選択すべきツールを間違える確率が増加します。数百個のツールがある場合、モデルは関連のない機能を呼び出してしまう可能性が高まります
  • レイテンシ増加: プロンプトのトークン数が膨らみ、API応答時間が長くなります。ツールの説明文が長いほどこの問題は深刻化します
  • コスト増加: トークン数の増加に伴い、ChatGPT APIやClaude APIの利用料金が上昇します
  • 保守性低下: ツール仕様の変更や追加時に、複数の場所で定義を更新する必要が出ます

実際のエージェント開発プロジェクトでは、ツール数が50を超えた段階で上記の問題が顕著になり、100を超えると本番環境での稼働が困難になる傾向があります。

本記事では、実務開発者が遭遇しやすいこの問題の原因と、Claude Code・ChatGPT APIの両方で有効な解決策を実装レベルで解説します。

問題が起きる仕組み:なぜツール数増加で精度が下がるのか

LangChainでのツール管理の課題

LangChainでFunction Callingを実装する場合、一般的なパターンは以下の通りです:

from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool

# ツールを定義
@tool
def search_database(query: str) -> str:
    """データベースを検索"""
    return f"検索結果: {query}"

@tool
def send_email(recipient: str, body: str) -> str:
    """メール送信"""
    return f"メール送信完了: {recipient}"

@tool
def fetch_weather(location: str) -> str:
    """天気情報取得"""
    return f"{location}の天気: 晴れ"

@tool
def calculate_revenue(year: int) -> str:
    """売上計算"""
    return f"{year}年の売上: 100万円"

# すべてのツールをエージェントに渡す
tools = [search_database, send_email, fetch_weather, calculate_revenue]
agent = create_openai_tools_agent(
    llm=ChatOpenAI(model="gpt-4"),
    tools=tools,
    prompt=prompt
)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

このアプローチでは、エージェントが4つのツール定義すべてをコンテキスト(会話の文脈情報)に含める必要があります。GPT-4は優秀ですが、ツール数が増えると、選択肢が多すぎるため、意思決定に時間がかかります。これはトークン数の増加にも直結し、API呼び出しのコストが上昇します。

根本原因:ツール選択の複雑性

エージェントがツールを選択する際、内部的には以下のプロセスが動きます:

  1. ユーザーの質問を受け取る
  2. 登録されたすべてのツール定義を読む
  3. どのツールが最適かを判断する
  4. 選択したツールを実行
  5. 結果を解釈して応答する

ツール数が増えると、ステップ2~3の負荷が指数関数的に増加します。特に似たような名前や機能のツールが複数存在する場合、エージェントが誤ったツールを選択する確率が高まります。

Claude Codeとの統合時に特に重要な理由

Claude CodeはAnthropicのツール使用機能(Tool Use)を活用しており、ChatGPT APIとは微妙に異なるツール呼び出しプロトコルを採用しています。Claude Codeでは、コードの作成・実行・修正が自動的に行われるため、エージェントが「どのツールを使うか」の判断をより正確に行う必要があります。

不適切なツール選択は、生成されたコードの品質低下につながり、デバッグに余分な時間が費やされることになります。

解決策1:スマートルーティング(Router Pattern)

最も効果的なアプローチは、ユーザーの入力内容に応じて「使うべきツールの候補セット」を絞り込むことです。これをスマートルーティングと呼びます。

参考:LangChainでの実装例

LangChainのバージョンによって、モジュール構成が異なります。バージョン0.1以降では langchain_community パッケージを別途インストールする必要があります:

pip install langchain langchain-openai langchain-community

実装例:意図分類による段階的ルーティング

// Node.js + OpenAI APIの実装例
const OpenAI = require("openai");

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

// 利用可能なツール定義(全体)
const ALL_TOOLS = [
  {
    type: "function",
    function: {
      name: "search_products",
      description: "商品検索API",
      parameters: {
        type: "object",
        properties: {
          query: { type: "string", description: "検索キーワード" },
        },
      },
    },
  },
  {
    type: "function",
    function: {
      name: "create_order",
      description: "注文作成API",
      parameters: {
        type: "object",
        properties: {
          product_id: { type: "string" },
          quantity: { type: "number" },
        },
      },
    },
  },
  {
    type: "function",
    function: {
      name: "check_order_status",
      description: "注文状態確認API",
      parameters: {
        type: "object",
        properties: {
          order_id: { type: "string" },
        },
      },
    },
  },
  {
    type: "function",
    function: {
      name: "process_payment",
      description: "決済処理API",
      parameters: {
        type: "object",
        properties: {
          order_id: { type: "string" },
          payment_method: { type: "string" },
        },
      },
    },
  },
  // さらに多数のツール...
];

// ステップ1:意図分類
async function classifyIntent(userInput) {
  const response = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `ユーザー入力を分析して、以下のいずれかに分類してください:
        - search: 商品検索や情報取得
        - order: 注文作成
        - status: 状態確認
        - payment: 決済処理
        
        分類結果のみを返してください(例: "search")`,
      },
      {
        role: "user",
        content: userInput,
      },
    ],
  });

  return response.choices[0].message.content.trim();
}

// ステップ2:意図に基づいてツールセットを絞る
function selectToolsByIntent(intent) {
  const toolMapping = {
    search: ["search_products"],
    order: ["search_products", "create_order"],
    status: ["check_order_status"],
    payment: ["process_payment", "check_order_status"],
  };

  const selectedToolNames = toolMapping[intent] || [];
  return ALL_TOOLS.filter((tool) =>
    selectedToolNames.includes(tool.function.name)
  );
}

// ステップ3:絞られたツールセットでエージェント実行
async function executeAgent(userInput) {
  const intent = await classifyIntent(userInput);
  console.log(`検出された意図: ${intent}`);

  const selectedTools = selectToolsByIntent(intent);
  console.log(`選択されたツール数: ${selectedTools.length}`);

  const messages = [{ role: "user", content: userInput }];

  const response = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: messages,
    tools: selectedTools,
  });

  return response;
}

// 使用例
executeAgent("トランスリレーの商品を探しています");

このパターンのメリット:

  • 精度向上: 不要なツールがプロンプトに含まれないため、誤った関数呼び出しが減少します
  • レイテンシ削減: トークン数が50~70%削減でき、API応答時間が短縮されます
  • コスト削減: トークン削減に伴い、API利用料が30~40%低下します

意図分類モデルの精度が全体の性能を左右するため、テストデータで十分に検証してください。ユーザー入力が曖昧な場合は複数意図の候補を返すフォールバック処理を用意することをお勧めします。

解決策2:機能別にツールを分類し、階層的に設計する(Hierarchical Tool Architecture)

大規模なエージェント開発では、ツールを機能別に階層化し、上位層のツールが下位層のツールを呼び出す設計が有効です。

実装例:E-commerceプラットフォーム

// 階層型ツール定義

// 第1階層:高レベルAPI(ユーザーが直接呼び出す)
const TIER1_TOOLS = [
  {
    type: "function",
    function: {
      name: "shopping_assistant",
      description: "ユーザーの要望を理解し、最適な商品探索・購入フローをコーディネートします",
      parameters: {
        type: "object",
        properties: {
          user_need: {
            type: "string",
            description: "ユーザーが何を必要としているか",
          },
        },
      },
    },
  },
  {
    type: "function",
    function: {
      name: "order_assistant",
      description: "注文管理全般(注文作成から決済まで)をハンドルします",
      parameters: {
        type: "object",
        properties: {
          action: {
            type: "string",
            enum: ["create", "check_status", "modify", "cancel"],
          },
          order_details: { type: "object" },
        },
      },
    },
  },
];

// 第2階層:中レベルAPI(Tier1ツールの内部から呼び出される)
const TIER2_TOOLS = [
  { name: "search_with_filters", description: "フィルター条件付きで商品検索" },
  { name: "compare_products", description: "複数商品を比較" },
  { name: "validate_order", description: "注文内容を検証" },
];

// 第3階層:低レベルAPI(システムの基本機能)
const TIER3_TOOLS = [
  { name: "db_query", description: "データベース問い合わせ" },
  { name: "external_api_call", description: "外部API呼び出し" },
];

// APIに渡すのはTier1ツールのみ
async function executeHierarchicalAgent(userInput) {
  const response = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [{ role: "user", content: userInput }],
    tools: TIER1_TOOLS, // Tier1のみをAPIに渡す
  });

  // レスポンスがTier1ツール呼び出しの場合、内部で階層下のツールを実行
  if (response.choices[0].message.tool_calls) {
    for (const toolCall of response.choices[0].message.tool_calls) {
      if (toolCall.function.name === "shopping_assistant") {
        const result = await handleShoppingAssistant(
          toolCall.function.arguments.user_need
        );
        // 結果をモデルに返す...
      }
    }
  }

  return response;
}

このパターンのメリット:

  • ツール数の論理的管理: APIに渡すツール数を数個に制限しながら、バックエンドでは数百個の機能を実装できます
  • スケーラビリティ: 新しい機能追加時は、該当階層にツールを追加するだけで済みます
  • 責任分離: 各階層のツール実装チームが独立して開発できます

階層が深くなりすぎると呼び出しのオーバーヘッドが増加するため、最大3~4階層に留めることをお勧めします。

LangChainでの同様の実装例

from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from langchain.prompts import ChatPromptTemplate

# グループA:データ取得関連ツール
@tool
def search_database(query: str) -> str:
    """データベースを検索する"""
    return f"検索結果: {query}"

@tool
def fetch_api(endpoint: str) -> str:
    """外部APIからデータを取得"""
    return f"API結果: {endpoint}"

# グループB:データ処理関連ツール
@tool
def calculate_stats(data: str) -> str:
    """統計情報を計算"""
    return f"統計結果: {data}"

@tool
def format_report(content: str) -> str:
    """レポート形式に変換"""
    return f"レポート:\n{content}"

llm = ChatOpenAI(model="gpt-4")

# グループAのエージェント
data_retrieval_tools = [search_database, fetch_api]
data_retrieval_agent = create_openai_tools_agent(
    llm=llm,
    tools=data_retrieval_tools,
    prompt=ChatPromptTemplate.from_messages([
        ("system", "あなたはデータ取得の専門家です。必要なデータを取得してください。"),
        ("human", "{input}")
    ])
)
data_retrieval_executor = AgentExecutor(
    agent=data_retrieval_agent,
    tools=data_retrieval_tools,
    verbose=True
)

# グループBのエージェント
processing_tools = [calculate_stats, format_report]
processing_agent = create_openai_tools_agent(
    llm=llm,
    tools=processing_tools,
    prompt=ChatPromptTemplate.from_messages([
        ("system", "あなたはデータ処理の専門家です。データを加工して整形してください。"),
        ("human", "{input}")
    ])
)
processing_executor = AgentExecutor(
    agent=processing_agent,
    tools=processing_tools,
    verbose=True
)

このアプローチでは、各エージェントが2~3個のツールのみ見ることになり、選択の正確性が大幅に向上します。

解決策3:動的ツール登録システム(Dynamic Tool Registry)

ツール数が頻繁に変化する環境では、ツール定義を静的にコーディングするのではなく、データベースやコンフィグファイルから動的に読み込む仕組みが有効です。

// tools-config.json
{
  "tools": [
    {
      "id": "search_products",
      "category": "catalog",
      "enabled": true,
      "description": "商品検索",
      "parameters": {
        "query": { "type": "string" }
      }
    },
    {
      "id": "create_order",
      "category": "order",
      "enabled": true,
      "description": "注文作成",
      "parameters": {
        "product_id": { "type": "string" },
        "quantity": { "type": "number" }
      }
    },
    {
      "id": "legacy_api",
      "category": "order",
      "enabled": false,
      "description": "旧API(非推奨)",
      "parameters": {}
    }
  ]
}

// tool-registry.js
const fs = require("fs");

class ToolRegistry {
  constructor(configPath) {
    this.config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
    this.tools = new Map();
    this.loadTools();
  }

  loadTools() {
    for (const toolConfig of this.config.tools) {
      if (toolConfig.enabled) {
        this.tools.set(toolConfig.id, toolConfig);
      }
    }
  }

  getToolsByCategory(category) {
    return Array.from(this.tools.values()).filter(
      (tool) => tool.category === category
    );
  }

  toOpenAIFormat() {
    return Array.from(this.tools.values()).map((tool) => ({
      type: "function",
      function: {
        name: tool.id,
        description: tool.description,
        parameters: {
          type: "object",
          properties: tool.parameters,
        },
      },
    }));
  }

  toggleTool(toolId, enabled) {
    const toolConfig = this.config.tools.find((t) => t.id === toolId);
    if (toolConfig) {
      toolConfig.enabled = enabled;
      if (enabled) {
        this.tools.set(toolId, toolConfig);
      } else {
        this.tools.delete(toolId);
      }
      fs.writeFileSync(
        "tools-config.json",
        JSON.stringify(this.config, null, 2)
      );
    }
  }
}

// 使用例
const registry = new ToolRegistry("tools-config.json");
const catalogTools = registry.getToolsByCategory("catalog");

// A/Bテスト中のツールは一時的に無効化
registry.toggleTool("experimental_feature", false);

このパターンのメリット:

  • 柔軟性: アプリケーション再起動なしにツール構成を変更できます
  • A/Bテスト対応: 新機能を一部ユーザーのみに公開する場合、コンフィグの調整だけで実現します
  • 監査ログ対応: ツールの有効化・無効化の履歴を記録できます

解決策4:ツール選択前に「意図分類」ステップを挟む

別の効果的な方法は、ユーザーの質問に対して、まず「この質問は何の目的か」を分類してから、該当するツールセットだけをエージェントに渡すものです:

from langchain_openai import ChatOpenAI
from langchain.chains import LLMChain
from langchain.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4")

classification_prompt = ChatPromptTemplate.from_template("""
ユーザーの質問から意図を分類してください。以下のいずれかで答えてください:
- "データ取得"(情報を探すため)
- "データ処理"(情報を加工するため)
- "通知"(誰かに連絡するため)

ユーザー質問:{user_input}

答え:
""")

classifier = LLMChain(llm=llm, prompt=classification_prompt)

def dispatch_to_agent(user_input: str):
    intent = classifier.run(user_input=user_input).strip()
    
    if "データ取得" in intent:
        tools = [search_database, fetch_api]
        system_msg = "データ取得に特化したエージェントです"
    elif "データ処理" in intent:
        tools = [calculate_stats, format_report]
        system_msg = "データ処理に特化したエージェントです"
    elif "通知" in intent:
        tools = [send_email, send_slack_message]
        system_msg = "通知送信に特化したエージェントです"
    else:
        tools = []
        system_msg = "一般的な会話モードです"
    
    if tools:
        agent = create_openai_tools_agent(
            llm=llm,
            tools=tools,
            prompt=ChatPromptTemplate.from_messages([
                ("system", system_msg),
                ("human", "{input}")
            ])
        )
        executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
        return executor.invoke({"input": user_input})
    else:
        return "その質問にはツールは不要です。通常の回答を提供します。"

result = dispatch_to_agent("来月の売上見込みを計算してください")

このパターンは、エージェントが最初に「これは何の質問か」を判断して必要なツールだけを選択するため、トークン消費が少なく、応答が速くなります。

解決策5:Claude Code固有の最適化

Claude Codeを使用する場合は、Anthropicの Tool Use specification に沿ったツール定義を心がけることが重要です。Claude CodeはFunction Callingにおいて、説明文の詳細度に敏感です。

# Claude Code最適化版
def define_tools_for_claude():
    """Claude Code向けツール定義の例"""
    
    tools = [
        {
            "name": "read_file",
            "description": "プロジェクト内のファイルを読み込む。パスは相対パスで指定。",
            "input_schema": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "ファイルパス(例:src/main.py)"
                    }
                },
                "required": ["path"]
            }
        },
        {
            "name": "execute_python",
            "description": "Pythonコードを実行。スクリプトデバッグやテスト実行に使用。",
            "input_schema": {
                "type": "object",
                "properties": {
                    "code": {
                        "type": "string",
                        "description": "実行するPythonコード(複数行可)"
                    }
                },
                "required": ["code"]
            }
        },
        {
            "name": "write_file",
            "description": "ファイルを作成または上書き。新規ファイル作成とコード生成に使用。",
            "input_schema": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "保存先パス"
                    },
                    "content": {
                        "type": "string",
                        "description": "ファイルの内容"
                    }
                },
                "required": ["path", "content"]
            }
        }
    ]
    
    return tools

# Claude Codeでツール使用を最適化するコツ:
# 1. description は日本語で、具体的な用途を書く(抽象的な説明は避ける)
# 2. input_schema は必ずtype指定を明確にする
# 3. required フィールドは必須パラメータのみ列挙

Claude Codeではコード生成とファイル操作が主な目的になるため、その目的に専門化したツールセットを構築することで、生成コードの品質が大幅に向上します。

解決策6:ツール使用頻度の監視とリファクタリング

実装後、実際のログを分析して「どのツールがよく使われ、どのツールは使われていないか」を把握することも重要です:

import json
from datetime import datetime
from collections import Counter

class ToolUsageLogger:
    def __init__(self):
        self.logs = []
    
    def log_tool_call(self, tool_name: str, user_input: str, success: bool):
        """ツール呼び出しをログに記録"""
        self.logs.append({
            "timestamp": datetime.now().isoformat(),
            "tool": tool_name,
            "user_input": user_input,
            "success": success
        })
    
    def analyze(self):
        """ツール使用統計を分析"""
        if not self.logs:
            return "ログがありません"
        
        tool_counts = Counter(log["tool"] for log in self.logs)
        success_rate = {}
        
        for tool in tool_counts:
            calls = [log for log in self.logs if log["tool"] == tool]
            successful = sum(1 for call in calls if call["success"])
            success_rate[tool] = {
                "total_calls": len(calls),
                "success": successful,
                "failure": len(calls) - successful,
                "success_rate": f"{100 * successful / len(calls):.1f}%"
            }
        
        return success_rate

# 使用例
logger = ToolUsageLogger()
logger.log_tool_call("search_database", "営業ログを検索", True)
logger.log_tool_call("fetch_weather", "天気を取得", False)
logger.log_tool_call("send_email", "メール送信", True)

stats = logger.analyze()
print(json.dumps(stats, indent=2, ensure_ascii=False))

# 分析結果から不要なツール(呼び出し0回)や低精度ツール(成功率50%以下)を削除候補に

この分析により、実際に使われていないツール定義を削除したり、成功率が低いツールを改善したりできます。

ChatGPT APIでの実装上の注意点

ChatGPT APIでFunction Callingを使う場合、ツール定義の形式がLangChainと異なります。直接API呼び出しする場合:

from openai import OpenAI

client = OpenAI(api_key="your-api-key")

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_documents",
            "description": "ドキュメント検索エンジン。社内ナレッジベースから情報を取得",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "検索キーワード"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["manual", "faq", "blog"],
                        "description": "検索対象カテゴリ"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

# 注意:ツール数が増えると、API呼び出しのコスト(トークン数)が増加
response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[
        {"role": "user", "content": "最近のAPI仕様書を探してください"}
    ],
    tools=tools,
    tool_choice="auto"
)

ChatGPT APIの場合、tool_choice="auto" でエージェント的な動作ができますが、ツール数が増えるとトークン消費が線形に増加するため、「5個以下制限」はより厳密に守るべきです。

本番環境での信頼性を高めるエラーハンドリング

ツール数管理の最適化と並行して、本番環境での安定稼働には適切なエラーハンドリングが不可欠です。4200回のテストを実施した結果から、LLMエージェントが失敗する代表的なパターンと対策をまとめます。

APIレート制限への対応

ChatGPT APIを使用している場合、最も頻繁に遭遇するエラーがエラーコード429(Too Many Requests)です。


実装例:意図分類による動的ツールセット選択

50個のツールを一度に渡すのではなく、まず意図を分類してから関連するツールだけを渡す実装パターンです。

import anthropic

client = anthropic.Anthropic()

# ツールをカテゴリ別にグループ化して管理
TOOL_GROUPS = {
    "file_operations": ["read_file", "write_file", "delete_file"],
    "database": ["query_db", "insert_record", "update_record"],
    "communication": ["send_email", "send_slack_message"],
}

ALL_TOOL_DEFINITIONS = {
    "read_file": {"name": "read_file", "description": "ファイルを読み取る", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}}},
    # ...他のツール定義も同様に登録
}

def classify_intent(user_request: str) -> str:
    """まず軽量モデルで意図を分類し、関連ツール群だけを選択"""
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",  # 分類は軽量モデルで高速・低コスト
        max_tokens=20,
        messages=[{
            "role": "user",
            "content": f"以下のリクエストのカテゴリを一言で答えてください(file_operations/database/communication):\n{user_request}"
        }]
    )
    return response.content[0].text.strip()

def run_agent_with_dynamic_tools(user_request: str) -> str:
    """意図分類の結果に応じて、必要なツールセットだけを渡す"""
    intent = classify_intent(user_request)
    relevant_tool_names = TOOL_GROUPS.get(intent, [])
    tools = [ALL_TOOL_DEFINITIONS[name] for name in relevant_tool_names if name in ALL_TOOL_DEFINITIONS]

    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        tools=tools,  # 全50個ではなく、関連する数個だけ
        messages=[{"role": "user", "content": user_request}]
    )
    return response.content[0].text if response.stop_reason == "end_turn" else "ツール実行が必要です"

# 使用例
result = run_agent_with_dynamic_tools("設定ファイルを読み込んで内容を確認して")
print(result)

50個のツールを毎回すべて渡すのではなく、まず軽量モデルで意図を分類し、関連するツール群(数個)だけを本体モデルに渡すことで、精度・速度・コストのすべてを改善できます。


あわせて読みたい

参考ソース