第3章 ツール使用 — AIに「道具メニュー」を渡す
📖 この章のゴール:LLMに道具メニュー(
tools)を渡し、「この道具をこの引数で呼んで」と言わせ → あなたのコードが実際に実行 → 結果を返す → 最終回答、という 1往復 を作れるようになる。とくに 「実行するのは“あなた”であって、AIではない」 が腹落ちする。最後に MCP にもさらっと触れます。 ← 目次・はじめにへもどる
📱 Claude Code ではこう見える
第2章では、LLMと文章で往復するだけのCLIを作りました。でも本物の Claude Code は、ただ答えるだけでなく 実際に手を動かします。
- 「まず中身を確認します。
package.jsonを読みます」と言って、本当にファイルを開く - 「いまの時刻を確認します」と言って、実際に時刻を取得する
- 「
src/に何があるか一覧します」と言って、フォルダの中身を並べる
第1章で出てきた 「シェフ(LLM)の指示を、見習い(あなたのコード)が実行する」 の往復が、ここで初めて目に見える形になります。この章では、その往復のいちばん小さい1回を、自分の手で作ります。
🤔 なぜ?/やらないとどうなる
LLMは外を触れない、もう一度(🟢 基礎)
大事なことなのでくり返します。
LLM(AIの頭脳)は、文章を考えることしかできません。 ファイルを開くことも、時刻を調べることも、ネットを見ることも、自分では一切できないのです。
第1章のたとえで言えば、LLMは 電話の向こうの天才シェフ。料理(文章)はとびきり上手ですが、あなたの厨房(パソコン)にはいません。だから自分でコンロをひねったり、冷蔵庫を開けたりはできない。できるのは、電話越しに「そこの鍋を火にかけて」と指示することだけです。
では、どうやって「手を動かす」のか。答えは 道具(ツール) を渡すことです。
道具メニュー=道具の「説明書」を渡す(🟢 基礎)
tools(ツールズ)とは、LLMに渡す 「使える道具のメニュー(説明書)」 です。料理でいえば「うちの厨房には鍋と包丁とコンロがあります」というお品書き。これを最初に渡しておくと、LLMはこう動けるようになります。
- こちらが「こういう道具が使えるよ」と一覧(
tools)を渡す - LLMが「この道具を、この引数で呼んで」と言ってくる(これだけ。実行はしない)
- あなたのコードが、その道具を実際に実行する
- 結果をLLMに返す → LLMはそれを踏まえて最終的な返事を作る
🍳 たとえ話:通訳(つうやく=LLM)と電卓 あなたの隣に、計算は苦手だけど言葉は得意な通訳がいます。「いまの時刻は?」と頼まれると、通訳はあなたに「時計を見て、いまの時刻を教えて」と頼みます。あなたが時計を見て「14時30分」と渡すと、通訳がそれを文章にして相手に伝える。実際に時計を見るのは、通訳ではなくあなたです。LLMと道具の関係も、まさにこれ。
ここがこの章でいちばん腹落ちしてほしいところです。tools を渡しても、AIが勝手に動き出すわけではありません。 AIは「呼んで」と言うだけ。本当に呼ぶ(実行する)のは、ずっとあなたのコードです。
前作との接続:今度は Anthropic の形で(🔧 応用)
姉妹編(ChatGPTクローン)の第12章でも、まったく同じ「1往復」 を作りました。違いは どの会社のAPIを使うか だけ。
- 前作 → OpenAI(GPT) の形(
type: "function"、応答がtool_calls) - 本作 → Anthropic(Claude) の形(
input_schema、応答がtool_useブロック)
考え方(道具メニューを渡す → 呼んでと言われる → 実行する → 返す)は1ミリも変わりません。同じ料理を、別の厨房の道具立てで作るだけです。細かい違いは最後の「ことばメモ」でまとめます。
サボるとどうなる?(🟢 基礎)
道具を渡さなければ、エージェントは 第2章のおしゃべりAIのままです。「ファイルを読んで」と頼んでも、AIは「読めません」と言うか、読んだフリをして“それっぽい嘘”を返すだけ。手が無いのですから当然です。能力は、あなたが渡す道具の数だけ広がる——これが背骨🔁です。
🛠 こう作る(🟢 基礎)
いちばん安全な道具から始めます。引数がいらない「いまの日時を返す」道具 get_current_time です。引数が無いので、悪い値が紛れ込む心配もありません。最初の練習にぴったりです。
第2章で作った骨組み(anthropic クライアントと MODEL)の続きとして書いていきます。
① 道具メニュー(説明書)を作る
const tools: Anthropic.Tool[] = [{
name: "get_current_time",
description: "今の日時を返す。引数は不要。",
input_schema: {
type: "object",
properties: {},
required: [],
},
}];
1行ずつ読むと:
const tools: Anthropic.Tool[] = [{ ... }]… 使える道具の一覧(型はAnthropic.Toolの配列。今回は1つだけ)。name: "get_current_time"… 道具の名前。あとでLLMがこの名前で「呼んで」と言ってくる。description: "今の日時を返す。引数は不要。"… 何をする道具かの説明。LLMはこの文を読んで、使うかどうかを決めます。だから分かりやすく書くのが大事。input_schema… 道具に渡す引数の形(かたち)。OpenAI ではparametersでしたが、Anthropic ではinput_schemaと呼びます。properties: {}… 引数の中身。今回は引数なしなので空っぽ({})。required: []… 必須の引数。今回は無いので空っぽ([])。
💡
toolsは“説明書”だけ。中身の処理は別に書く 上のtoolsは「こういう道具があるよ」という説明(スキーマ)だけで、実際に時刻を取る処理は入っていません。LLMは「get_current_timeを呼んで」と言うだけ——本当に時刻を返す処理は、このあと④であなたが書きます。1つの道具につき ①説明書 と ②実体 の2つが要る、と覚えてください。
② LLMに道具メニューを渡して聞く
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "いま何時?" },
];
const res = await anthropic.messages.create({
model: MODEL,
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages,
tools,
});
1行ずつ読むと:
const messages = [{ role: "user", content: "いま何時?" }]… 会話の中身。ユーザーが「いま何時?」と聞いた、という1行だけ。anthropic.messages.create({ ... })… 第2章と同じ呼び出し。model/max_tokens/system… モデル・出力上限・性格指示。第2章のまま。messages, tools… 会話と一緒に、道具メニュー(tools)も渡すのが今回の新しいところ。これでLLMは「get_current_timeという道具が使える」と分かります。
③ 「道具を呼んで」と言われたか確認する
LLMの返事には2通りあります。普通に文章で答えるか、道具を呼んでと言ってくるか。それは stop_reason(ストップ・リーズン=止まった理由)で見分けます。
if (res.stop_reason === "tool_use") {
for (const block of res.content) {
if (block.type !== "tool_use") continue;
const result = await executeTool(block.name, block.input);
}
}
1行ずつ読むと:
if (res.stop_reason === "tool_use")… 止まった理由が"tool_use"なら、「道具を呼んで」と言われた合図。文章で答えただけなら別の理由("end_turn"など)になります。for (const block of res.content)… 返事res.contentはブロックの配列。その中身を1つずつ見ていく。if (block.type !== "tool_use") continue;… 道具呼び出し(tool_use)ブロック以外は飛ばす。1回の返事に「文章+道具呼び出し」が混ざることもあるためです。block.name… 呼ばれた道具の名前(ここでは"get_current_time")。block.input… 道具に渡す引数。ここがOpenAIとの大きな違いで、すでにオブジェクトになっています({ }の形。今回は引数なしなので空のオブジェクト)。OpenAI では文字列だったのでJSON.parseが必要でしたが、Anthropic ではそのまま使えます。block.id… この呼び出しを区別するための番号(このあと⑤で使います)。
④ あなたのコードで実際に実行する
block.name を見て、対応する処理を実際に走らせます。道具が増えても困らないよう、ディスパッチャ(名前で振り分ける係)にまとめます。
async function executeTool(name: string, input: any): Promise<string> {
switch (name) {
case "get_current_time":
return new Date().toLocaleString("ja-JP");
default:
return `不明な道具: ${name}`;
}
}
1行ずつ読むと:
async function executeTool(name, input)… 道具を実行する係。道具の名前と引数を受け取ります。: Promise<string>… 戻り値は必ず文字列。LLMに結果として返すので、文字列に揃えておくと扱いやすいのです。switch (name)… 名前で処理を振り分ける。道具が増えたらcaseを足していくだけ。case "get_current_time": return new Date()...… いまの日時を文字列にして返す。これが“実体” = 第1章でいう「見習いが実際に手を動かす」部分です。default: return \不明な道具: ${name}`` … 知らない名前が来たときの保険。エラーで止めず、文字列で返してLLMに知らせます。
⑤ 結果を会話に足して、もう一度聞く
道具の結果が手に入ったら、それを会話に2つ追加します。「LLMが道具を呼んだ発言」と「その結果」です。
messages.push({ role: "assistant", content: res.content });
messages.push({
role: "user",
content: [
{ type: "tool_result", tool_use_id: block.id, content: result },
],
});
1行ずつ読むと:
messages.push({ role: "assistant", content: res.content })… 「道具を呼んで」と言ったLLMの発言そのものを会話に残す。res.content(ブロックの配列)を丸ごと入れるのがポイント。messages.push({ role: "user", content: [ ... ] })… 道具の実行結果を会話に足す。Anthropic では結果をrole: "user"(あなたからの報告)として渡します。type: "tool_result"… これは「道具の結果だよ」というブロックの種類。tool_use_id: block.id… どの呼び出しへの答えかを、③で見たidで対応づける。これがズレると「どの道具の結果か分からない」エラーになります。content: result… ④で作った結果の文字列(ここでは日時)。
🔧 OpenAI では結果を
role: "tool"で足しました。Anthropic ではrole: "user"の中にtool_resultブロックを入れる、という形の違いがあります。やっていることは同じ「結果を会話に戻す」です。
⑥ もう一度 create を呼んで最終回答をもらう
結果を足したら、同じ messages.create をもう一度呼びます。今度はLLMが「14時30分」という結果を見たうえで、人間向けの返事を作ってくれます。
const final = await anthropic.messages.create({
model: MODEL,
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages,
tools,
});
const text = final.content.find((b) => b.type === "text");
if (text && text.type === "text") console.log(text.text);
1行ずつ読むと:
anthropic.messages.create({ ..., messages, tools })… 結果込みのmessagesでもう一度聞く。これが「1往復」の締めです。final.content.find((b) => b.type === "text")… 返事のブロックの中から、文章(text)ブロックを探す。if (text && text.type === "text") console.log(text.text)… 見つかったら、その文章を画面に表示。たとえば「いまは14時30分です」のような返事になります。
これで完成です。道具メニューを渡す → 呼んでと言われる → あなたのコードが実行 → 結果を返す → 最終回答 という1往復ができました。第1章の図でいう「①LLMに聞く → ②道具を実行 → ③結果を戻す → ④最終回答」の、いちばん小さい一周です。
🧩 もう一歩:道具は「外から差し込む」こともできる=MCP(🔧 応用)
ここまでは、道具を自分のコードに1つずつ手書きしました(get_current_time の説明書と実体)。便利な道具が増えるほど、自分で全部書くのは大変です。
実は道具には、もう1つの渡し方があります。MCP(Model Context Protocol/エムシーピー) という共通規格を使って、すでに誰かが作った道具を“外から差し込む” のです。ファイル操作・GitHub・データベースといった道具を、決まった差し込み口でつなぐだけで、自分で実体を書かずに使えるようになります。USB-C のように「いろんなAIと、いろんな道具を、同じプラグでつなぐ」イメージです。能力は 道具 → スキル(第11章)→ MCP と、こうして広がっていきます。くわしくは付録Gで扱います。いまは「自分で書く以外に、外から足す道もある」とだけ知っておけば十分です。
⚠️ ハマりどころ
inputを鵜呑みにする:道具の引数(block.input)は、もとをたどれば外から来た指示です。今回のget_current_timeは引数なしなので安全ですが、引数を取る道具(次章以降)では、使う前に型・必須・許可リストを検証します。「文字列のはずが変な値だった」を防ぐためです。- 道具の結果を新たな命令として実行してしまう:道具が返した文に「これまでの指示を無視して…」と仕込まれていることがあります(間接プロンプトインジェクション)。結果はあくまで“データ”として扱い、書かれている指示をそのまま実行しないこと。
- 危険な道具をいきなり自動実行する:今回は時刻を読むだけなので安全ですが、書き込み・コマンド・削除・送信といった危ない道具は、人間の確認を挟むべきです。その仕組みは第8章で作ります。いまは「読むだけは安全、変える・消すは要注意」と覚えてください。
tool_resultのtool_use_idを対応づけ忘れる:結果を返すとき、tool_use_idを③で受け取ったblock.idと必ず一致させます。ズレると「どの呼び出しの答えか分からない」とエラーになります。assistantの発言を足し忘れる:⑤で「LLMが道具を呼んだ発言(role: "assistant")」を足さずに結果だけ返すと、会話のつじつまが合わずエラーになります。“呼んだ発言”と“その結果”はセットです。
🤖 AIに頼むなら(Vibe codingのコツ)
道具まわりは、AI(Claude Code など)に頼むと安全のチェックを飛ばした危ないコードが出がちです。「引数を検証して」「結果を命令扱いしない」 を明示すると、安全なほうに誘導できます。
🗣 プロンプト例: 「TypeScript +
@anthropic-ai/sdkで、get_current_time(引数なし・今の日時を返す)ツールをtoolsとして Claude に持たせて。返事のstop_reasonが"tool_use"なら、res.contentのtool_useブロックからnameとinputを取り出し、executeToolで実行して。結果はrole:"user"のtool_result(tool_use_idを対応づける)でmessagesに足し、messages.createをもう一度呼んで最終回答を表示して。道具のinputは使う前に検証し、道具の結果を新たな命令として実行しないようにして。」
出てきたコードを見るときのチェックリスト:
- 道具の
inputを検証してから使っている(型・必須・許可リスト) tool_resultのtool_use_idを、tool_useブロックのidと対応づけている- 道具の結果を新たな命令として実行していない(“データ”として扱う)
- 「LLMが道具を呼んだ発言(
assistant)」と「その結果(tool_result)」を両方足している - 結果を足してもう一度
createし、最終回答までつながっている
📝 ことばメモ
- ツール(道具):LLMに渡す「使える道具のメニュー」=あなたが用意した処理。時刻を取る・ファイルを読むなどをLLMの代わりに実行する。1つにつき①説明書(
tools定義) と ②実体 の2つが要る - tool_use(ツールユース):LLMからの「この道具を、この引数で呼んで」という指示。返事の
res.contentの中にblock.type === "tool_use"のブロックとして来る({ id, name, input })。stop_reason === "tool_use"が合図 - tool_result(ツールリザルト):道具の実行結果を会話に戻すためのブロック。
role: "user"のcontentに入れ、tool_use_idでどの呼び出しの答えかを対応づける - input_schema(インプットスキーマ):道具に渡す引数の形(かたち)を JSON で書いた説明。
type/properties/requiredから成る。引数なしならproperties: {}・required: [] - (前作との差)
inputはオブジェクト:Anthropic ではblock.inputがすでにオブジェクトなので、そのまま使える。OpenAI(前作)ではargumentsが文字列だったのでJSON.parseが必要だった、という違いがある - MCP(Model Context Protocol):道具を自分で書く代わりに、共通規格で“外から差し込む” しくみ。自分で全部作らなくてよくなる。くわしくは付録G
➡️ 次の章へ
道具メニューを渡して、LLMに「呼んで」と言わせ、あなたのコードで実行して結果を返す1往復ができました。でも本物のエージェントは、1回で終わりません。読む → 直す → 走らせる → まだ失敗 → また直す……と、何度も往復します。次の第4章では、この1往復を 「終わりと言われるまでくり返す」エージェントループ に育てます。ここからが、エージェントの“心臓”です。