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に登録していくと、やがてエージェントの精度が低下し、応答が遅くなります。

このような問題が起きる理由は、LangChainなどのフレームワークを使う際、ツール定義の管理とツール選択のプロセスが分離しているためです。エージェントが多数のツール定義を受け取ると、目的に合った正しいツールを選択するまでに時間がかかり、場合によっては不適切なツールを呼び出してしまいます。

本記事では、実務開発者が遭遇しやすいこの問題の原因と、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:機能別にツールを分類し、階層的に設計する

最も効果的な対策は、ツールの数を意図的に制限し、分類ごとに異なるエージェントを構築することです。

参考:LangChainでの実装例

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

pip install langchain langchain-openai langchain-community

機能別分類の実装例を示します:

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}"

# グループAのエージェント
llm = ChatOpenAI(model="gpt-4")
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
)

# 使用例
print("=== ステップ1:データ取得 ===")
retrieval_result = data_retrieval_executor.invoke({
    "input": "営業ログデータを取得してください"
})

print("\n=== ステップ2:データ処理 ===")
processing_result = processing_executor.invoke({
    "input": f"以下のデータを統計処理してレポートにしてください\n{retrieval_result}"
})

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

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

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

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

# ステップ1:意図分類
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("来月の売上見込みを計算してください")

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

解決策3: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ではコード生成とファイル操作が主な目的になるため、その目的に専門化したツールセットを構築することで、生成コードの品質が大幅に向上します。

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

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

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%以下)を削除候補に

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

実務的なベストプラクティス:ツール設計の7つの原則

多くの実案件で有効な原則を以下にまとめます:

原則1:ツール数は最大5個に制限

エージェントが正確に動作するには、見えるツールの数を5個以下に保つことが推奨されます。これを超える機能が必要な場合は、複数のエージェントに分割するか、意図分類ステップを導入してください。

原則2:ツール名と説明は明確に

# 悪い例
@tool
def process(input: str) -> str:
    """入力を処理する"""
    pass

# 良い例
@tool
def extract_customer_name_from_invoice(invoice_text: str) -> str:
    """請求書テキストから顧客名を抽出する。請求書の「Bill To」セクションを解析"""
    pass

原則3:1つのツール=1つの責任

複数の機能を1つのツールに詰め込まない:

# 悪い例:1つのツールで多すぎる
@tool
def database_operation(operation: str, query: str) -> str:
    """データベース操作(検索・作成・更新・削除に対応)"""
    pass

# 良い例:責任を分割
@tool
def search_customer_records(query: str) -> str:
    """顧客レコードを検索"""
    pass

@tool
def create_customer_record(name: str, email: str) -> str:
    """新規顧客レコードを作成"""
    pass

原則4:エラーハンドリングはツール内で完結

ツールが失敗したときの動作をツール内で定義することで、エージェントの判断負荷を減らします:

@tool
def safe_database_query(query: str) -> str:
    """安全なデータベースクエリ。エラーメッセージを返す"""
    try:
        result = execute_query(query)
        return f"成功: {result}"
    except ValueError as e:
        return f"エラー: クエリ形式が不正です。{str(e)}"
    except ConnectionError:
        return "エラー: データベースに接続できません。後で再試行してください。"
    except Exception as e:
        return f"エラー: 予期しないエラーが発生しました。{str(e)}"

原則5:出力形式を統一

すべてのツールが統一された形式で結果を返すことで、エージェントが解釈しやすくなります:

import json

def format_tool_result(success: bool, data: any, message: str = ""):
    """ツール出力を統一フォーマットで返す"""
    return json.dumps({
        "success": success,
        "data": data,
        "message": message
    }, ensure_ascii=False)

@tool
def search_with_format(query: str) -> str:
    """フォーマット済みの検索結果を返す"""
    try:
        result = search_database(query)
        return format_tool_result(True, result, "検索に成功しました")
    except Exception as e:
        return format_tool_result(False, None, str(e))

原則6:テストと検証を組み込む

ツール定義後、実際にエージェントが正しく選択・実行するか検証します:

def test_agent_tool_selection(agent, test_cases):
    """エージェントのツール選択をテスト"""
    for test_input, expected_tool in test_cases:
        result = agent.invoke({"input": test_input})
        # ログから実際に使用されたツールを確認
        used_tool = extract_tool_name_from_log(result)
        
        if used_tool == expected_tool:
            print(f"✓ PASS: '{test_input}' → {used_tool}")
        else:
            print(f"✗ FAIL: '{test_input}' → 期待値: {expected_tool}, 実際: {used_tool}")

# テストケース
test_cases = [
    ("営業データを取得してください", "search_database"),
    ("メールを送信してください", "send_email"),
    ("天気を教えてください", "fetch_weather"),
]

test_agent_tool_selection(agent, test_cases)

原則7:定期的なツール削除と最適化

プロジェクトが進むにつれて、不要なツール定義が蓄積します。定期的に監視ログを確認し、以下の基準で削除を検討してください:

  • 過去3ヶ月で1回も呼び出されていない
  • 成功率が30%未満
  • より一般的なツールと機能が重複している

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呼び出しのコスト(トークン数)が増加
# Claude Codeでも同様の原則が適用される
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個以下制限」はより厳密に守るべきです。

まとめ:ツール多すぎ問題を完全に解決するチェックリスト

以下のリストを参考に、プロジェクト開始前・途中・定期見直し時に確認してください:

  • エージェントあたりのツール数は5個以下か
  • ツール名と説明から、その機能が一目で理解できるか
  • 1つのツールが複数の責任を持っていないか
  • すべてのツールでエラーハンドリングを実装しているか
  • ツール出力形式が統一されているか
  • ツール選択の精度をテストしたか
  • ツール使用ログを監視し、不要なツールを定期削除しているか
  • 複数ジャンルの機能が必要な場合、複数エージェント化を検討したか
  • Claude Code使用時は、Tool Use specificationに従っているか

これらの対策により、Claude CodeやChatGPT APIで構築するエージェントの精度と応答速度を大幅に改善できます。


あわせて読みたい

参考ソース