レッスン6: ハンズオン: 3つのツールをエージェントに接続する
学習目標:
- エージェントを実際に動作させる完全なツール実行ループを書く
- ツールのインターフェース定義と実装を1つのテーブルに登録し、2つの側面が決して乖離しないようにする
- ループに安全弁を取り付け、ログを読んでツールが誤接続されているタイミングを判断する
前提: レッスン1〜5を修了し、基本的なJavaScript / Node.jsを読むことができる | 前: << レッスン5
まずは結果: 1回の完全な実行
これがこのレッスンが構築するものです。ターミナルに1つの文を入力すると、エージェントは自分でどのツールを呼び出し、何回呼び出すかを決定します:
3つのターン、3つのツール、そして各ターンの引数は前のターンの結果に基づいて構築されます: まずlodashが現れるファイルを見つけ、次にpackage.jsonを読んでバージョンを確認し、次にその名前を取得してGitHubについて尋ねます。これはハードコードされたスクリプトではありません — モデル自身が次にどのツールを呼び出し、どの引数を渡すかを決定します。
このレッスンはそれをゼロから構築します: 3つのツール、1つのレジストリ、1つの実行ループ、いくつかの安全弁。
裏で何が起こっているか: 次々と続くAPIのラウンドトリップ
上で見た各「ターン」は、裏では完全なHTTPリクエストです。レッスン2「ツール呼び出しの完全なラウンドトリップ」は、単一のツール呼び出しのラウンドトリップがどのように見えるかを示しました。ここでは、それをループに接続するだけです — モデルがstop_reason: "tool_use"を返し、あなたのコードがツールを実行し、結果を会話に戻し、モデルがツール呼び出しを要求しなくなるまで、別のリクエストを送信します。1
3つのツール呼び出しターンは、実際にはmessages.createへの4回の呼び出しです: 最初の3回でモデルはツールを要求し続け、4回目にはGitHubのデータを持ち、十分だと判断し、テキスト回答を直接提供し、ループを終了します。ツールを要求し続けるかどうかの判断は完全にモデル側にあります。あなたのコードは実行して結果を返すだけです。
ステップ1: 各ツールの契約を書く
レッスン4「ツールインターフェースの設計: 名前、説明、パラメータ、戻り値」は、ツールインターフェースの3つのコアフィールドをカバーしました: name、description、input_schema。2ここでは、それらを直接コードに変換します。3つのツールは、レッスン3「5つの一般的なツールタイプ: 読む、書く、実行する、検索する、呼び出す」の5つのツールタイプのうち3つにマップされます: search、read、call — writeとexecuteは、演習で接続してもらいます。
github_repo_infoにはgithub_プレフィックスが付いています — 公式のガイダンスは、ツールが外部サービスに触れるときはツール名にサービス名を付けることで、モデルが間違ったツールを選ぶ可能性を大幅に下げます。3search_filesとread_fileはローカルファイルシステムで動作し、「どのサービス」という曖昧さがないため、プレフィックスは不要です。
3つすべての説明は、何も見つからなかったときに返ってくるテキストを明示しており、これは単なる埋め草ではありません。レッスン4は、良い説明は入力と出力の曖昧さを取り除くことを指摘しました。4ここでの曖昧さはパラメータにあるのではなく、ツールが「何も見つかりませんでした」をどのように表現するかにあります — 「安全弁」セクションで発動する罠です。
ステップ2: 契約と実装を1つのテーブルに登録する
よくある罠: スキーマリストと実行時に使用されるハンドラールックアップテーブルが2つの別々のコピーとして書かれている場合、遅かれ早かれそれらは乖離します。search_filesをfind_in_filesに名前変更しても、ハンドラーテーブルのキーを更新するのを忘れます。モデルは新しいスキーマに対して呼び出しを発行し、ハンドラーテーブルにはそのキーの下に何もなく、スローします。
修正は、name、description、input_schema、および実際に実行される関数がすべて同じオブジェクトに座る単一のテーブルを維持することです。APIが必要とするスキーマリストと実行が必要とするハンドラールックアップテーブルは、この1つのテーブルから派生します:
toolSchemasとtoolHandlersは永遠に同期を保ちます。なぜなら、それらは同じデータから計算される2つのビューであり、2つの手書きのコピーではないからです。ツールの名前を変更したり、パラメータを追加したりすることは、TOOLSを正確に1か所で変更することを意味します。
ステップ3: 3つのツールを境界付きで実装する
searchFilesは、grepにシェルアウトするのではなく、ディレクトリ自体を歩きます — これにより、ユーザー入力をコマンドラインに接合してコマンドインジェクションを招くことを回避します。ヒット数は制限されているため、単一の検索がコンテキストに数千行を詰め込むことはできません:
readFileは1つのことを行います: ターゲットパスがプロジェクトルートから逃げていないことを確認します。レッスン5からの境界のアイデアは、セパレータを持つ単一のプレフィックスチェックとしてここに現れます。単純なstartsWith(PROJECT_ROOT)ではないことに注意してください: プロジェクトルートが/Users/me/projで、モデルが../proj-backup/xを渡す場合、解決後に/Users/me/proj-backup/xを取得し、単純なプレフィックスマッチはまだ通過します — path.sepを追加すると、境界は最終的にディレクトリセパレータに着地します:
githubRepoInfoは、プロジェクト外にデータを送信する唯一のツールです — ローカルファイルコンテンツが、モデルによって2つの文字列ownerとrepoに凝縮され、次に公開インターネットに送信されます。これは、「プライベートデータを読む」と「外部に通信する」という2つの高リスク条件が満たされる正確なシナリオです。5そのため、明示的な権限ルールを取得します: 引数はGitHubの有効な命名形式に一致しなければならず、他のものは許可されません:
GITHUB_TOKENは環境変数から読み取られ、決してコードに現れません。それなしでも実行されますが、匿名リクエストのレート制限が低くなります。これは、レッスン5の権限ルールと同じアイデアを異なる形式で表したものです: そのレッスンでは、Claude Codeの設定ファイル内の宣言的なallow/deny/askルールをカバーしました。6これは、ツールコードに書き込まれた命令的なバージョンです — どちらも、高リスク操作が越えることができない線を引きます。7
ステップ4: 実行ループを書く
toolSchemasとtoolHandlersを手に入れれば、ループ自体は複雑ではありません。コアロジックは4つのステップです: リクエストを送信し、stop_reasonを見て、それがtool_useでない場合はテキストを返し、そうである場合は、すべてのツール呼び出しブロックを実行し、結果を戻します。1
ここには見逃しやすい詳細があります: for (const block of response.content)はこのターンに返されたすべてのコンテンツブロックを反復し、最初のものだけではありません。モデルは多くの場合、1つのターンで2つまたは3つのツールを並行してリクエストします。それぞれが実行され、独自のtool_resultを生成する必要があり、tool_use_idが1対1でマッチし、1つも欠けてはいけません。8レベル2の演習では、1つを見逃す罠を直接体験します。
安全弁、そしてツールが誤接続されているタイミングを判断する方法
上記のループは実行されますが、2つの安全策が欠けています。それらを追加します:
**安全策1: ツールの失敗はフィードバックされなければならず、ループをクラッシュさせることは許されません。**生の呼び出しをtry/catchでラップし、失敗時にもまだtool_resultを生成し、ただis_error: trueでマークします — モデルがそのマークを見ると、通常、引数を調整して再試行し、同じエラーを繰り返すのではありません。9 8
**安全策2: 同じ引数で同じツールが3回連続で呼び出された場合、停止する必要があります。**これは推測ではありません — 最後のいくつかの呼び出しのシグネチャを記録することに基づいています:
マスタースイッチとしてのMAX_TURNSと一緒に、3つの安全弁には異なる仕事があります: MAX_TURNSは「モデルが新しいバリエーションでツールを要求し続け、決して停止しない」のを防ぎます。繰り返し呼び出し検出は「モデルが同じ引数でスピンし続ける」のを防ぎます。そして、ツールの内部パスとフォーマットチェック(ステップ3で書かれたもの)は「モデルが範囲外の引数を作り上げ、ツールがそれをとにかく実行する」のを防ぎます。3つのレイヤーのいずれかを落とすと、ループは暴走したり、踏み越えたりするリスクがあります。7
**ログからツールが誤接続されていることをどのように判断しますか?**最も一般的な2つのシグナル:
- モデルが同じツールを何度も呼び出す、引数は狭い範囲内でのみ変化します(大文字小文字の変更、単語の追加または削除)。10回中9回、モデルは愚かではありません —
tool_resultのコンテンツが曖昧すぎるのです。「見つかりませんでした」が空の文字列を返すと、モデルは「本当に何もない」のか「ツールが壊れている」のかを判断できず、推測して再試行するしかありません。
- モデルが引数を推測して入力する、例えば、存在しないパスを
read_fileに渡します。トレースバックすると、通常、2つの原因のいずれかが判明します: descriptionが引数がどこから来るべきかを明示しなかった(レッスン4のエコー)、または前のツールの出力が正確なパスを提供せず、モデルが1つを発明する必要があった。
まとめ
- ツールのスキーマとハンドラーを同じテーブル(
TOOLS)に登録し、toolSchemasとtoolHandlersの両方がそこから派生するため、1か所を変更しても、もう1か所が変更されないままになることはありません
- 実行ループのコアは: リクエストを送信 →
stop_reasonがtool_useかどうかを確認 → そうである場合、すべてのツール呼び出しブロックを反復し、実行し、tool_resultを戻す → そうでない場合、テキストを返してループを終了
- 1つのターンに複数の並列ツール呼び出しがある場合があります。すべての
tool_useには一意に一致するtool_resultが必要で、1つ欠けると次のリクエストがエラーになります
- 3つの安全弁はそれぞれレイヤーを守ります:
MAX_TURNSはモデルが無期限にツールを要求するのを止め、繰り返し呼び出し検出はモデルが同じ引数セットでスピンするのを止め、ツールの内部パスとフォーマットチェックは範囲外の引数を止めます
tool_resultのコンテンツは「見つかりませんでした」と「エラーが発生しました」を明確に述べる必要があります。曖昧な空の戻り値は、モデルが何度も再試行し、ログがツールが誤接続されているように見える第1の原因です
このコースの6つのレッスンすべてを修了しました。「なぜエージェントにツールが必要か」から、自分で動作するツール実行ループを書くまで。次に行う最も価値のあることは、別のレッスンを読むことではありません — 自分のプロジェクトから小さな実際のタスクを選び、それを2つまたは3つのツールに分解し、このループのスケルトンをいくつかの調整で持ち越すことです。一度動かすことは、10の説明を読むことに勝ります。デバッグ中で特定のフィールドについて確信が持てない場合は、sources.mdに戻り、S4とS5を確認してください。それらは、このマルチターンループの最も主要な仕様テキストです。