AI エージェント開発実践 #2 良いツールを設計する
第1回でループを堅牢にしました。ところが、同じループを使う二つのエージェントの性能が大きく分かれるなら、原因はほとんどの場合ツールです。Claude が見る世界は私たちが与えたツール一覧がすべてなので、ツールが曖昧だとエージェントも曖昧に動きます。今回はツールを設計する原則を整理します。
description はモデルが読むドキュメントです #
ツールの description はコメントではありません。Claude が「このツールを今使うか」を判断する唯一の根拠です。そのため、ツールが 何をするか だけを書くのでは足りず、いつ使うべきか まで書く必要があります。
# 不十分な例 — 何をするかだけ書いた
{
"name": "search_orders",
"description": "注文を検索する。",
...
}
# 良い例 — いつ使うか、何が返るか、限界まで
{
"name": "search_orders",
"description": (
"注文番号、顧客名、日付範囲で注文を検索する。"
"ユーザーが特定の注文の状態や内訳を尋ねたら、まずこのツールで注文を探す。"
"直近90日以内の注文のみ検索でき、最大20件を返す。"
),
...
}特に最新のモデルはツールを慎重に選んで使う傾向があるため、「ユーザーが X を尋ねたら、まずこのツールを呼ぶ」のようにトリガー条件を明示すると、ツールの使用率が目に見えて上がります。
スキーマは狭く、説明はパラメータごとに #
input_schema は自由度を与える場所ではなく、減らす場所です。受け取れる値が決まっているなら enum で固定し、本当に必要なパラメータだけを required に入れ、パラメータごとに description を付けます。
{
"name": "update_order_status",
"description": "注文の状態を変更する。状態変更の依頼を受けたときだけ使う。",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "注文番号。例: ORD-2026-0001",
},
"status": {
"type": "string",
"enum": ["paid", "shipped", "delivered", "cancelled"],
"description": "変更する状態。この四つ以外の値は存在しない。",
},
},
"required": ["order_id", "status"],
},
}status を自由な文字列のままにすると、Claude は「配送中」「shipping」「SHIPPED」のようなバリエーションを作り出します。enum で固定すれば、この問題はまるごと消えます。同じ理由で、日付は「YYYY-MM-DD 形式」のように形式を説明に明記しておくのが良いです。
エラーメッセージも設計の対象です #
第1回で、ツールのエラーは is_error の結果として返すことにしました。そのメッセージの品質がそのままエージェントの復旧能力になります。Claude はエラーメッセージを読んで次の行動を決めるので、次の行動を決められる情報 を盛り込む必要があります。
# 不十分な例 — 読んでも次の行動を決められない
return "エラーが発生しました。"
# 良い例 — 何が間違っていて、どう直すか
return (
"注文番号 'ORD-9999' が見つかりません。"
"search_orders ツールで、顧客名や日付からまず注文を検索してみてください。"
)人に見せるエラーメッセージを作るときと基準は同じです。違いがあるとすれば、エージェントはそのメッセージを本当に読み、そのとおりに行動するという点です。直し方を書いておけば、高い確率でその方法に従います。
ツールの危険度を分類します #
ツールを追加する前に、「このツールが誤って呼ばれたら何が起きるか」を一度自問しておくのが良いです。
| 分類 | 例 | 扱い方 |
|---|---|---|
| 読み取り | 検索、照会 | 自由に呼ばせる |
| 書き込み(取り消せる) | 状態変更、一時保存 | 入力を検証してから実行 |
| 書き込み(取り消しにくい) | 決済、削除、メール送信 | 実行前に人の承認を通す |
取り消しにくいツールは、呼び出し自体を禁止するのではなく、実行の直前に確認ステップをはさみます。ツール実行関数で分岐すれば十分です。
DANGEROUS_TOOLS = {"send_email", "delete_order", "refund_payment"}
def execute_tool(block) -> dict:
if block.name in DANGEROUS_TOOLS:
if not confirm_with_human(block.name, block.input):
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": "ユーザーがこの操作を承認しませんでした。別の方法を探してください。",
"is_error": True,
}
...拒否もエラー結果として返している点に注目してください。Claude は「承認されなかった」という事実を知り、計画を修正します。第7回の実践プロジェクトで、この承認フローを実際に実装します。
ツールの数と重なりを管理します #
ツールが増えるほど、Claude の選択は難しくなります。特に説明が重なるツールが問題です。search_orders と find_order が同居していると、どちらを使うか毎回ぶれます。点検の基準は二つです。
- 重なるツールは統合するか削除します。 似た仕事をするツール二つより、パラメータで分岐するツール一つのほうが良いです。
- 使わないツールは外します。 今回の作業と無関係なツールが一覧にあると、その分だけ誤って選ぶ余地が生まれます。作業の種類ごとにツール一覧を変えて構成するのも一つの方法です。
ツール一つひとつが良くても、一覧全体が混乱していればエージェントは迷います。ツール一覧はメニュー表のように、全体を見渡して磨く必要があります。
よくつまずくところ #
- description を開発者目線で書く — 「社内注文 API のラッパー」のような説明は、Claude にとって何の情報にもなりません。このツールで何が分かり、いつ使うのかを、利用者の視点で書きます。
- すべてのパラメータを required にする — 任意のパラメータまで必須にすると、Claude は知らない値をでっち上げて埋めます。本当に必要なものだけを必須にします。
- エラーを隠す — ツールが失敗したのに空の結果を返すと、Claude は「結果がなかった」と誤って受け取ったまま進みます。失敗は失敗だと伝えてこそ、復旧が始まります。
まとめ #
今回は、エージェントの性能を左右するツール設計を扱いました。
descriptionには、何をするかに加えていつ使うかを書きます。トリガー条件がツールの使用率を上げます。- スキーマは
enumとrequiredで狭め、エラーメッセージには次の行動を決められる情報を盛り込みます。 - ツールを危険度で分類し、取り消しにくいツールには人の承認をはさみます。
次回の「AI エージェント開発実践 #3 計画と自己修正」では、ツールを十分に備えたエージェントが複数段階の作業をどう計画し、途中の失敗をどう自分で立て直すかを扱います。