レッスン6: ハンズオン: 制御付きエージェントハーネスを手書きする
学習目標:
- これまでのレッスンの
stop_reasonループを@anthropic-ai/sdkの上で動くwhileループとして書き、ツールを呼び続けるのか、テキストを返して切り上げるのかを自分で判断させられるtool_useとtool_resultの内容ブロックを仕様どおりに構築し、1ターン分の複数の結果を1つのuserメッセージに入れて返せる- そのループに4つの制御弁 — 最大ターン数、予算上限、無進捗検知、高インパクトなアクションの承認 — を取り付け、それぞれがループのどのステップに属するかを正確に言える
前提: レッスン2から5を読み、
stop_reasonで駆動するループ、停止条件、暴走のフォールバック、ヒューマン・イン・ザ・ループの介入を理解していること | 前: レッスン5 <<
まず、走っている姿を見る
最初の5つのレッスンは、機械を1つずつ分解してきました: ループはどう回るのか、いつ止まるべきか、暴走はどう見えるのか、人はどう割って入るのか。このレッスンでは、その部品を溶接して最小の動くハーネスにします。コードを見る前に、ターミナルでの振る舞いを見てください — 2つのおもちゃのツール(get_time は時刻を報告し、read_file はプロジェクト内のファイルを読む)を配線したエージェントに、一文を渡します: 「README.md の1行目を読んで、それから今何時か教えて」。
ここで何が起きたかをよく見てください: ユーザーは一文を言っただけで、ツールを何回呼ぶか、どれを先に呼ぶか、いつ止まるかは、すべてループの中でモデルが決めました。 それがエージェントとワークフローの分かれ目です — ワークフローの経路はコードで固定されているのに対し、エージェントはモデルが自分のプロセスを動的に主導し、どのツールを使うかを自分で決めるものです1。ホストコード(このレッスンで書くハーネス)は「先にファイルを読み、それから時刻を確認する」とは一言も指定していません。ただ忠実にループを回し、モデルが名指ししたツールを実行し、結果を返しただけです。ここでは2つのツールとも無害なので、実行は何にも中断されませんでした — しかしこのハーネスには承認弁も溶接されており、モデルがファイルの削除やリクエストの送信のような高インパクトなものに手を伸ばせば、動く前に止まって人間のうなずきを待ちます(レッスンの後半で書きます)。このレッスンの残りは、あのターミナル出力の裏にあるコードを1行ずつ組み立てていきます。
コアループ: 骨格を持ち込み、本物の SDK に差し替える
レッスン2「コアループ: 一度の往復から継続的な運転へ」の callModel は擬似コードでした。いまそれが本物の @anthropic-ai/sdk になります。ループの骨格は同一です: messages を携えてリクエストを送り、response.stop_reason を見る — "tool_use" ならツールを実行し、結果を縫い合わせてもう一度送る。そうでなければ(たとえば end_turn)テキストを返してループを抜ける2。
まずは制御弁がまったくない最小版で、ループそのものを見えるようにします:
これをレッスン2の骨格と並べると、構造は動いていません: while の行はいまも「stop_reason が tool_use であるかぎり繰り返す」と言っており、本体もいまも同じ4ステップ — assistant を push、ツールを実行、tool_result を push、response を再代入 — です。実質的な変更は callModel が client.messages.create(...) になったことと、本体の末尾のあの再代入だけです。その再代入こそが停止を可能にしているのであり、落とせば stop_reason は古い値のまま永遠に変わりません。それがまさに、レッスン4「暴走とフォールバック: デッドループ、空回り、予算バーンアウト」のデッドループです。
tool_use / tool_result のフィールドを、1つも欠かさない
runToolUses は、モデルが名指ししたツールが実際に走る場所です。ここでいちばん間違えやすいのは内容ブロックのフィールドなので、仕様に従ってください: tool_use ブロックは id / name / input を持ち、tool_result ブロックは tool_use_id(どの呼び出しへの答えかを名乗る)と content を持ち、ツールの実行が失敗したときは is_error: true を足します3。もう1つ堅いルールがあります: 応答に tool_use ブロックがいくつ含まれていようと、同じ数の tool_result ブロックを返さなければならず、そのすべてを直後の1つの user メッセージに詰めます3 — 上のループ本体の messages.push({ role: "user", content: toolResults }) の行が、そのルールを守っています。
try/catch に注目してください: ツールが吹き飛んだからといって、ハーネス全体を道連れにすべきではありません。エラーを is_error: true と印を付けた tool_result に包んで返せば、モデルは別の引数で再試行するか、別の道を取る機会を得ます。例外を投げてプロセスを殺すより、そのほうがはるかに安定します。
4つの制御弁を取り付ける
これでループは回りますが、レッスン2の裸のループ — モデルを信頼し、自分に逃げ道を残していないもの — のままです。モデルが end_turn を返したターンで止まり、その間には境界がどこにもありません。そしてエージェントの自律性は、より高いコストと、ループを一周するごとに誤りが累積増幅する可能性を意味し、モデルは多くのターンにわたって動作しうるのですから1、裸のループは止まるか続けるかの判断をまるごとモデルに賭けていることになり、危険すぎます。ここで、これまでのレッスンの4つの弁を1つずつ溶接していきます。
各弁は1つのことを守っており、どの位置も適当に決めたものではありません:
- 弁1、最大ターン数(レッスン3「停止条件: エージェントはいつ手を引くべきか」):
turns >= MAX_TURNSは本体の最も先頭、turns++の手前にあります。意味は「この一周に入る前に、もう一周が許されているかを確認する」です。この明示的な停止条件は、モデル自身のend_turnとは別に、制御を自分の手に握っておくために存在します1。 - 弁2、予算上限(レッスン4「暴走とフォールバック: デッドループ、空回り、予算バーンアウト」): 応答が返るたびに
response.usageからトークンを積み上げ、天井で止めます。ターン数は少なくても1ターンあたりのコンテキストが巨大な場合、ターン数だけでは支出を押さえられません。トークンという独立した別のゲートが要ります。 - 弁3、無進捗検知(レッスン4): このターンのツール呼び出しを署名に平坦化し、前のターンと比べます。同一なら空回りです。これが捕まえるのは、ターン数は上限を超えておらず予算も破裂していないのに、モデルが足踏みして同じツールを同じ引数で何度も呼んでいる、という停滞したケースです。
- 弁4、承認弁(レッスン5「介入と操舵: 中断、方向転換、ヒューマン・イン・ザ・ループ」):
runToolUsesの中、実際にツールを実行する手前で、高インパクトなアクションはまず人間の確認を取ります。高インパクトなアクションへのヒューマン・イン・ザ・ループの承認は、まさに過剰なエージェンシーのリスクを抑える推奨手段です4。
弁3の署名関数は、退屈なほど素朴です — そのターンのすべての tool_use ブロックの名前と引数を1つの文字列に連結するだけ。「何がどんな引数で呼ばれたか」を区別できれば、それで十分です:
承認弁: 実行の直前の瞬間に差し込む
4つの弁のうち、位置が最も重要で、最も間違えやすいのが承認弁です。モデルがツールを名指ししたが、そのツールはまだ実際には走っていない、というその瞬間に差し込まなければなりません — これから起きるアクションを表示し、人間を待ち、確認が取れてから実行します。1ステップ遅ければ、ファイルはすでに書かれ、リクエストはすでに送られ、「確認しますか?」と尋ねる意味はありません。というわけで、これは runToolUses の中、impl(...) の行の手前に置きます:
approve は外から渡される関数です。ターミナルでは「アクションを表示し、入力を1行読む」という意味になります:
1つ重要な細部があります: ユーザーが拒否した場合でも、何も返さないのではなく is_error: true と印を付けた tool_result を返します。仕様は、すべての tool_use に対応する tool_result を返すことを要求しており3、飛ばせば1つのツール呼び出しに結果がないせいで次のリクエストがエラーになります。拒否は無視とは違います — 拒否それ自体がモデルに知らせるべき結果であり、拒否されたと学んだモデルは、そもそも高インパクトなアクションを必要としない道へ切り替えることがよくあります。
2つのおもちゃのツールで、ループを実際に走らせる
弁は付きました。足りないのは、モデルが呼べるツールです。このレッスンでは絶対に安全なおもちゃを2つだけ使い、危険な操作は戸口の外に置いておきます: get_time は現在時刻を報告し、read_file はファイルを読みます — ただし path.resolve でプロジェクトディレクトリの内側にしっかり固定し、モデル(あるいはツール出力に進路を狂わされたモデル)が /etc/passwd のような境界外のパスを読みにいけないようにします:
どちらのツールも HIGH_IMPACT の集合に入っていないので、承認は起動しません — 作りからして無害です。承認弁をデモするには、write_file を toolImpls と HIGH_IMPACT に足してください。このレッスンでは、例を走らせただけであなたのファイルが壊れないよう、本物の書き込み操作を意図的に導入していません。
組み立てる: node agent.js で走らせられるエントリポイント
最後に、runAgent、runToolUses、ツール定義、承認関数を、そのまま走らせられる1つのエントリポイントにまとめます — このレッスン冒頭のターミナル出力の裏にあるものです:
ここまでの部品(import、client、MODEL、runAgent、runToolUses、signatureOf、approveInTerminal、toolImpls、tools、main)を1つの agent.js に入れ、ANTHROPIC_API_KEY を設定し、npm i @anthropic-ai/sdk を実行すれば、node agent.js "あなたのタスク" で走ります。
この百数十行を振り返ると、新しい概念は1つもないことに気づきます: while ループと stop_reason はレッスン2から、MAX_TURNS はレッスン3から、予算と空回り検知はレッスン4から、承認弁はレッスン5から来ています。ハーネスは何か深遠なフレームワークではありません。あなた自身が書き、自分で制御する、このループ+弁の層です。 同じモデル、同じ2つのツールでも、この4つの弁を備えたハーネスとレッスン2の裸のループとでは、同じタスクをどれだけ安定して走らせられるかが大きく違いえます。エージェントが頼れるかどうかを決めているのは、内側のモデルだけでなく、この外側の制御コードの層だという部分が大きいからです5。
複雑さについては釣り合いの感覚も持っていてください: すべてのエージェントに4つの弁が要るわけではありませんし、覚えておく価値のある一文は、複雑さを足すのは結果を明確に改善するときだけにすべきだ、というものです1。統制された環境で3〜5ターン走る小さなツールなら MAX_TURNS だけで十分かもしれません。4つの弁は、何ターンも連続で走り、高インパクトなアクションに手を伸ばしうるケースのためのものです。
まとめ
- 動くハーネスの中核は、いまもレッスン2のループです:
messagesを携えてリクエストを送る →stop_reasonを見て、tool_useならツールを実行し、tool_resultを縫い合わせてもう一度送る。そうでなければテキストを返して切り上げる2。本物の SDK に切り替えても、callModelがclient.messages.create(...)になるだけです - 内容ブロックのフィールドは仕様どおり、1つも欠かしません:
tool_useはid/name/inputを、tool_resultはtool_use_id/contentを、失敗時にはさらにis_errorを持ちます。1ターンにtool_useブロックがいくつあろうと、同じ数のtool_resultブロックを返し、そのすべてを直後の1つのuserメッセージに詰めます3 - 4つの制御弁はそれぞれ1か所を守り、位置は入れ替えられません: 最大ターン数(レッスン3)と予算上限(レッスン4)はループが必ず止まることを保証するハードな境界、無進捗検知(レッスン4)は足踏みを捕まえ、承認弁(レッスン5)はツールの実行より手前に差し込まなければなりません — エージェントの自律性はより高いコストとエラーの累積増幅をもたらし、モデルは多くのターンにわたって動作しうる1ため、モデル自身の
end_turnでは押さえきれないからです - 高インパクトなアクションに人間の確認を要求する承認弁は、過剰なエージェンシーのリスクを抑える推奨手段です4。拒否された場合でも
is_errorのtool_resultを返し、呼び出しを宙ぶらりんにしないでください3 - ハーネスは深遠なフレームワークではなく、あなた自身が書き、自分で制御する、このループ+弁の層です — 同じモデルでも制御コードが違えば、信頼性は大きく変わりえます5。ただし弁を弁のために積み上げてもいけません: 複雑さを足すのは、結果を明確に改善するときだけにしてください1
このコースはこれで終わりです。「ハーネスとは何か」から、4つの制御弁を備えたループを手書きするところまで来て、いまあなたの手にあるのは概念の集まりだけではありません — 実際に走り、編集でき、これからも制御を足していける本物のコードです。自分のツールに配線して、あなたのために働かせてみてください。
Footnotes
-
Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
How tool use works — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works ↩ ↩2
-
Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls ↩ ↩2 ↩3 ↩4 ↩5
-
LLM06:2025 Excessive Agency — OWASP Gen AI Security Project — https://genai.owasp.org/llmrisk/llm062025-excessive-agency/ ↩ ↩2
-
The 2026 Agent Engineering Roadmap — GitHub (codejunkie99/agent-roadmap-2026) — https://github.com/codejunkie99/agent-roadmap-2026 ↩ ↩2