AIエージェントの「ツール多すぎ問題」解決法|Claude Code・ChatGPT APIでの最適設計【2026年版】
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呼び出しのコストが上昇します。
根本原因:ツール選択の複雑性
エージェントがツールを選択する際、内部的には以下のプロセスが動きます:
- ユーザーの質問を受け取る
- 登録されたすべてのツール定義を読む
- どのツールが最適かを判断する
- 選択したツールを実行
- 結果を解釈して応答する
ツール数が増えると、ステップ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個のツールを毎回すべて渡すのではなく、まず軽量モデルで意図を分類し、関連するツール群(数個)だけを本体モデルに渡すことで、精度・速度・コストのすべてを改善できます。
あわせて読みたい
- Claude Codeのコンテキスト管理5ステップ【セッション間で説明を繰り返さない設定方法】
- Claude Codeで修正回数を減らす7つのプロンプトテクニック|精度を上げる質問構造
- Claude Codeのトークン消費を98%削減する方法【MCP活用+コンテキスト最適化】
参考ソース
- LangChain: ModuleNotFoundError - langchain_community
- LangChain: How to use a custom embedding model locally
- Differences between LangChain & LlamaIndex
- What is the difference between OpenAI and ChatOpenAI in LangChain
- How to create a langchain doc from an str
- OpenAI API: How to count tokens before sending a request