Week 1 教室 / 日本語書き下ろしレッスン / 対応: D1の土台・D4の入口

Claude API とエージェントループ
— Claude Code の「正体」を50行で理解する

全ドメインの土台になる1本。これを理解すると「Claude Codeが裏で何をしているか」が透けて見えるようになり、D1(27%)の問題が「知識問題」から「当たり前の話」に変わる。英語のSkilljarコースの代わりに、このページだけで Week 1 のインプットが完結するように書いてある。

このレッスンの流れ(約60〜90分)

STEP 1大前提:AIとの通信は「手紙のやりとり」

最初に知っておくべきことは1つだけ。Claude API は「会話の全履歴を毎回送りつけて、次の1通の返事をもらう」仕組みだということ。

  • Claudeの側には記憶がない。前に何を話したかは、こちらが毎回全部書いて送る(だからコンテキスト管理=D5が試験ドメインになるほど重要になる)
  • 1回の送信=「system(役割設定)+ messages(今までの会話全部)」、1回の受信=「assistantの返事1通」
  • この往復をプログラムのループで自動化したものがエージェント。つまりエージェントは魔法ではなく「手紙の往復の自動化」

tomoさんが毎日見ている「Claude Codeがツールを呼んでは考え、また呼ぶ」動きは、すべてこの手紙の往復でできている。

STEP 2最小のAPI呼び出しを読む

まず道具なしの素の呼び出し。Pythonの公式SDK(pip install anthropic)を使う。

# 最小のClaude API呼び出し
import anthropic

client = anthropic.Anthropic()  # APIキーは環境変数 ANTHROPIC_API_KEY から読む

response = client.messages.create(
    model="claude-sonnet-5",        # 使うモデル
    max_tokens=1024,                # 返事の長さの上限
    system="あなたは簡潔に答えるアシスタント。",   # 役割設定(システムプロンプト)
    messages=[
        {"role": "user", "content": "日本で一番高い山は?"}
    ],
)

print(response.content)      # 返事の中身(ブロックのリスト)
print(response.stop_reason)  # ← 今週の主役。なぜ止まったか

覚えるパラメータは4つだけ:model(誰に聞くか)、max_tokens(返事の上限)、system(役割)、messages(会話履歴)。messagesrole(user か assistant)と content のペアが並んだリストで、会話が進むほどこのリストが長くなっていく

STEP 3応答の解剖 — stop_reason という「止まった理由」

返ってくる response には「返事の本文(content)」と一緒に、「なぜそこで書くのをやめたか」を示す stop_reason が入っている。ここが試験の最重要ポイント。

stop_reason の値意味プログラムがすべきこと
"end_turn"言いたいことを言い終わったループ終了。返事をユーザーに見せる
"tool_use"「このツールを使いたい」と要求して一時停止したループ継続。要求されたツールを実行し、結果を返す
"max_tokens"上限に達して途中で切れた上限を増やす・続きを促す等の異常系処理
試験の急所
  • ループを続けるか止めるかはこの値だけで判定する。「返事のテキストに『完了しました』とあるか」で判定する選択肢は常に誤答(自然言語は信用できない)
  • 「最大10回まで」のような回数上限は暴走への保険であって、主要な停止判定に使う選択肢も誤答

STEP 4ツールを持たせる — tool use の往復

Claudeに「天気を調べる」などの能力を持たせるには、ツールのカタログ(名前・説明文・入力の型)を送信時に同封する。すると Claude は必要なときに「そのツールをこの引数で実行して」とお願いしてくる。実行するのはこちらのプログラム——Claudeは自分では何も実行できない。ここを誤解しないこと。

# ツールのカタログを定義して同封する
tools = [
    {
        "name": "get_weather",
        "description": "指定した都市の現在の天気を返す。都市名は日本語でも英語でもよい。"
                       "天気・気温の質問に使う。ニュースや歴史の質問には使わない。",  # ←説明文が命(D2)
        "input_schema": {                    # 入力の型(JSONスキーマ)
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "都市名。例: 東京"}
            },
            "required": ["city"],
        },
    }
]

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    tools=tools,                             # ← カタログを同封
    messages=[{"role": "user", "content": "大阪の天気は?"}],
)

# stop_reason が "tool_use" になり、content にツール要求ブロックが入る:
#   ToolUseBlock(id="toolu_abc123", name="get_weather", input={"city": "大阪"})

ツール要求が来たら、こちらで実行して結果を会話履歴に追記して送り返す。このとき「どの要求への返answerか」を tool_use_id で対応付ける。

# ツールを実行して、結果を「返信」する
weather = get_weather("大阪")   # ←自分のプログラムで実行(API天気サービスを呼ぶ等)

messages.append({"role": "assistant", "content": response.content})  # Claudeの要求を履歴に残す
messages.append({
    "role": "user",
    "content": [{
        "type": "tool_result",
        "tool_use_id": "toolu_abc123",   # どの要求への答えか
        "content": "晴れ、28度",
    }],
})
# → この messages でもう一度 create() を呼ぶと、Claudeは結果を踏まえた返事をする

STEP 5本題:エージェントループを組み立てる

STEP 2〜4 の部品を while ループで繋ぐと、エージェントが完成する。これが今週の到達点。

# エージェントループ完全版(約40行)
import anthropic

client = anthropic.Anthropic()

def run_tool(name, args):
    """要求されたツールを実際に実行する(ここは自分のコード)"""
    if name == "get_weather":
        return f"{args['city']}は晴れ、28度"   # 本物はAPIを呼ぶ
    return f"不明なツール: {name}"

def agent(user_input):
    messages = [{"role": "user", "content": user_input}]

    while True:                                      # ← エージェントループ
        response = client.messages.create(
            model="claude-sonnet-5",
            max_tokens=1024,
            tools=tools,                             # STEP 4 のカタログ
            messages=messages,
        )

        # Claudeの応答(テキストやツール要求)を履歴に追記
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "tool_use":       # ★ ツールを使いたい → 続行
            results = []
            for block in response.content:
                if block.type == "tool_use":
                    output = run_tool(block.name, block.input)   # 実行するのは自分
                    results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,                 # 要求と結果を対応付け
                        "content": output,
                    })
            messages.append({"role": "user", "content": results})  # 結果を履歴に追記
            continue                                  # → もう一周

        if response.stop_reason == "end_turn":       # ★ 話し終わった → 終了
            return "".join(b.text for b in response.content if b.type == "text")

agent("大阪と札幌の天気を比べて")

読み解きのポイント:

  • 26行目の分岐がすべてtool_use なら実行して continue、end_turn なら return。試験のD1.1はこの構造を言葉で問うている
  • ツール結果は messages追記される(=会話履歴が育つ)。「結果を履歴に足すからこそ、モデルは新情報を踏まえて次を考えられる」——これも頻出の言い回し
  • 1回の応答に複数のツール要求が入ることがある(for文で回しているのはそのため)。「大阪と札幌」なら get_weather が2個同時に来る——これが並列ツール呼び出しで、D1.3の「1応答に複数のTask呼び出し=並列サブエージェント」と同じ原理

STEP 6これが Claude Code の正体

いま書いた40行と、毎日使っている Claude Code の対応表:

40行のループの部品Claude Code での姿
tools カタログRead / Write / Bash / Grep / Glob / Task / MCPツール群
run_tool()Claude Code本体(ファイルを読む・コマンドを実行する側)
stop_reason == "tool_use" の一周画面で「ツールを呼んでは結果を読む」1回分
stop_reason == "end_turn"ターンが終わってあなたに返事が表示される瞬間
messages が育っていくコンテキスト消費が増えていく(→限界が来ると /compact)
ループの外側の安全装置hooks(larc-approval-gate が実行前に割り込む等)

この対応が腹落ちすると、D1の問題は「仕様の暗記」ではなく「いつも見ている光景の言語化」になる。

STEP 7おまけ:構造化出力の裏技(D4の予告編)

tool use には有名な副産物がある。「実在しないツール」を定義して無理やり呼ばせると、確実にスキーマ通りのJSONが手に入る

# 「抽出結果を記録する」という体のツールを定義し、
# tool_choice で強制的に呼ばせる → 出力は必ずこのスキーマに従う
response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    tools=[{
        "name": "record_invoice",
        "description": "請求書から抽出した情報を記録する",
        "input_schema": {
            "type": "object",
            "properties": {
                "issuer":  {"type": "string"},
                "total":   {"type": "number"},
                "due_date": {"type": ["string", "null"]},  # ←無いかもしれない欄はnull許容(捏造防止)
            },
            "required": ["issuer", "total"],
        },
    }],
    tool_choice={"type": "tool", "name": "record_invoice"},   # ← 必ずこのツールを呼べ
    messages=[{"role": "user", "content": f"この請求書から情報を抽出して: {document}"}],
)
# response.content の tool_use ブロックの input が、そのまま検証済みJSONになっている

「JSONで返して」とお願いするより確実(壊れたJSONが来ない)。ただし形式の保証であって中身の保証ではない(合計が合わない等の意味エラーは残る)——この区別はD4.3で深掘りする。

STEP 8演習課題(公式演習1の日本語版)

公式ガイド指定の演習を、この教室のレベル感に合わせて再構成したもの。全部で2〜3時間。私(Claude)に「W1演習やる」と言えば、環境準備から伴走する。

  1. 最小ループの写経:STEP 5 の40行を動かす(APIキーは1Passwordから)。「大阪と札幌の天気を比べて」で2回のツール実行が起きることをprintで確認する
  2. 紛らわしいツールを足すget_weather に加えて get_weather_history(過去の天気)を定義し、説明文で境界を明確に書き分ける。「昨日の東京の天気は?」が正しく履歴側に行くかテストする(→D2.1の体験)
  3. 構造化エラーを返す:存在しない都市を聞かれたら {"errorCategory": "validation", "isRetryable": false, "message": "都市が見つからない"} の形でツール結果を返し、Claudeがユーザーに適切に説明することを確認する(→D2.2の体験)
  4. 業務ルールの割り込み:run_tool の手前に「1ターンにツール実行は3回まで。超えたら実行せず『上限に達した』を返す」というゲート関数を挟む(→D1.5 hookの原理の体験)
  5. 複数論点の依頼:「大阪の天気と、京都の天気と、傘が要るかの判断」を1回で投げ、分解→並列実行→統合の流れをログで観察する

理解度チェック(30秒で説明できたらチェック)

終わったら

D1ページの 1.1〜1.3 を照合して、チェックリストを更新。Skilljarの動画版(Building with the Claude API の序盤)を見たい場合はChromeの日本語翻訳を併用。次は Week 2(マルチエージェント+Claude Code設定の「翻訳学習」)へ。

← ホーム 照合先: D1 エージェント設計 →