はじめに — 30個のツールを渡して、何個使われたか
AIエージェントにツールを30個渡しました。
で、LLMがちゃんと使ってくれたの、何個だと思います?
...正直に言うと、 半分くらい でした。
残りの半分は「存在を無視される」か「間違ったタイミングで呼ばれる」か「引数が全然違う」か。とにかく、思ったように動かない。
最初は「LLMの性能の問題かな」と思ってた。でも、ツール定義の 書き方 を変えただけで、劇的に変わったんです。
この記事では、僕がAIエージェントを半年以上運用する中で見つけた ツール設計の5つのパターン を共有します。どれも「今日から1つ書き直すだけ」で効果が出るものばかりです。
前提: Function Callingとは
LLM(ChatGPT、Claude、Gemini等)に「こういうツールが使えますよ」と定義を渡すと、必要な場面でLLMがツールを選んで引数を生成してくれる仕組みです。ツール定義は基本的にJSON Schema形式で書きます。この記事では特定のSDKやフレームワークに依存しない、 JSON Schema形式 で統一して解説します。
パターン1: Naming — 動詞_名詞で意図を伝える
これ、一番簡単なのに一番効果がある。
ツール名って、LLMにとっては 名刺 みたいなものなんです。初対面の人が名刺を見て「この人は何をしてくれる人か」を判断するのと同じで、LLMはツール名を見て「このツールは何をしてくれるか」を判断する。
Before(ダメな例)
{
"name": "process_data",
"description": "データを処理する"
}
process_data って...何を処理するんですかね。人間でもわからない。LLMにわかるわけがない。
After(改善例)
{
"name": "extract_csv_rows",
"description": "CSVファイルから条件に合う行を抽出する"
}
動詞_名詞 のフォーマットにするだけで、ツールの目的が一目瞭然になる。
命名のルール
| ❌ 曖昧な名前 | ✅ 明確な名前 |
|---|---|
handle_data |
parse_json_response |
do_task |
create_github_issue |
process |
resize_image_to_thumbnail |
run |
execute_sql_query |
get_info |
fetch_user_profile_by_id |
ポイントは3つ。
-
動詞で始める —
search_,create_,delete_,list_,update_等 - 対象を明記する — 何を操作するのかを名前に含める
-
曖昧語を避ける —
process,handle,manage,doは禁止
僕の環境では、ツール名を具体的にしただけで、 ツール選択の正確性が体感で倍近く 上がりました。たった名前を変えるだけなのに。
パターン2: Parameter — 5個以下ルールと型の力
パラメータが10個あるツール、想像してみてください。人間でも混乱しますよね。LLMも同じです。
Before(パラメータ過多)
{
"name": "search_articles",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string" },
"category": { "type": "string" },
"author": { "type": "string" },
"date_from": { "type": "string" },
"date_to": { "type": "string" },
"sort_by": { "type": "string" },
"sort_order": { "type": "string" },
"limit": { "type": "integer" },
"offset": { "type": "integer" },
"include_drafts": { "type": "boolean" }
},
"required": ["query", "category", "author", "date_from", "date_to",
"sort_by", "sort_order", "limit", "offset", "include_drafts"]
}
}
10個のパラメータが 全部required 。これ、LLMは高確率でどれかを間違えます。
After(最小限のパラメータ)
{
"name": "search_articles",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "検索キーワード"
},
"category": {
"type": "string",
"enum": ["tech", "business", "lifestyle"],
"description": "記事カテゴリ(省略時: 全カテゴリ)"
},
"limit": {
"type": "integer",
"description": "取得件数(省略時: 10件)",
"default": 10
}
},
"required": ["query"]
}
}
必須は1個だけ 。残りはOptionalでデフォルト値を持たせる。
なぜ5個以下なのか
人間のワーキングメモリは「7±2」と言われますが、LLMのパラメータ生成精度も似たような傾向がある。僕の運用では、 パラメータが5個を超えると誤りが急増 しました。特にrequiredが多いほど顕著です。
もう一つ大事なのが enum型 の活用。
"format": {
"type": "string",
"enum": ["json", "csv", "text"],
"description": "出力形式"
}
自由入力の string より、 enum で選択肢を制約した方がLLMは 100%正しく指定 してくれます。これは選択肢が明確だからですね。「何でも入れていいよ」より「この3つから選んで」の方が迷わない。人間と同じです。
パラメータ設計のルール
- requiredは最小限に — 本当に必須なものだけ
- Optionalにはdefault値を設定 — 省略時の挙動を明確に
- 自由入力よりenumを使う — 選択肢が有限なら制約する
- 各パラメータにdescriptionを書く — 名前だけでは不十分
- 5個を目安に — 超える場合はツール分割を検討
パターン3: Description — LLMが"読む"仕様書を書く
ここが一番見落とされがちで、一番効果が大きいパターンかもしれない。
ツールの description って、多くの人が1行で済ませてる。「ファイルを読む」「データを取得する」とか。でも、LLMにとってdescriptionは ツールを選ぶかどうかの最終判断材料 なんです。
Before(雑なDescription)
{
"name": "read_file",
"description": "ファイルを読む"
}
「ファイルを読む」。...で? どんなファイル? いつ使う? 何が返ってくる?
After(構造化されたDescription)
{
"name": "read_file",
"description": "指定パスのテキストファイルの内容を読み取って返す。UTF-8エンコーディングのテキストファイル(.txt, .md, .json, .csv等)に対応。バイナリファイル(画像、動画等)には使用しない。Use when: ユーザーがファイルの中身を確認したい時、設定ファイルの内容を取得したい時。Don't use when: 画像ファイルの場合(→ analyze_image を使う)、ディレクトリ一覧が欲しい場合(→ list_files を使う)。"
}
長い? そう、長くていいんです。
descriptionは人間が読むドキュメントじゃなくて、 LLMが読む仕様書 。必要な情報は全部書く。
Description テンプレート
僕が実際に使っているフォーマットはこう。
{目的を1文で}。{対応する入力の種類}。{出力の形式}。
Use when: {使うべき場面を具体的に}。
Don't use when: {使うべきでない場面と、代替ツールへの誘導}。
この Use when / Don't use when のフォーマットが本当に効きます。特にツールが10個以上ある環境では、LLMが「このツールとあのツール、どっちを使うべき?」と迷う場面が多い。そこで「この場面では使わないでください。代わりに○○を使ってください」と明示してあげると、ツール選択の精度が格段に上がる。
実例: 類似ツールの使い分け
[
{
"name": "web_search",
"description": "Webを検索してAI合成された回答と引用元URLを返す。最新ニュースやリアルタイム情報の取得に使用。Use when: 最新情報が必要な時、事実確認をしたい時。Don't use when: 特定URLの中身を読みたい場合(→ web_fetch を使う)。"
},
{
"name": "web_fetch",
"description": "指定URLのWebページを取得してMarkdown形式で返す。特定ページの内容を詳しく読みたい時に使用。Use when: URLが既知で、そのページの内容を読みたい時。Don't use when: 検索クエリから情報を探したい場合(→ web_search を使う)。"
}
]
web_search と web_fetch 。名前だけだと似てるけど、descriptionでの 相互参照 を入れることで、LLMは迷わずに正しい方を選べるようになる。
パターン4: Error Response — 次のアクションを導くエラー設計
エラーが起きた時、LLMに何を返していますか?
Before(情報ゼロのエラー)
{
"error": "failed"
}
これだと、LLMは「失敗した」ことしかわからない。で、同じツールを同じ引数でもう一度呼ぶ。当然また失敗する。無限ループの始まりです。
After(次のアクションを導くエラー)
{
"error": "file_not_found",
"message": "パス '/data/report.csv' にファイルが見つかりません",
"suggestion": "list_files ツールで '/data/' ディレクトリの中身を確認してください",
"available_actions": ["list_files", "create_file"]
}
ポイントは3つ。
-
エラーの種類を明示 —
file_not_found,permission_denied,invalid_format等 - 何が起きたかを人間語で説明 — LLMはこの文を読んで状況を理解する
-
次に何をすべきかを提案 —
suggestionフィールドで具体的なアクションを示す
特に suggestion フィールドの威力がすごい。LLMはエラーメッセージを読んで次のアクションを決めるので、 「次はこのツールを使ってください」と教えてあげる だけで、自律的にリカバリできるようになる。
エラーレスポンスのテンプレート
type ToolErrorResponse = {
error: string; // エラーコード(snake_case)
message: string; // 人間語での説明
suggestion?: string; // 次にすべきアクション
available_actions?: string[]; // 使えるツール名の候補
context?: Record<string, unknown>; // デバッグ用の追加情報
};
これを統一するだけで、 LLMのエラー時の自律復旧率 が大きく変わります。僕の環境では、エラー後に正しいリカバリアクションを取れる確率が体感で5割から8割くらいに上がりました。
よくあるエラーパターンと suggestion の例
| エラー | suggestion |
|---|---|
file_not_found |
list_files ツールでディレクトリの中身を確認してください |
permission_denied |
このファイルは読み取り専用です。別のパスに書き出してください |
rate_limit_exceeded |
30秒後に再試行してください |
invalid_parameter |
format は "json", "csv", "text" のいずれかを指定してください |
empty_result |
検索条件を変えて再検索してください。キーワードを短くすると見つかりやすいです |
エラーメッセージは「怒る」のではなく、 「導く」 もの。「ダメです」ではなく「こうしてみてください」。LLMに限らず、良いAPIはそういう設計になっている。
パターン5: Negative Examples — 使わない場面を明記する
最後のパターンは、地味だけど本当に効くやつ。
ツールが5個くらいなら問題ないんですが、10個、20個、30個と増えていくと、 似たようなツール同士の誤選択 が増えてくる。
例えば、ファイル操作に関するツールが3つあるとする。
-
read_file— ファイルを読む -
write_file— ファイルに書く -
edit_file— ファイルの一部を編集する
「ファイルの内容を変えたい」時に、LLMが write_file(全上書き)を選ぶか edit_file(部分編集)を選ぶか。これ、descriptionだけでは判断しきれないことがある。
Negative Examples のフォーマット
{
"name": "write_file",
"description": "指定パスにファイルを新規作成または上書きする。ファイルが存在しない場合は新規作成、存在する場合は全内容を上書き。Use when: 新しいファイルを作成する時、ファイルの全内容を置き換えたい時。Don't use when: 既存ファイルの一部だけを修正したい場合(→ edit_file を使う。上書きするとdiffが見えなくなる)。"
}
{
"name": "edit_file",
"description": "既存ファイルの特定箇所を検索して置換する。oldTextに完全一致する箇所をnewTextで置き換える。Use when: ファイルの一部を修正したい時、関数名を変更したい時、バグを1行修正したい時。Don't use when: ファイルを新規作成したい場合(→ write_file を使う)。ファイルの大部分を書き換えたい場合(→ write_file で全体を書き直す方が効率的)。"
}
Don't use when で「なぜ使わないか」の理由まで書く。「上書きするとdiffが見えなくなる」「全体を書き直す方が効率的」といった理由があると、LLMは文脈に応じて判断できる。
実運用での効果
僕がAIエージェントのスキルシステムを設計した時、各スキルに Negative Examples を導入しました。
description: Remotionで動画を作成する
Use when: PR動画、プロモーション動画、一般的な解説動画
Don't use when: プログラミング・技術コード解説動画
→ tech-guide-remotion を使う
Negative examples:
- ❌「Pythonの解説動画を作って」→ tech-guide-remotion
- ❌「git コマンドの使い方動画」→ tech-guide-remotion
これを入れる前は、技術解説動画の依頼が一般動画スキルに誤ルーティングされることが頻繁にありました。Negative Examplesを追加してからは、 誤発火がほぼゼロ になった。
重要なのは、「使わない場面」だけじゃなくて 「代わりに何を使うか」 まで書くこと。LLMは「これは違う」と言われたら「じゃあ何を使えばいいの?」と探し始める。その時に答えが書いてあれば、迷わない。
まとめ — ツール設計は「LLMへの思いやり」
5つのパターンをまとめます。
| # | パターン | 一言で | 即効性 |
|---|---|---|---|
| 1 | Naming | 動詞_名詞で名前をつける | ⭐⭐⭐ 5分で改善可 |
| 2 | Parameter | 必須は最小限、enumで制約 | ⭐⭐⭐ 5分で改善可 |
| 3 | Description | Use when / Don't use when を書く | ⭐⭐⭐ 10分で改善可 |
| 4 | Error Response | suggestion で次のアクションを導く | ⭐⭐ 実装変更あり |
| 5 | Negative Examples | 使わない場面と代替を明記 | ⭐⭐⭐ 10分で改善可 |
全部やる必要はない。 今日、1つだけツール定義を書き直してみてください 。
結局のところ、ツール設計って LLMへの思いやり なんだと思う。
「この名前でわかるかな」「このパラメータ、多すぎないかな」「エラーの時、次に何すればいいか伝わるかな」
...これ、人間同士のコミュニケーションと同じですよね。相手の立場に立って、わかりやすく情報を伝える。API設計の基本であり、ツール設計の本質でもある。
ツール定義を1つ書き直す。たったそれだけで、エージェント全体の振る舞いが変わる。そしてエージェントの精度が上がると、開発がどんどん楽しくなる。
...なんかそういうポジティブなループに入れたら、最高だなと思います。
補足: トークン消費について
ツールの description を充実させるとトークン消費が増えるのでは?という懸念はもっともです。確かに増えます。ただ、description を雑にして ツール選択ミス → リトライ → また失敗 のループに入る方がトータルのトークン消費は大きい。「一発で正しいツールを選ぶ」ための投資と考えると、description の充実はコスト削減にもなります。
ツール数が非常に多い場合(50個以上等)は、全ツールを毎回渡すのではなく、文脈に応じて 必要なツールだけを動的に選択して渡す 設計(Lazy Loading)も検討する価値があります。この辺りは別の記事で詳しく書こうかなと思っています。