第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はこう動けるようになります。

  1. こちらが「こういう道具が使えるよ」と一覧(tools)を渡す
  2. LLMが「この道具を、この引数で呼んで」と言ってくる(これだけ。実行はしない)
  3. あなたのコードが、その道具を実際に実行する
  4. 結果を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_resulttool_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.contenttool_use ブロックから nameinput を取り出し、executeTool で実行して。結果は role:"user"tool_resulttool_use_id を対応づける)で messages に足し、messages.create をもう一度呼んで最終回答を表示して。道具の input は使う前に検証し、道具の結果を新たな命令として実行しないようにして。」

出てきたコードを見るときのチェックリスト:

  • 道具の input を検証してから使っている(型・必須・許可リスト)
  • tool_resulttool_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往復を 「終わりと言われるまでくり返す」エージェントループ に育てます。ここからが、エージェントの“心臓”です。

第4章 エージェントループ — 使ったら結果を返して、また考えさせる へすすむ →

← 目次・はじめにへもどる