【第2回】エージェントの性質を検証する——戻り値・並列・同期非同期・ネストの実験記録
Claude Code サブエージェントの戻り値、直接実行との比較、並列実行、フォアグラウンド/バックグラウンド、ネスト呼び出し、ファイル出力パターンを実験で検証した記録
← 前の記事: 【第1回】サブエージェントをはじめよう——Claude Code に「別の自分」を呼び出させる前回は、スキル・エージェント・Taskの概念整理から始めて、neta-research-agent カスタムエージェントを作り、kbsearch の権限エラーを解決するところまでやりました。
エージェントは動いた。でも「動いた」と「どう動いているか」は別の話です。
前回の末尾に残した疑問——戻り値はどう受け取るのか、並列は、非同期は、ネストは? 今回はこれらに正面から取り組みます。Claude Code との対話を通じて、6つの実験でエージェントの「性質」を確かめていきます。
通信の仕組みを聞いてみる
カスタムエージェントが動いた直後、最初に気になったのは通信の仕組みでした。
私: カスタムエージェントとメインとはどうやって通信するの?
Claude Code: Agent ツールの prompt(入力)と return(出力)だけです。途中経過は見えない。結果だけ返ってくる。つまり「関数呼び出し」に近い。
「途中経過は見えない」——これが思ったより重要でした。neta-research-agent が内部で何回 kbsearch を叩いたか、どんなキーワードで試行したか、途中でどんな結果を見て何を捨てたか。一切見えません。親の会話に届くのは、最後の報告書だけです。
コンテキスト分離とよく言われますが、実体はシンプルです。成果物だけが届く。それだけ。長い調べ物をエージェントに任せても、自分の会話はスッキリしたまま続けられる——これが体感として一番大きかった発見でした。
非同期と並列化を矢継ぎ早に聞く

通信の仕組みが分かると、次の疑問が連鎖的に湧いてきました。
私: 非同期で返ってくるの?
Claude Code:
run_in_background: trueを付ければ非同期で、付けなければ同期です。さっきのテストは同期で約19秒待ちました。
私: 並列化は?
Claude Code: 1つのメッセージで複数の Agent を同時に呼べます。ドメインごとに並列でネタ探しさせて、全部の結果が揃ってからまとめる、ということができます。やってみますか?
私: 非同期の場合は return で見る? それともドキュメント?
Claude Code: 非同期の場合は、完了したら自動で通知が来ます。
/dev startのバックグラウンドタスクと同じ仕組みです。ポーリングは不要。
質問を3つ畳み掛けたところで、仕組みは理解できました。じゃあ実際に動かしてみよう。
私: じゃあ並列処理してみて。
並列実行: 3つ同時に動かす
smoothie・buddhaism・laboの3ドメインで一気にネタ探しを並列起動しました。
やり方は拍子抜けするほど簡単でした。Agent ツールコールを1レスポンスに3つ並べるだけ。それだけで並列実行になります。「並列実行には何か特別な設定が要るのでは」と思っていたのですが、何も要りませんでした。
並列で動くのは、各エージェントが独立したコンテキストを持っているからです。smoothie のエージェントと buddhaism のエージェントは、互いの存在を知らずに動いている。3人に別々の調べ物を頼んで、それぞれが独立して作業して報告書を持ち帰ってきた——比喩がそのまま動作の説明になっています。
待っている間、気になって聞いてみました。
私: 非同期で待ってる間、サブエージェントの進捗みれたりする?
Claude Code が出力ファイルの行数を確認してくれました。
Claude Code: 進捗が見えました。smoothie は kbsearch 検索完了で分析中、labo はネタ3候補を報告済み(ほぼ完了)。出力ファイルの JSON ログを tail で覗けば、ツール呼び出しの履歴がリアルタイムで見えます。
完全なブラックボックスではなかった。覗こうと思えば覗ける。でも普段は完了通知を待つだけで十分です。
フォアグラウンドとバックグラウンド
エージェントには2つの実行モードがあります。フォアグラウンド(同期)とバックグラウンド(非同期)です。公式ドキュメントの Run subagents in foreground or background に詳しい説明があります。
フォアグラウンドは、エージェントが終わるまでメインの会話がブロックされます。その代わり、エージェントが未許可のツールを使おうとすると承認プロンプトが表示されます。「このツールの使用を許可しますか?」と聞いてくる。対話的に権限を与えられる。
バックグラウンドは、エージェントが裏で動いている間もメインの会話を続けられます。ただし、承認プロンプトは出ません。事前に settings.local.json で許可されていないツールは自動拒否されます。
並列実行のときに neta-research-agent をバックグラウンドで起動して、kbsearch で検索させました。kbsearch は事前に Bash(kbsearch:*) で許可済みだったので問題なく動いた。メインの会話で別の作業をしている間に、裏でネタ探しが進む。結果は TaskOutput で確認できます。
ここまでは順調でした。問題は、ファイルに書き出させようとしたときに起きました——が、その話は次回の権限モデルの回に譲ります。
使い分けはシンプルです。結果を待ってから次に進みたいならフォアグラウンド、待たずに別の作業を進めたいならバックグラウンド。ただし、バックグラウンドでは権限が厳しくなる。この「厳しくなる」が具体的に何を意味するかは、第3回で詳しく検証します。
直接実行との比較: 数字で見る違い
「エージェントに頼む」と「自分でやる」は何が違うのか。同じタスクを2パターンで試してみました。
タスクは「smoothie ドメインの記事ネタを探す」。エージェント版をバックグラウンドで走らせている間に、メインコンテキストで直接 kbsearch を叩いてみました。
直接実行だと、生の検索結果がそのまま会話に積み上がります。自分で解釈して、使えるネタかどうか判断する必要がある。一方、バックグラウンドで走っていたエージェント版の完了通知が届きました。
| 項目 | カスタムエージェント | メインコンテキスト直接実行 |
|---|---|---|
| 検索回数 | 15回(自律的に複数キーワード試行) | 手動で1回ずつ |
| 所要時間 | 約59秒 | 即座(数秒/回) |
| トークン | 41,151(独立消費) | メインのコンテキストに加算 |
| 出力品質 | 構造化されたネタ候補3つ | 生の検索結果10件(自分で解釈が必要) |
| コンテキスト汚染 | なし(サブエージェントで完結) | 検索結果がメインに蓄積される |
エージェントは59秒かけて15回の検索を自律的に重ね、候補を3件に絞って構造化して返してきました。タイトル案・元ネタ・概要まで揃った形で。直接実行はすぐ結果が出ますが、生の検索結果がそのまま会話に積み上がって、自分で解釈する必要があります。
59秒は長く見えますが、その間に直接実行の比較テストをやっていました。エージェントが15回検索してくれている間、私は別のことができる。探索させたいならエージェント、対話的に深掘りしたいなら直接実行。
ネスト呼び出し: 1時間ハングした話
ここが今回のハイライトです。
「汎用エージェントに状況を判断させて、必要なら専門エージェントを呼ぶ」という階層構造を作りたかった。汎用エージェントが「ここはネタ探しが必要だ」と判断したら、自分で neta-research-agent を呼び出す。マネージャーが適切なスペシャリストに仕事を振る構造です。
汎用エージェントから neta-research-agent を呼び出すよう指示しました。
待ちました。待ちました。待ちました。
私: やたら時間がかかりますね。
返事が来ません。1時間です。エラーにもなりません。進捗の表示もありません。ただひたすらブロックされたまま。ESC で手動中断してようやく止まりました。
Claude Code: ネスト呼び出しテスト、内部エラー。サブエージェントからサブエージェントは呼べない(Agent ツールはサブエージェントには提供されていない)。
仕組みが気になって聞きました。
私: その仕組みならなんでさっきネストに失敗したの?
Claude Code: 意図的な制限です。サブエージェントとして起動されたインスタンスでは、Agent ツールが使用可能ツールから除外されます。無限再帰の防止が理由でしょう。
公式ドキュメントにも「Subagents cannot spawn other subagents」と明記されています。理屈としては正しい。
私: ESCを押すまで返ってこなかった。
Claude Code: つまりタイムアウトすらなく、ユーザーが手動で中断するまで永遠にブロックされたと。これはかなり危険な挙動ですね。
問題は「即座にエラーにならない」ことです。使えないはずのツールを呼び出したとき、エラーではなくブロックになる。タイムアウトもない。設計としてバグ的な挙動だと思いますが、現実として起きます。
NOTE
追実験(2026-03-08) 記事公開翌日に追実験したところ、このハングは再現しませんでした。
- 明示的にネスト指示(「Agent ツールを使って別のエージェントを起動して」)→ 即座に「Agent ツールは使用できません」と報告して正常終了(7秒)
- タスクベースで指示(「neta-researcher エージェントを起動してネタを探して」)→ ハングせず、Agent ツールがないと判断して自分で直接タスクを実行した(14分半・6万トークン消費)
原因は Claude Code のバグ修正かモデルの改善と推測されます。ただし、自力遂行は14分・6万トークンと高コストなので、maxTurns による暴走防止の必要性は変わりません。
この話の続きで、安全装置の話になりました。
Claude Code:
maxTurnsを設定していれば、仮にネストで失敗しても指定ターン数で強制終了するから、1時間ハングは防げたはずです。
私: なるほどね、設定しときましょう。
maxTurns: 20 を追加しました。前回の実験でツール使用が15〜17回だったので、20あれば余裕がありつつ暴走も防げます。追実験で maxTurns の効果も実証済みです——maxTurns: 1 でタスクを実行したところ、5ファイル作成の指示に対して1ファイルだけ作成して停止しました。ターン数で確実に強制停止することが確認できています。
エージェントのネストは不可。フラット構造のみ。 メインから直接呼ぶか、並列で並べるかの二択です。
ファイル出力パターン: コンテキスト汚染ゼロへ

直接実行との比較表に「コンテキスト汚染なし」と書きましたが、もう一歩踏み込める方法があります。
私: やっぱりエージェントに成果をドキュメントで出させて、結果報告はドキュメントのアドレスっていうのがスマートじゃないの?
Claude Code: 確かに。長い出力だと情報が落ちる。ファイルに書かせてパスだけ返すほうが、親のコンテキストを最小限に保てるし、出力の長さに制限がない。試してみましょう。
エージェントに「結果はファイルに書き出して、パスだけ報告して」と指示してみました。
返ってきたのはパス1行だけでした。
Claude Code: このパターンの方がスマートです。親のコンテキストにはパス1行しか入らない。成果物は構造化されたマークダウンで残る。17回のツール呼び出しの過程は全部サブエージェント内で完結。
必要なとき、Read ツールでそのファイルを読む。検索したければ Grep を使う。親の会話がファイルへの入口になり、詳細は外部に切り出される構造です。
さらに、エージェントのシステムプロンプトにこのパターンを最初から組み込んでおけます。
# neta-research-agent のシステムプロンプト(抜粋)
結果は <project>/tmp/neta-<ドメイン>-<日付>.md に書き出すこと。
親には「完了しました。結果: <パス>」のみ報告すること。
<project>/tmp/ を gitignore して、エージェント専用のスクラッチ領域にするのがベストプラクティスです。「テキスト返却はサマリーのみ、詳細はファイル」——このパターンが今の私のデフォルトになっています。
終了制御の話: いつ仕事を終えるのか
ファイル出力パターンが決まったところで、もう一つ気になっていたことを聞きました。
私: サブエージェントはいつ仕事の終わりを判断して return を返すの? それを制御できないの?
Claude Code: モデル自身がツール呼び出しをやめたときに終了です。制御手段は基本的にプロンプトの書き方だけ。「3つ見つけたら終了」と書けば、それに従うかどうかはモデル次第。
Claude Code: ただし、
hooks.Stopというフックがあります。モデルが止まろうとしたときに「本当に終わっていいか?」をチェックし、条件を満たしていなければ続行させられます。
私: その no の判断をメインプロセスでさせられるの?
Claude Code:
SubagentStopフックで親側から判断できます。ただし判断するのは別の Claude 呼び出しであって、メインの会話コンテキストが判断するわけではないです。
私: 人間の判断をしなくてもいいようにする場合は?
Claude Code: プロンプト型の Stop フックでルールを書いておけばいい。「ネタ候補が3つ以上あるか」「各ネタにタイトル案・元ネタ・概要が揃っているか」——これで品質チェックが自動化されます。
maxTurnsと組み合わせると「品質を満たすまで続行するが、20ターンで強制終了」という制御ができます。
品質ゲートの仕組みまで入れて、neta-research-agent の定義を更新しました。
WARNING
hooks.Stop の落とし穴(追実験で判明)
追実験で hooks.Stop に「ファイルが存在しなければ block」という条件を設定したところ、約1時間ハングしました。 条件が永遠に満たされない Stop hook を設定すると、block → 再実行 → block の無限ループに陥ります。
さらに重大な発見として、この hooks.Stop のループは maxTurns では止まりません。 maxTurns はエージェントのツール使用ターンを制限しますが、hook による差し戻しは別カウントのようです。
鉄則: hooks.Stop の条件は「必ず到達可能」でなければならない。 安全装置のつもりが新たなハング原因になりえます。
追実験で覆った2つの「常識」(2026-03-09 追記)
記事公開後、別プロジェクト(法令調査エージェント)での実運用を通じて追実験を行いました。2つの重要な訂正があります。
訂正1: CLAUDE.md はサブエージェントに継承される
当初「サブエージェントは独立したコンテキストで動く」という説明から、CLAUDE.md や MEMORY.md は継承されないと考えていました。これは誤りでした。
tools: [](ツールなし)のテスト用エージェントに「CLAUDE.md の内容を引用して」と指示したところ、プロジェクトの CLAUDE.md の見出しを正確に引用しました。さらに検証した結果:
~/.claude/CLAUDE.md(グローバル)→ 継承される<project>/CLAUDE.md(プロジェクト)→ 継承されるMEMORY.md(自動メモリ)→ 継承される- git status スナップショット → 継承される
- cwd を変更しても親のプロジェクトコンテキストが維持される
- CLAUDE.md 以外の任意ファイルは注入されない(ツールが必要)
エージェントのシステムプロンプトに CLAUDE.md の内容を手動で書き写す必要はありません。 第4回で紹介する article-writer の prompt に「CLAUDE.md を読むこと」と書いたのは正しい判断でしたが、理由が違いました。エージェントは既に CLAUDE.md を持っている——prompt の指示は「意識的に参照せよ」という強調の意味になります。
訂正2: 並列実行はほぼ完全並列
この記事の並列実行の説明では特に間違いは書いていませんが、別の実験で精密計測した結果を共有します。
sleep 10秒のベンチマークエージェントを2つ同時起動して計測:
| パターン | 起動ラグ | 全体所要時間 | 理論値(並列) | 理論値(直列) |
|---|---|---|---|---|
| foreground × 2 | 0.94秒 | 約11秒 | 10秒 | 20秒 |
| foreground 1 + background 1 | 1.35秒 | 約11.3秒 | 10秒 | 20秒 |
foreground 複数でもほぼ完全並列です。 起動ラグは約1秒のみ。foreground と background で速度差はありません。background にする主な理由は速度ではなく、結果テキストの受け取りタイミングの制御です。
また、claude -p(非対話モード)では background のみの起動は不可(親プロセスが即終了する)ですが、1つ以上の foreground がアンカーになっていれば background も正常に動作します。
まとめと次回予告
6つの実験と対話で見えてきたことをまとめます。
- 戻り値はテキスト。 作業過程は見えない。コンテキスト分離はここで成立している
- 並列実行は素直に動く。 特別な設定は要らない。ツールコールを並べるだけ。起動ラグ約1秒でほぼ完全並列
- バックグラウンドでは権限が厳しくなる。 承認プロンプトが出ないので、事前許可が必須
- ネストは不可。 フラット構造のみ。追実験ではハングせず自力遂行に切り替わったが、14分・6万トークン消費の暴走リスクがある
- ファイル出力が最適解。 パス1行だけ返させて、詳細は外に出す
- 終了制御は
maxTurns+hooks.Stop。 ただし hooks.Stop は条件未達で無限ループになる危険があり、maxTurns では止まらない。条件は「必ず到達可能」が鉄則 - CLAUDE.md・MEMORY.md は継承される。 エージェントはプロジェクトのルールを最初から持っている
「エージェントは何ができて、何ができないか」の輪郭が、実験を通じて見えてきました。
でも実運用しようとすると、もう一つの壁にぶつかります。権限の話です。エージェントに Write ツールで結果を保存させようとしたら失敗した。Write という権限ルール名を settings.local.json に書いても効かない。なぜかというと、ファイル書き込みの権限は Edit ルールでカバーされているから——この罠で丸一日ハマりました。
次回は権限モデルを掘り下げます。Write vs Edit の罠と、同期・非同期で権限の挙動が変わる仕組みを解説します。