AI エージェント開発実践 #2 良いツールを設計する

読了 5分

第1回でループを堅牢にしました。ところが、同じループを使う二つのエージェントの性能が大きく分かれるなら、原因はほとんどの場合ツールです。Claude が見る世界は私たちが与えたツール一覧がすべてなので、ツールが曖昧だとエージェントも曖昧に動きます。今回はツールを設計する原則を整理します。

description はモデルが読むドキュメントです #

ツールの description はコメントではありません。Claude が「このツールを今使うか」を判断する唯一の根拠です。そのため、ツールが 何をするか だけを書くのでは足りず、いつ使うべきか まで書く必要があります。

tool_description.py
# 不十分な例 — 何をするかだけ書いた
{
    "name": "search_orders",
    "description": "注文を検索する。",
    ...
}

# 良い例 — いつ使うか、何が返るか、限界まで
{
    "name": "search_orders",
    "description": (
        "注文番号、顧客名、日付範囲で注文を検索する。"
        "ユーザーが特定の注文の状態や内訳を尋ねたら、まずこのツールで注文を探す。"
        "直近90日以内の注文のみ検索でき、最大20件を返す。"
    ),
    ...
}

特に最新のモデルはツールを慎重に選んで使う傾向があるため、「ユーザーが X を尋ねたら、まずこのツールを呼ぶ」のようにトリガー条件を明示すると、ツールの使用率が目に見えて上がります。

スキーマは狭く、説明はパラメータごとに #

input_schema は自由度を与える場所ではなく、減らす場所です。受け取れる値が決まっているなら enum で固定し、本当に必要なパラメータだけを required に入れ、パラメータごとに description を付けます。

tool_schema.py
{
    "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 はエラーメッセージを読んで次の行動を決めるので、次の行動を決められる情報 を盛り込む必要があります。

tool_error_message.py
# 不十分な例 — 読んでも次の行動を決められない
return "エラーが発生しました。"

# 良い例 — 何が間違っていて、どう直すか
return (
    "注文番号 'ORD-9999' が見つかりません。"
    "search_orders ツールで、顧客名や日付からまず注文を検索してみてください。"
)

人に見せるエラーメッセージを作るときと基準は同じです。違いがあるとすれば、エージェントはそのメッセージを本当に読み、そのとおりに行動するという点です。直し方を書いておけば、高い確率でその方法に従います。

ツールの危険度を分類します #

ツールを追加する前に、「このツールが誤って呼ばれたら何が起きるか」を一度自問しておくのが良いです。

分類扱い方
読み取り検索、照会自由に呼ばせる
書き込み(取り消せる)状態変更、一時保存入力を検証してから実行
書き込み(取り消しにくい)決済、削除、メール送信実行前に人の承認を通す

取り消しにくいツールは、呼び出し自体を禁止するのではなく、実行の直前に確認ステップをはさみます。ツール実行関数で分岐すれば十分です。

dangerous_tool_gate.py
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_ordersfind_order が同居していると、どちらを使うか毎回ぶれます。点検の基準は二つです。

  • 重なるツールは統合するか削除します。 似た仕事をするツール二つより、パラメータで分岐するツール一つのほうが良いです。
  • 使わないツールは外します。 今回の作業と無関係なツールが一覧にあると、その分だけ誤って選ぶ余地が生まれます。作業の種類ごとにツール一覧を変えて構成するのも一つの方法です。

ツール一つひとつが良くても、一覧全体が混乱していればエージェントは迷います。ツール一覧はメニュー表のように、全体を見渡して磨く必要があります。

よくつまずくところ #

  • description を開発者目線で書く — 「社内注文 API のラッパー」のような説明は、Claude にとって何の情報にもなりません。このツールで何が分かり、いつ使うのかを、利用者の視点で書きます。
  • すべてのパラメータを required にする — 任意のパラメータまで必須にすると、Claude は知らない値をでっち上げて埋めます。本当に必要なものだけを必須にします。
  • エラーを隠す — ツールが失敗したのに空の結果を返すと、Claude は「結果がなかった」と誤って受け取ったまま進みます。失敗は失敗だと伝えてこそ、復旧が始まります。

まとめ #

今回は、エージェントの性能を左右するツール設計を扱いました。

  • description には、何をするかに加えていつ使うかを書きます。トリガー条件がツールの使用率を上げます。
  • スキーマは enumrequired で狭め、エラーメッセージには次の行動を決められる情報を盛り込みます。
  • ツールを危険度で分類し、取り消しにくいツールには人の承認をはさみます。

次回の「AI エージェント開発実践 #3 計画と自己修正」では、ツールを十分に備えたエージェントが複数段階の作業をどう計画し、途中の失敗をどう自分で立て直すかを扱います。

X