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

📖 この章のゴール:前章で作った 「道具を1回だけ使う1往復」 を、for(または while)のループに育てる。stop_reason(ストップ・リーズン=止まった理由)が "end_turn"(えんど・たーん=もう話すことはない)になるまで、「考える→道具→結果→また考える」をぐるぐる回す。これこそがエージェントの心臓。あわせて、暴走しないための回数の上限もつける。 ← 目次・はじめにへもどる


📱 Claude Code ではこう見える

第1章でも見た、あの動き。もう一度ゆっくり眺めてみます。Claude Code に「このテスト直して」と頼むと——

  1. ファイルを読む(道具を1回使った)
  2. 結果を見て「ここが原因だな」と考え、コードを直す(また道具を使った)
  3. npm test走らせる(また道具)
  4. 結果を読むと……まだ失敗している。「じゃあ別のところを直そう」とまた直す
  5. もう一度走らせる → 今度は成功
  6. 「直りました」と報告して止まる

注目してほしいのは、1回道具を使って終わりではないこと。読む→直す→走らせる→まだ失敗→また直す→また走らせる…… と、何度も何度も往復しています。そして「もう直った(=やることが無くなった)」と判断するまで、勝手には止まりません

前章まででは、AIに「道具メニュー」を渡し、AIが道具を1つ選び、それを実行して結果を返す——という たった1往復 を作りました。でも本物のエージェントは、その結果を見てまた考え、必要ならまた道具を使う。この「結果を見て、また考える」のくり返しが、今日の主役です。


🤔 なぜ「1往復」では足りないのか(🟢 基礎)

前章の1往復を、もう一度図にしてみます。

あなたの入力 → LLMに相談 → LLMが道具を指示 → 道具を実行 → 結果をLLMに返す → おわり

最後が「おわり」で終わっているのが問題です。結果をLLMに返したのに、そこで会話を打ち切ってしまっている。これでは、せっかく道具の結果をLLMに見せても、LLMがそれを使って次の一手を打つ前に終わってしまうのです。

人間にたとえるなら、料理人に「まず冷蔵庫の中を見て」と頼んで、見た瞬間に電話を切るようなもの。中を見たうえで「じゃあ次に何をするか」を聞かなければ、料理は一向に進みません。

だから必要なのは、たった一つの発想の転換です。

道具の結果を返したら、終わりにせず「で、次はどうする?」ともう一度LLMに聞く。 そしてまた道具を指示されたら、また実行して、また結果を返して、また聞く。 これを LLMが「もう道具はいらない、これで完成」と言うまでくり返す。

この 「結果を見て、また考える」のくり返しエージェントループ と呼びます。これが、ただのチャットをエージェントに変える、たった一つの仕掛けです。

「まだ道具を使いたい」か「もう完成」かを、どう見分ける?(🟢 基礎)

ループを回すには、いつ止めるかを知る必要があります。その合図が、API の返事に入っている stop_reason(止まった理由) です。前章でも少し出てきましたね。大事なのは次の2つです。

stop_reason の値 意味 エージェントがすべきこと
"tool_use"(つーる・ゆーず) LLMが「まだ道具を使いたい」と言っている 道具を実行して、結果を返し、もう一度LLMに聞く(ループ続行)
"end_turn"(えんど・たーん) LLMが「もう話すことはない=完成」と言っている 最終回答を表示して、ループを終わる

つまりループの判断はとてもシンプルです。

stop_reason"tool_use" のあいだは回し続ける。"end_turn"(など "tool_use" 以外)になったら止める。

「考える → 道具 → 結果 → また考える → 道具 → 結果 → …… → もう道具いらない(end_turn)→ おわり」。これだけ。

🛡 でも、止まらなかったら?(🟢 基礎・背骨)

ここで背骨の 🛡 安全 が顔を出します。「end_turn になるまで回す」と聞いて、こう思いませんでしたか——もし end_turn がずっと来なかったら?

実際に起こります。AIが同じ道具を延々と呼び続けたり、堂々めぐりに陥ったりすることがあるのです。そうなると、ループは永遠に止まりません。これが 無限ループ

そして、エージェントの無限ループはただ固まるだけでは済みません。ループが1回まわるたびに LLMのAPIを1回呼ぶ=お金がかかるからです。止まらないループは、気づかぬうちに料金をどんどん積み上げます(コストの話は付録F)。

🔑 だから鉄則:ループには必ず「最大何回まで」の上限をつける。 たとえば「最大10回」と決めておけば、たとえAIが暴走しても10往復で必ず止まる。安全弁です。これは毎章のチェックリストにも入る、エージェントの基本作法です。

end_turn で正常終了。上限回数で強制終了」——この2つの出口を用意するのが、安全なループの形です。


🛠 こう作る(🟢 基礎)

では、前章までの部品(anthropicMODELmessagestools)を使って、エージェントの心臓——runAgent 関数を組み立てます。これが本書で以降ずっと使う形です。

① エージェント本体:runAgent

async function runAgent(userInput: string) {
  messages.push({ role: "user", content: userInput });

  for (let step = 0; step < 10; step++) { // 暴走防止に上限10回
    const res = await anthropic.messages.create({
      model: MODEL, max_tokens: 1024, system: SYSTEM_PROMPT, messages, tools,
    });
    messages.push({ role: "assistant", content: res.content });

    if (res.stop_reason !== "tool_use") {
      printText(res); // 最終回答を表示して終了
      return;
    }

    const toolResults: Anthropic.ToolResultBlockParam[] = [];
    for (const block of res.content) {
      if (block.type !== "tool_use") continue;
      const result = await executeTool(block.name, block.input);
      toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result });
    }
    messages.push({ role: "user", content: toolResults });
  }
}

1行ずつ読むと:

  • async function runAgent(userInput: string) { … エージェント本体。あなたが打った1つの入力(例:「このファイルを読んで」)を受け取って、完了するまで面倒を見る関数です。
  • messages.push({ role: "user", content: userInput }); … まず、あなたの発言を会話履歴(messages)に追加します。LLMはこの履歴を丸ごと読んで考えるので、何を入れるかが大事でした。
  • for (let step = 0; step < 10; step++) { … ここがループstep が 0,1,2,… と増えながら、最大10回までくり返します。この < 10 が、さっき話した暴走防止の上限(安全弁)です。
  • const res = await anthropic.messages.create({ … }); … LLMに「いまの履歴を見て、どうする?」と相談します。messages(履歴)と tools(道具メニュー)を毎回いっしょに渡しているのがポイント。
  • messages.push({ role: "assistant", content: res.content });LLMの返事を、まるごと履歴に追加します。res.content は文字どおり全部(テキストも、道具を使いたいという指示も)。ここが超重要なので、後で「ハマりどころ」でもう一度だけ強調します。
  • if (res.stop_reason !== "tool_use") { … 「LLMはもう道具を使いたいわけじゃない」=ふつうは "end_turn"(完成)です。
  • printText(res); … 完成したので、最終回答を画面に表示します(中身は前章までの「テキストブロックを取り出して表示」する関数です)。
  • return; … そして関数を抜けて終わり。これが「end_turn で正常終了」の出口です。
  • const toolResults: Anthropic.ToolResultBlockParam[] = []; … ここから下は「まだ道具を使いたい」= stop_reason"tool_use" のとき。結果をためる空の箱を用意します。複数の結果を入れられるよう、配列にしているのがミソ。
  • for (const block of res.content) { … LLMの返事(複数のブロックの集まり)を1つずつ見ていきます。テキストのブロックも、道具を使いたいブロックも混じっています。
  • if (block.type !== "tool_use") continue; … 道具の指示以外(ただのテキストなど)は読み飛ばします。
  • const result = await executeTool(block.name, block.input); … 道具を実際に実行します。block.name(道具の名前)と block.input(引数。前章で見たとおりもうオブジェクトになっているJSON.parse 不要)を、次に作る executeTool に渡します。結果は文字列で返ってきます。
  • toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result }); … 実行結果を「どの道具呼び出しへの返事か」(tool_use_id)とセットで箱に入れます。この id をそろえないと、LLMはどの結果がどの依頼への返事か分からなくなります。
  • messages.push({ role: "user", content: toolResults });集めた結果を、ユーザーの発言として履歴に追加します(道具の結果は role: "user" 側で返すのが Anthropic の流儀)。
  • }(forの閉じ) … これでループの先頭(create)に戻り、結果を踏まえてLLMにもう一度相談します。これが「結果を見て、また考える」の正体です。

💡 図にすると、たったこれだけ。

          ┌──────────────────────────────┐
          ▼                              │
  ① LLMに相談 (create)                    │
          │                              │
  stop_reason は?                        │
    ├─ "end_turn" → 表示して return(おわり)│
    └─ "tool_use" → ② 道具を実行 ───────────┘
                      ③ 結果を履歴に追加して①へ
  (ただし最大10回まで。超えたら強制終了)

② 道具を実行する係:executeTool

runAgent が「read_file を実行して」と言われたとき、実際に手を動かすのがこの関数です。前章までの形をそのまま使い、ここでは安全な道具を2つだけ用意します(本物のファイル操作は次章から)。

async function executeTool(name: string, input: any): Promise<string> {
  switch (name) {
    case "get_current_time":
      return new Date().toString();
    case "add":
      return String(input.a + input.b);
    default:
      return `不明な道具: ${name}`;
  }
}

1行ずつ読むと:

  • async function executeTool(name: string, input: any): Promise<string> { … 道具の実行ディスパッチャ(振り分け係)。道具の名前 name と引数 input を受け取り、結果を文字列で返します。「結果は必ず文字列」がコツ——LLMにそのまま渡せるからです。
  • switch (name) { … 名前を見て、どの道具を動かすか振り分けます。道具が増えても case を足すだけ。
  • case "get_current_time": … 「今の日時を返す」道具(前章で定義した安全な道具)。
  • return new Date().toString(); … いまの日時を文字列にして返すだけ。何も壊さない=安全な道具です。
  • case "add": … 「2つの数を足す」道具。引数で ab をもらう想定です。
  • return String(input.a + input.b); … 足し算の結果を文字列にして返します(String(...) で数値を文字列へ)。これも読むだけで何も壊しません。
  • default: … メニューに無い名前を言われたとき。
  • return \不明な道具: ${name}`;` … エラーで落とさず、「そんな道具は無い」と文字列で返します。落とさないのが大事——LLMはこの返事を読んで「あ、その道具は無いのか」と方針を変えられます。

🔧 応用(読み飛ばし可):道具の中で失敗しそうな処理(ファイル読み込み・通信など)は、try { … } catch (e) { return エラー: ${…} ; } で囲み、例外を投げる代わりに文字列で返すのが定石です。そうすればループは止まらず、LLMがエラー内容を読んで自分でリカバリしようとします。次章から本物の道具を作るとき、この形が効いてきます。

③ 動かしてみる

第2章で作った入力ループから runAgent を呼べば完成です(再掲)。

import * as readline from "node:readline/promises";
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
while (true) {
  const userInput = await rl.question("> ");
  if (userInput === "exit") break;
  await runAgent(userInput);
}
rl.close();

1行ずつ読むと:

  • import * as readline … … ターミナルで1行ずつ入力を読むための、Node.js標準の道具を読み込みます。
  • const rl = readline.createInterface({ … }); … キーボード入力(stdin)と画面出力(stdout)をつないだ「入力受付係」を作ります。
  • while (true) { … 「exit と打つまで、ずっと入力を受け付ける」外側のループ(これは会話そのもののループ。エージェントループとは別物です)。
  • const userInput = await rl.question("> ");> と表示して、あなたの入力を1行待ちます。
  • if (userInput === "exit") break;exit なら会話を終了。
  • await runAgent(userInput); … 入力をエージェント本体に渡します。ここで初めて、さっきの「考える→道具→結果→また考える」ループが回ります。1回の入力につき、内側で何往復もするわけです。
  • rl.close(); … 会話を終えたら受付係を閉じます。

例えば「今日の日付を教えて、それに 7 を足した日は何日?」のように頼むと、エージェントは内側で get_current_time を呼ぶ → 結果を見る → add を呼ぶ → 結果を見る → 文章で答える と、自分で何往復もしてから答えてくれます。あなたが書いたのは「ループと道具」だけ。考える順番はLLMが決めています。これが心臓の動く瞬間です。


⚠️ ハマりどころ

  • ループの上限を付け忘れる(無限ループ=コスト爆発)for (let step = 0; step < 10; step++)< 10 を消したり while (true) にしたりすると、AIが堂々めぐりしたとき永遠に止まりません。しかも1往復ごとにAPIを呼ぶので、止まらない=料金が積み上がり続ける。上限は気休めではなく必須の安全弁です(コストの詳細は付録F)。

  • assistant の返事を「テキストだけ」履歴に入れてしまう → ここが一番やりがちな罠。messages.push({ role: "assistant", content: res.content });res.content を、「テキストの部分だけ取り出して」入れてしまうと、LLMが「道具を使いたい」と言った tool_use ブロックが消えてしまいます。すると次に返す tool_result が「どの依頼への返事か」分からなくなり、API がエラーになります。res.content は丸ごと履歴に積むのが鉄則。テキスト表示用に中身を読むのは別の話で、履歴に入れるのは加工せず全部です。

  • tool_use を1つしか処理しない → LLMは1回の返事で、道具を同時に複数呼ぶことがあります(例:2つのファイルを並行して読みたい)。なので for (const block of res.content)全部の tool_use ブロックを処理し、結果も全部 toolResults に入れて返さないといけません。1つだけ返すと、残りの依頼が宙ぶらりんになってエラーになります。「最初の1個だけ」で済ませないこと。

  • 毎ターン履歴を丸ごと再送している、と知らずにいる → このループは、回るたびに messages(=これまでの全履歴)をまるごとLLMに送り直しています。つまり往復が増えるほど、送るトークン(=入力コスト)が雪だるま式に増えます。動作上は正しいのですが、長い作業ではここがコストの中心になります。対策(要約・上限・プロンプトキャッシュ)は第14章と付録Fで扱います。

  • stop_reason"end_turn" だけで判定してしまう → 止める条件は「"tool_use" 以外なら止める」(!== "tool_use")と書くのが安全です。"end_turn" 以外にも終了に近い理由(max_tokens で途中で切れた等)があり得るため、「道具を使いたいとき以外は止める」という発想にしておくと、思わぬ取りこぼしを防げます。


🤖 AIに頼むなら(Vibe codingのコツ)

エージェントループは、Claude Code などに頼んで書いてもらえます。ただ「ループ作って」だけだと、上限の無い危ないループや、tool_use を1個しか処理しないコードが出がちです。止め方と上限をはっきり指定して頼みましょう。

🗣 プロンプト例: 「TypeScript と公式 @anthropic-ai/sdk で、エージェントループの関数 runAgent(userInput) を書いて。要件は4つ: ① for ループで回し、暴走防止に最大10回の上限をつける。 ② 毎回 anthropic.messages.create を呼び、返事(res.content)は加工せず丸ごと messagesrole: "assistant" で push する。 ③ res.stop_reason"tool_use" 以外なら、最終回答を表示して return(ループを抜ける)。 ④ "tool_use" のときは res.content の中の すべての tool_use ブロックexecuteTool(name, input) で実行し、結果を tool_resulttool_use_id を一致させて)にまとめて role: "user" で push してから次の回へ。 executeTool は switch で振り分け、いまは get_current_timeadd安全な2つだけでいい。」

出てきたコードを見るときの確認ポイント:

  • ループに回数の上限がある(while (true) のまま放置していない)か?
  • stop_reason"end_turn"(="tool_use" 以外)のとき、ちゃんと止まるか?
  • res.content の中の すべての tool_use を処理しているか?(最初の1個だけになっていないか)
  • assistant の返事を res.content 丸ごと push しているか?(テキストだけにして tool_use を捨てていないか)

📝 ことばメモ

  • エージェントループ:「LLMに相談 → 道具を実行 → 結果を履歴に戻す → また相談」を、完了するまでくり返す往復。エージェントの心臓。これがチャットとエージェントを分ける一点
  • stop_reason(ストップ・リーズン):APIの返事に入っている「なぜ止まったか」の合図。ループを続けるか止めるかの判断に使う
  • "end_turn"(えんど・たーん)stop_reason の値の一つ。「もう話すことはない=完成」。これが来たらループを抜ける
  • "tool_use"(つーる・ゆーず)stop_reason の値の一つ。「まだ道具を使いたい」。これが来たら道具を実行して結果を返し、ループを続ける
  • イテレーション上限(じょうげん):ループを回す最大回数(例:10回)。end_turn がいつまでも来ない無限ループ=コスト爆発を防ぐための安全弁。回数を1つ足すたびにAPIを1回呼ぶ=お金がかかる、という事実とセットで覚える

➡️ 次の章へ

これで心臓ができました。「考える→道具→結果→また考える」をループで回し、end_turn で止まり、上限で暴走を防ぐ——エージェントの土台はもう動いています。

ただし、いま持たせた道具は「日時を返す」「足し算する」だけ。まだ自分のパソコンには手を出していません。次の第5章では、いよいよ本物の道具を持たせます——ファイルを読む・一覧する。読み取りは何も壊さない安全な道具なので、最初の一歩にぴったりです。心臓に、ようやく“手”が生えます。

次の章:道具をつくる① ファイルを読む・一覧する →

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