ぷらすのブログ

段階的な改善で学ぶ Agent の Tool 設計の基本

目次

こんにちは、ぷらす(@p1ass)です。

Agent の開発について学んだことを整理する連載の 2 本目です。連載の構成は次のとおりです。

  1. Agent の基本構成
  2. Tool の設計 (この記事)
  3. コンテキスト管理
  4. ハーネス
  5. Multi-Agent
  6. Workflow と Guardrails
  7. Observability と Eval

1 本目の記事では、Agent を Model、Instructions、Tool の 3 つの要素に整理し、Agent Loop を回して処理を実行することを学びました。

この記事では、その中から Tool を取り上げ、Agent に適した設計を整理します。

既存の API をそのまま Tool にした会議予約 Agent を実装し、Tool の定義とレスポンス、Tool の粒度の順に改善しながら、それぞれの段階で Agent の動きがどう変わるかを実行結果で確かめます。

題材には Anthropic の「Writing effective tools for agents」で例に挙げられている会議予約の Tool を使います。

Tool の定義とレスポンスが重要な理由

1 本目の記事で見たとおり、Model は Tool の定義 (名前、説明、引数の JSON Schema) を参照して、どの Tool をどんな引数で呼ぶか判断します。 そして、アプリケーションが返すレスポンスを受け取って、次の行動を決めます。

つまり、Model が Tool について確実に受け取れる情報は、定義とレスポンスの 2 つです。 Claude Code のように、ドキュメントやソースコードを自分で読みに行ける Agent もありますが、調べる分だけ Tool Call とターンが増えてしまいます。

したがって、Model が迷わず Tool を正しく使えるような定義とレスポンスを設計することがポイントです。

Tool の改善の進め方

この記事では Anthropic と OpenAI のドキュメントや事例をもとに、Tool を次の 3 つのバージョンで改善します。

  1. v1 では、既存の API をそのまま Tool にする。
  2. v2 では、Tool の定義とレスポンスを整理する。Model が引数とレスポンスを正しく判断できる形にする。
  3. v3 では、Tool の粒度を見直す。Agent に任せたい作業から逆算して、どのような Tool を用意するか再設計する。

各バージョンでは同じプロンプトで Agent を繰り返し動かし、ターン数や Tool Call の回数、消費したトークンを比べます。

既存の API をラップした会議予約 Agent (v1)

題材は社内の予定を調整する Agent です。 Anthropic の「Writing effective tools for agents」で例に挙げられている list_users、list_events、create_event の 3 つの Tool をもとに、Agent を実装します。

「来週月曜の午後に、坂本太郎さんと一ノ瀬さんとの 30 分の打ち合わせを入れてください」と依頼し、カレンダーに予定を登録させます。

社員データやカレンダーはコードの中にダミーデータとして用意しました。社員は 20 人で、そのうち「坂本」さんは 2 人います。 来週月曜 (2026-09-28) の 2 人の予定は次のように設定しました。12 時から 18 時の間で 2 人とも空いている 30 分の枠は、16:00〜16:30 の 1 つだけです。

時間坂本 太郎一ノ瀬 奈々
12:00-13:00ランチ MTG
13:00-14:00設計レビュー
14:00-15:00採用面接
15:00-16:001on1
16:00-16:30
16:30-18:00定例

Agent Loop は、1 本目の記事で Anthropic Messages API を使って実装したものと同じです。 Tool は次のような構造体にしました。

type Tool struct {
	Name        string         // 名前
	Description string         // 説明
	Properties  map[string]any // 引数の JSON Schema
	Required    []string       // 必須の引数
	Strict      bool           // strict mode を使うか

	Run func(ctx context.Context, input json.RawMessage) (string, error) // Tool Call を受けて実行する関数
}

Instructions とプロンプトは次のようにしました。

const (
	instructions = "あなたは社内の予定調整アシスタントです。今日は 2026-09-23 (水) で、時刻は日本時間 (JST) で扱います。依頼者本人の予定は考慮しなくてかまいません。登録する前に依頼者へ確認する必要はありません。"
	prompt       = "来週月曜の午後に、坂本太郎さんと一ノ瀬さんとの 30 分の打ち合わせを入れてください"
)

同じ依頼を繰り返し実行して計測するため、登録前の確認を省き、途中でループが止まらないようにしました。 実際に運用する Agent では、誤った相手を招待してしまわないような対策が必要です。

環境は次のとおりです。

v1 の Tool の定義

v1 では、API の名前、引数、レスポンスをそのまま Tool の定義にしました。説明はシンプルに 1 行だけです。

{
	Name:        "calendar_list_users",
	Description: "ユーザーの一覧を返す",
	Properties:  map[string]any{},
	Run:         ...,
},
{
	Name:        "calendar_list_events",
	Description: "ユーザーの予定を返す",
	Properties: map[string]any{
		"user":     map[string]string{"type": "string"},
		"time_min": map[string]string{"type": "string"},
		"time_max": map[string]string{"type": "string"},
	},
	Required: []string{"user", "time_min", "time_max"},
	Run:      ...,
},
{
	Name:        "calendar_create_event",
	Description: "予定を作成する",
	Properties: map[string]any{
		"title":     map[string]string{"type": "string"},
		"attendees": map[string]any{"type": "array", "items": map[string]string{"type": "string"}},
		"start":     map[string]string{"type": "string"},
		"end":       map[string]string{"type": "string"},
	},
	Required: []string{"title", "attendees", "start", "end"},
	Run:      ...,
},

レスポンスも API のものをそのまま JSON で返します。 calendar_list_users は全員の ID、名前、メールアドレス、部署、作成日時を、calendar_list_events は予定の ID、件名、参加者の ID、UTC の開始・終了時刻を、calendar_create_event は作成した予定の ID と状態を返します。

v1 の実行結果

v1 を実行したときのログです。

$ go run . -version v1 -n 30
turn 1: calendar_list_users {} -> [{"id":"usr_3a91c000-4f1c-4b7a-9e3d-00005d2e0000","name":"坂本 太郎","email":"[email protected]","department":"開発部","create...
turn 2: calendar_list_events {"user":"usr_3a91c000-4f1c-4b7a-9e3d-00005d2e0000","time_min":"2026-09-28T12:00:00+09:00","time_max":"2026-09-28T18:00:00+09:00"} -> [{"id":"evt_01","summary":"設計レビュー","attendees":["usr_3a91c000-4f1c-4b7a-9e3d-00005d2e0000"],"start":"2026-09-28T04:00:00...
turn 2: calendar_list_events {"user":"usr_3a91fdde-4f1c-4b7a-9e3d-00005d313232","time_min":"2026-09-28T12:00:00+09:00","time_max":"2026-09-28T18:00:00+09:00"} -> [{"id":"evt_03","summary":"ランチ MTG","attendees":["usr_3a91fdde-4f1c-4b7a-9e3d-00005d313232"],"start":"2026-09-28T03:00:0...
turn 3: calendar_create_event {"title":"打ち合わせ","attendees":["usr_3a91c000-4f1c-4b7a-9e3d-00005d2e0000","usr_3a91fdde-4f1c-4b7a-9e3d-00005d313232"],"start":"2026-09-28T16:00:00+09:00","end":"2026-09-28T16:30:00+09:00"} -> {"id":"evt_06","status":"confirmed"}
final: 来週月曜(9/28)の午後で、お二人とも空いている時間に打ち合わせを登録しました。

- **日時**: 2026年9月28日(月) 16:00〜16:30 (JST)
- **参加者**: 坂本 太郎さん、一ノ瀬 奈々さん
- **タイトル**: 打ち合わせ

午後(12:00〜18:00)のうち、30分の空きが重なるのはこの時間帯だけでした。

- 坂本さんは 13:00〜14:00 と 15:00〜16:00 に予定があります。
- 一ノ瀬さんは 12:00〜13:00、14:00〜15:00、16:30〜18:00 に予定があります。

「坂本」さんは他に坂本 修さん(営業部)もいます。ただ、「太郎さん」と指定があったため、坂本 太郎さん(開発部)で登録しました。
run 2: turns=4 calls=4 errors=0 input_tokens=10752 cache_read=7512 ok=true

Model は calendar_list_users で 2 人の ID を調べ、2 人の予定を並行して取得してから、空いている 16:00 に予定を登録しています。

各バージョンは 30 回ずつ実行し、run N: ... の行を集計しました。 v1 の結果は次のとおりです。

項目v1
正しく登録できた回数30 回中 30 回
ターン数の平均4.1
Tool Call の平均4.3 回
エラーが起きた回数30 回中 4 回
入力トークンの平均11,309 (最小 10,690、最大 14,686)

v1 は 30 回とも正しい枠に予定を登録できましたが、その過程では Model が多くの作業を担っています。

  • calendar_list_users のレスポンスから名前で 2 人を探し、ID を取り出す
  • UTC で返ってきた予定の時刻を JST に変換して、空いている枠を探す
  • 登録する予定の開始時刻と終了時刻を、タイムゾーン付きの RFC 3339 で組み立てる

また、30 回中 4 回は、calendar_list_events の user にメールアドレスを渡して 404 Not Found になり、次のターンで ID を渡し直していました。

turn 2: calendar_list_events {"user":"[email protected]","time_min":"2026-09-28T12:00:00+09:00","time_max":"2026-09-28T18:00:00+09:00"} -> 404 Not Found
turn 2: calendar_list_events {"user":"[email protected]","time_min":"2026-09-28T12:00:00+09:00","time_max":"2026-09-28T18:00:00+09:00"} -> 404 Not Found

エラーになった回は、ターンが 1 つ増え、入力トークンは 14,000 を超えています。

このように、正しく登録できた回数だけを見ていると完璧にみえますが、実際にはターン数が増えてコストが増えていることがあります。 以降のセクションでは、正答率に加えて、ターン数と入力トークンの変化を見ていきます。

Tool の定義とレスポンスの改善 (v2)

v2 では、Tool の数と役割は v1 のまま、Tool の定義とレスポンスを改善します。 v1 で Model が担っていた作業のうち、時刻の変換と組み立てを Tool 側に移し、判断に使わない情報をレスポンスから除いてみます。 引数のスキーマでは何を渡せばよいかを伝え、レスポンスでは次の行動に必要な情報を返します。

引数のスキーマ

引数の名前と型から渡すべき値が分かるようにします。 Anthropic の記事では、引数名について次のように書かれています。

instead of a parameter named user, try a parameter named user_id.

v1 では、user という名前から何を渡せばよいかが分からず、メールアドレスを渡してエラーになる回がありました。 v2 では引数名を user_id にし、説明に「calendar_list_users で調べた usr_ で始まる ID」と書きました。どの Tool のレスポンスを渡せばよいかが、引数の定義から分かります。 また、引数の型も目的に合うように変えました。

Toolv1 の引数v2 の引数
予定の一覧user、time_min、time_max (RFC 3339)user_id、date (YYYY-MM-DD)
予定の登録title、attendees、start、end (RFC 3339)title、attendee_user_ids、start (JST の YYYY-MM-DDTHH:MM)、duration_minutes

依頼は「月曜の午後に 30 分」なので、日付と長さで受け取れば、Model がタイムゾーン付きの時刻や終了時刻を組み立てる必要はありません。 v2 の calendar_list_events と calendar_create_event の定義は次のとおりです。

attendeeUserIDsSchema := map[string]any{
	"type":        "array",
	"items":       map[string]string{"type": "string"},
	"description": "参加者の user_id。calendar_list_users で調べた usr_ で始まる ID",
}

// ...

{
	Name:        "calendar_list_events",
	Description: "指定したユーザーの、指定した日の予定を返す。時刻はすべて日本時間 (JST)。空き時間を調べるために使う。",
	Properties: map[string]any{
		"user_id": map[string]string{"type": "string", "description": "calendar_list_users で調べた usr_ で始まる ID"},
		"date":    map[string]string{"type": "string", "description": "予定を調べる日付。YYYY-MM-DD 形式"},
	},
	Required: []string{"user_id", "date"},
	Strict:   true,
	Run:      ...,
},
{
	Name:        "calendar_create_event",
	Description: "参加者全員のカレンダーに予定を登録する。誰かの予定と重なる場合は登録せず、重なっている予定を返す。",
	Properties: map[string]any{
		"title":             map[string]string{"type": "string", "description": "予定の件名"},
		"attendee_user_ids": attendeeUserIDsSchema,
		"start":             map[string]string{"type": "string", "description": "開始日時 (JST)。YYYY-MM-DDTHH:MM 形式"},
		"duration_minutes":  map[string]any{"type": "integer", "description": "予定の長さ (分)"},
	},
	Required: []string{"title", "attendee_user_ids", "start", "duration_minutes"},
	Strict:   true,
	Run:      ...,
},

なお、Tool の使い方は例で示すより、引数そのものに意味を持たせるほうが効果的です。 Anthropic の「The new rules of context engineering for Claude 5 generation models」は、最新の Model では例を示すとかえって探索の幅を狭めると述べ、引数の設計で表現するよう勧めています。

Instead of using examples, think more about the design of your tools, scripts and files- what parameters does Claude have and how can they be more expressive?

また、v2 では strict mode を有効にしました。Tool の定義に strict: true を付けると、Tool の入力は必ずスキーマのとおりになります。

レスポンス

レスポンスには、Model が次の判断に使う情報を絞り込んで返します。 後続の Tool Call に必要な ID と、Model が判断に使う名前をあわせて返します。

v2 の calendar_list_users はメールアドレスや作成日時を省き、user_id と名前と部署を 1 人 1 行で返します。

usr_3a91c000-4f1c-4b7a-9e3d-00005d2e0000: 坂本 太郎 (開発部)
usr_3a91deef-4f1c-4b7a-9e3d-00005d2f9919: 坂本 修 (営業部)
usr_3a91fdde-4f1c-4b7a-9e3d-00005d313232: 一ノ瀬 奈々 (開発部)
...

v1 の予定の一覧 Tool では、予定の ID、参加者の ID、UTC の時刻を返していました。 v2 では、空き時間の判断に使わない予定の ID と参加者の ID を省き、時刻を JST に直して件名とあわせて返しています。 v1 と v2 のレスポンスを並べると、次のとおりです。

v1: [{"id":"evt_01","summary":"設計レビュー","attendees":["usr_3a91c000-..."],"start":"2026-09-28T04:00:00Z","end":"2026-09-28T05:00:00Z"},...]
v2: 坂本 太郎 の 2026-09-28 (月) の予定: 13:00-14:00「設計レビュー」、15:00-16:00「1on1」

予定の登録も、v1 は {"id":"evt_06","status":"confirmed"} を返していました。 v2 では、登録した件名、日時、参加者の名前を返します。Model は依頼どおりに登録できたかをこのレスポンスで確かめられます。

「打ち合わせ」を 2026-09-28 (月) 16:00-16:30 に登録しました。参加者: 坂本 太郎、一ノ瀬 奈々

なお v2 ではレスポンスの形式を JSON からテキストに変えましたが、テキストのほうが JSON より優れているというわけではありません。 Anthropic の記事は、XML、JSON、Markdown のどれが適しているかはタスクによって異なり、試して比べたうえで選ぶよう勧めています。

Even your tool response structure—for example XML, JSON, or Markdown—can have an impact on evaluation performance: there is no one-size-fits-all solution.

今回のレスポンスは返す項目が少なく、短い文にまとめられたのでテキストにしました。

v2 の実行結果

v2 を実行したときのログです。

$ go run . -version v2 -n 30
turn 1: calendar_list_users {} -> usr_3a91c000-4f1c-4b7a-9e3d-00005d2e0000: 坂本 太郎 (開発部)
usr_3a91deef-4f1c-4b7a-9e3d-00005d2f9919: 坂本 修 (営業部)
usr_3a91fdde-...
turn 2: calendar_list_events {"date":"2026-09-28","user_id":"usr_3a91c000-4f1c-4b7a-9e3d-00005d2e0000"} -> 坂本 太郎 の 2026-09-28 (月) の予定: 13:00-14:00「設計レビュー」、15:00-16:00「1on1」
turn 2: calendar_list_events {"date":"2026-09-28","user_id":"usr_3a91fdde-4f1c-4b7a-9e3d-00005d313232"} -> 一ノ瀬 奈々 の 2026-09-28 (月) の予定: 12:00-13:00「ランチ MTG」、14:00-15:00「採用面接」、16:30-18:00「定例」
turn 3: calendar_create_event {"attendee_user_ids":["usr_3a91c000-4f1c-4b7a-9e3d-00005d2e0000","usr_3a91fdde-4f1c-4b7a-9e3d-00005d313232"],"duration_minutes":30,"start":"2026-09-28T16:00","title":"打ち合わせ"} -> 「打ち合わせ」を 2026-09-28 (月) 16:00-16:30 に登録しました。参加者: 坂本 太郎、一ノ瀬 奈々
final: 来週月曜(9/28)16:00〜16:30 に「打ち合わせ」を登録しました。参加者は坂本 太郎さん(開発部)と一ノ瀬 奈々さん(開発部)です。

午後に2人とも空いている30分は、この時間帯だけでした。

- 坂本さん:13:00〜14:00 設計レビュー、15:00〜16:00 1on1
- 一ノ瀬さん:14:00〜15:00 採用面接、16:30〜 定例

なお、「坂本」は営業部にも坂本 修さんがいます。今回は「坂本 太郎さん」とのご指定どおり、坂本 太郎さんで登録しました。
run 1: turns=4 calls=4 errors=0 input_tokens=8366 cache_read=7980 ok=true

Tool の呼び出し方は v1 と同じで、ユーザーの一覧を取得し、2 人の予定を並行して取得してから登録しています。 30 回実行した結果の比較は次のとおりです。

項目v1v2
正しく登録できた回数30 回中 30 回30 回中 30 回
ターン数の平均4.14.0
Tool Call の平均4.3 回4.0 回
エラーが起きた回数30 回中 4 回30 回中 0 回
入力トークンの平均11,3098,414

v2 では 30 回ともエラーが起きず、入力トークンは v1 の約 74% になりました。v1 でエラーになった回で引数を渡し直していた Tool Call がなくなった分と、レスポンスを絞り込んだ分だけ値が減っています。

一方で、ターン数の平均は 4.0 で、v1 とほとんど変わっていません。ログを見ると、Tool の組み合わせ方も v1 と同じです。ユーザーの一覧から ID を探し、2 人の予定を取得し、空いている枠を探して登録するという手順を、Model が 4 ターンかけて組み立てています。空いている枠の計算も、Model が会話の中で行っています。

Tool の粒度の見直し (v3)

Anthropic の記事は、Agent に適しているかを考えずに既存の API をラップしただけの Tool をよく見られる誤りとして挙げています。そのうえで、次のようにまとめることを勧めています。

Instead of implementing a list_users, list_events, and create_event tools, consider implementing a schedule_event tool which finds availability and schedules an event.

OpenAI の Function calling のガイドにも、いつも続けて呼ぶ関数は 1 つにまとめるよう書かれています。

Combine functions that are always called in sequence.

ユーザーの検索や空き時間の計算は、入力が決まれば結果も決まる処理です。 こうした処理を Tool の中のコードで実行すれば、Model が手順を組み立てる必要がなくなり、ターン数が減ります。名前の取り違えや時刻の計算の誤りも起きず、全員の一覧や予定を Model が読み込む必要もありません。

v3 の Tool の定義

v3 では、Tool を calendar_find_free_slots と calendar_schedule_event の 2 つに作り直しました。

まとめるのは良いものの、1 つの Tool に複数の目的を持たせると、Model はその Tool をいつ、どう呼べばよいか判断しにくくなってしまうため、今回は 2 つにしました。

participantsSchema := map[string]any{
	"type":        "array",
	"items":       map[string]string{"type": "string"},
	"description": "参加者の名前。「坂本 太郎」のようなフルネームか、社内で 1 人に絞れる姓や名。依頼者本人は含めない",
}

// ...

{
	Name: "calendar_find_free_slots",
	Description: "指定した参加者全員の予定が空いている時間帯を、指定した日付の範囲で探す。" +
		"会議を登録する前に、候補の時間帯を決めるために使う。時刻はすべて日本時間 (JST)。" +
		"名前が複数の人に該当する場合は候補の一覧をエラーとして返すので、フルネームで指定し直す。",
	Properties: map[string]any{
		"participants":     participantsSchema,
		"date":             map[string]string{"type": "string", "description": "探す日付。YYYY-MM-DD 形式"},
		"duration_minutes": map[string]any{"type": "integer", "description": "会議の長さ (分)"},
		"earliest":         map[string]string{"type": "string", "description": "探し始める時刻。HH:MM 形式。午後なら 12:00"},
		"latest":           map[string]string{"type": "string", "description": "会議が終わっていなければならない時刻。HH:MM 形式。午後なら 18:00"},
	},
	Required: []string{"participants", "date", "duration_minutes", "earliest", "latest"},
	Strict:   true,
	Run:      ...,
},
{
	Name: "calendar_schedule_event",
	Description: "参加者全員のカレンダーに会議を登録する。" +
		"登録する時間帯は calendar_find_free_slots で空いていることを確かめてから指定する。" +
		"誰かの予定と重なる場合は登録せず、重なっている予定を返す。",
	Properties: map[string]any{
		"title":            map[string]string{"type": "string", "description": "会議の件名"},
		"participants":     participantsSchema,
		"start":            map[string]string{"type": "string", "description": "開始日時 (JST)。YYYY-MM-DDTHH:MM 形式"},
		"duration_minutes": map[string]any{"type": "integer", "description": "会議の長さ (分)"},
	},
	Required: []string{"title", "participants", "start", "duration_minutes"},
	Strict:   true,
	Run:      ...,
},

v2 から変えたのは次の 3 点です。

  • 参加者を user_id ではなく名前で受け取り、Tool の中でユーザーを探す。calendar_list_users が不要になる。
  • 予定の一覧ではなく、全員が空いている枠を返す。空き時間の計算はコードが行う。
  • Tool の説明に 2 つの Tool を使う順番を教える。

エラーの返し方

Tool が名前を受け取れるようにすると、同じ姓の人が複数いる場合に誰を指すのかがあいまいになってしまいます。 その場合は候補の一覧をエラーとして返し、Model にフルネームで指定し直してもらうようにしました。たとえば participants に「坂本」とだけ渡すと、次のエラーになります。

「坂本」に該当する人が 2 人います: 坂本 太郎 (開発部)、坂本 修 (営業部)。フルネームで指定してください

このアイデアは Anthropic の記事にも記載されており、分かりにくいエラーコードやトレースバックではなく、具体的で行動につながる改善点を伝えるよう勧めています。

you can prompt-engineer your error responses to clearly communicate specific and actionable improvements, rather than opaque error codes or tracebacks.

予定が重なったときも、409 Conflict のようなステータスだけではなく、重なっている予定の時間と件名を返します。 このように、どちらのエラーも例外とせず、Tool のレスポンスとして Model に返すことで、Model が自分で次の行動を決められます。

v3 の実行結果

v3 を実行したときのログです。

$ go run . -version v3 -n 30
turn 1: calendar_find_free_slots {"date":"2026-09-28","duration_minutes":30,"earliest":"12:00","latest":"18:00","participants":["坂本 太郎","一ノ瀬"]} -> 2026-09-28 (月) に 坂本 太郎、一ノ瀬 奈々 が全員空いている 30 分の枠: 16:00-16:30
turn 2: calendar_schedule_event {"duration_minutes":30,"participants":["坂本 太郎","一ノ瀬 奈々"],"start":"2026-09-28T16:00","title":"打ち合わせ"} -> 「打ち合わせ」を 2026-09-28 (月) 16:00-16:30 に登録しました。参加者: 坂本 太郎、一ノ瀬 奈々
final: 来週月曜(9/28)の 16:00〜16:30 に「打ち合わせ」を登録しました。参加者は坂本 太郎さんと一ノ瀬 奈々さんです。

月曜午後に、このお二人が両方空いていた 30 分の枠はここだけでした。
run 1: turns=3 calls=2 errors=0 input_tokens=4525 cache_read=3962 ok=true

Model は 1 ターン目で空いている枠を探し、2 ターン目で登録しています。 participants には依頼文のとおり 一ノ瀬 と渡していますが、Tool の中で 一ノ瀬 奈々 に絞り込めたため、そのまま空き枠を得られています。

結果は 30 回とも正しく登録でき、エラーは起きませんでした。

ここまでの改善を、各バージョンで変えたものと結果の対応で並べると、次のとおりです。

バージョンTool変更点ターン数の平均Tool Call の平均入力トークンの平均
v1calendar_list_users、calendar_list_events、calendar_create_eventなし (API をそのまま Tool にした)4.14.3 回11,309
v2calendar_list_users、calendar_list_events、calendar_create_event引数、レスポンス4.04.0 回8,414
v3calendar_find_free_slots、calendar_schedule_eventTool の粒度3.02.0 回4,596

v1 から v2 では、引数を誤ったエラーがなくなり、レスポンスが小さくなった分だけ入力トークンが減りました。 v2 から v3 では、ユーザーの検索や空き時間の計算のように、Model が組み立てていた Tool Call の回数が減りました。

Tool を改善するときに行ったこと

この記事では、Tool を改善するときに次の 3 つのことを行いました。

同じ依頼を繰り返し実行する

Agent は、同じ依頼でも毎回同じ行動をとるとは限りません。 そのため、1 回の実行結果だけを比べても、違いが Tool を改善した効果なのか、たまたまのばらつきなのかを区別できません。

この記事でも、v1 で引数を誤ってエラーになったのは 30 回中 4 回だけでした。数回の実行では、このエラーに気付かなかった可能性があります。

ログを読んで原因を探す

繰り返し実行してエラーが起きたことは分かっても、その原因までは分かりません。 Anthropic の「Writing effective tools for agents」では、Tool Call と Tool のレスポンスを含むトランスクリプトをそのまま読むよう勧めています。

Review the raw transcripts (including tool calls and tool responses) to catch any behavior not explicitly described in the agent’s CoT.

この記事でも、v1 のエラーの原因は、user にメールアドレスを渡しているログから判明しました。

正答率以外の指標も集計する

正答率だけでは、Tool を改善した効果を確かめられないことがあります。 同じ記事では正答率に加えて、Tool Call の回数やトークンの消費量、Tool のエラーも集計するよう勧めています。

As well as top-level accuracy, we recommend collecting other metrics like the total runtime of individual tool calls and tasks, the total number of tool calls, the total token consumption, and tool errors.

この記事では、Agent Loop の中でターン数、Tool Call とエラーの回数、入力トークンを集計しました。 v2 は 30 回とも正しく登録でき、エラーも起きませんでしたが、ターン数の平均は 4.0 のままでした。これが Tool の粒度を見直すきっかけになりました。

おわりに

この記事では、既存の API をそのまま Tool にした会議予約 Agent を題材に、Tool の定義とレスポンス、Tool の粒度の順に改善しました。

Tool の改善によって、ターン数の平均は 4.1 から 3.0 に、入力トークンの平均は 11,309 から 4,596 に減りました。

次回の記事では、コンテキスト管理についてまとめようと思います。