Lesson 01 · 基礎

ループなくして agent なし

Claude Code の仕組みは一行で書ける:while stop_reason == "tool_use"

⏱ 約 10 分 · 📝 3 つのインタラクティブ要素 · 🧑‍💻 出典 shareAI-lab · s01_agent_loop.py

agent は実際に何をしているのか

ターミナルで Claude Code を起動し、「すべての TODO コメントをリストにまとめて」と頼むと、まず grep、次にいくつかのファイルを cat、そして Markdown を出力する——そんな動きを目にする。モデルはコードを自分で実行しない——実行を依頼するだけだ。実際に「手足」のように見せているのは、その外側にある 30 行ほどのグルーコードだ。

このレッスンではそのグルーコードを解体して正体を明らかにする。これが agent loop(エージェントループ)で、構造はこうだ:

while response.stop_reason == "tool_use":
    response = LLM(messages, tools)      # 1. ask the model
    execute_tools(response.tool_calls)   # 2. run what it asked for
    messages.append(tool_results)        # 3. feed results back

以上だ。それだけ。本番の Claude Code はこの上に権限管理・hook・サブエージェント・worktree 分離・メモリ圧縮を積み上げているが、核心は変わらずこの4行だ。

重要な直感:モデルの返答は常に「ツールを呼びたい」か「言い終わった」のどちらかだ。前者の状態が続く限りループは回り続け、後者に入った瞬間ループは終了する。判定基準はレスポンスの stop_reason フィールドだ。

messages[] が一歩ずつ育っていく様子

以下のループが模擬するタスクは「カレントディレクトリのファイルを一覧にして、package.json を読み込む」だ。Step を押すたびにループ内の操作が一つ進む。左側が人間向けの会話バブル、右側がモデルに渡される実際の messages[] 配列——どう増えていくかに注目してほしい。

ツール結果は必ずメッセージ履歴に戻さなければならない

初心者が最もはまる落とし穴は、「ツールを実行した」ことを副作用として扱ってしまうことだ——実行して終わり、と。しかしモデルは次のターンで messages[] しか参照できない。そこにない情報は起きていないも同然だ。append 一行を忘れるだけで、ループ全体が壊れる。

壊れ方は一種類ではない。よくある2パターン:

  • append を忘れる:モデルは次のターンで結果を確認できないため、同じツールをもう一度呼ぶ——無限ループの出来上がり。
  • append したが tool_use_id を落とす:Anthropic API は直接 tool_result must have tool_use_id エラーを返し、ループは API 呼び出し時点でクラッシュする。

同じタスク:「プロジェクト内の Python ファイルの数を数える」。2つのバージョンを並べて実行し、結末の違いを確認しよう。

stop_reason を読み解く

モデルが返すたびに stop_reason が付いてくる。ループを続けるかどうかはこのフィールド一つで決まる:

  • tool_use — モデルがツールを呼びたい。ループを継続。
  • end_turn — モデルが言い終えた。ループを終了。
  • max_tokens — token 上限に達して切り捨てられた。ループを終了(通常は例外として扱い、出力が不完全であることをユーザーに伝える)。
  • stop_sequence — カスタムの停止文字列にヒットした。ループを終了。

tool_use 以外なら継続」と書き間違えたり、max_tokens を無視して正常終了として扱ったりするのはよくあるバグだ。

手を動かしてみる

ローカルに shareAI-lab/learn-claude-code をクローン:

git clone https://github.com/shareAI-lab/learn-claude-code
cd learn-claude-code
pip install -r requirements.txt
cp .env.example .env  # fill in ANTHROPIC_API_KEY
python agents/s01_agent_loop.py

そして実際に何か頼んでみよう:このリポジトリにある .py ファイルの数を数えてls -R を送信し、出力を確認し、次に find . -name "*.py" | wc -l を送信して答えを返す——その一連の流れがこのループそのものだ。

Interactive

Widget 1 · Loop Stepper · messages[] の成長

理解すること:ツール結果は特殊な形式(tool_result ブロック、tool_use_id 付き)で messages に書き戻される——これがモデルが次のターンで「ツールが実行されて結果は X だった」と知る唯一の手段だ。

会話ビュー未開始
Step を押して開始 →
messages[] JSONlength: 0
[]
stop_reason:
Interactive

Widget 2 · Break the Loop · 正しい実装 vs append 忘れ

理解すること:messages.append(tool_results) を一行でも省くと、agent は agent でなくなる——毎ターン前のターンを忘れる読み取り機に成り下がる。

while True:
    resp = LLM(msgs, tools)
    msgs.append({"role":"assistant",
                 "content": resp.content})
    if resp.stop_reason != "tool_use":
        return
    results = execute(resp.tool_uses)
    msgs.append({"role":"user",
                 "content": results})
シミュレーション実行
while True:
    resp = LLM(msgs, tools)
    msgs.append({"role":"assistant",
                 "content": resp.content})
    if resp.stop_reason != "tool_use":
        return
    results = execute(resp.tool_uses)
    # BUG: forgot to append results
    # msgs.append(...)
シミュレーション実行
Interactive

Widget 3 · Spot the stop_reason · 4つの実際のレスポンスを判定

それぞれが実際にありうるモデルレスポンスの断片だ。「ループ継続」か「ループ終了」かを選択し、即座に正誤を確認しよう。

正解 0 / 4