6.1 KiB
s04: Subagents
サブエージェントは新しいメッセージリストで実行され、親とファイルシステムを共有し、要約のみを返す -- 親のコンテキストをクリーンに保つ。
問題
エージェントが作業するにつれ、メッセージ配列は膨張する。すべてのツール呼び出し、ファイル読み取り、bash出力が蓄積されていく。20-30回のツール呼び出しの後、コンテキストウィンドウは無関係な履歴で溢れる。ちょっとした質問に答えるために500行のファイルを読むと、永久に500行がコンテキストに追加される。
これは探索的タスクで特に深刻だ。「このプロジェクトはどのテストフレームワークを使っているか」という質問には5つのファイルを読む必要があるかもしれないが、親エージェントには5つのファイルの内容すべては不要だ -- 「pytest with conftest.py configuration」という回答だけが必要なのだ。
このコースでの実用的な解決策は fresh messages[] 分離だ: messages=[]で子エージェントを生成する。子は探索し、ファイルを読み、コマンドを実行する。終了時には最終的なテキストレスポンスだけが親に返される。子のメッセージ履歴全体は破棄される。
解決策
Parent agent Subagent
+------------------+ +------------------+
| messages=[...] | | messages=[] | <-- fresh
| | dispatch | |
| tool: task | ---------->| while tool_use: |
| prompt="..." | | call tools |
| | summary | append results |
| result = "..." | <--------- | return last text |
+------------------+ +------------------+
|
Parent context stays clean.
Subagent context is discarded.
仕組み
- 親エージェントにサブエージェント生成をトリガーする
taskツールが追加される。子はtaskを除くすべての基本ツールを取得する(再帰的な生成は不可)。
PARENT_TOOLS = CHILD_TOOLS + [
{"name": "task",
"description": "Spawn a subagent with fresh context.",
"input_schema": {
"type": "object",
"properties": {
"prompt": {"type": "string"},
"description": {"type": "string"},
},
"required": ["prompt"],
}},
]
- サブエージェントは委譲されたプロンプトのみを含む新しいメッセージリストで開始する。ファイルシステムは共有される。
def run_subagent(prompt: str) -> str:
sub_messages = [{"role": "user", "content": prompt}]
for _ in range(30): # safety limit
response = client.messages.create(
model=MODEL, system=SUBAGENT_SYSTEM,
messages=sub_messages,
tools=CHILD_TOOLS, max_tokens=8000,
)
sub_messages.append({
"role": "assistant", "content": response.content
})
if response.stop_reason != "tool_use":
break
# execute tools, append results...
- 最終テキストのみが親に返される。子の30回以上のツール呼び出し履歴は破棄される。
return "".join(
b.text for b in response.content if hasattr(b, "text")
) or "(no summary)"
- 親はこの要約を通常のtool_resultとして受け取る。
if block.name == "task":
output = run_subagent(block.input["prompt"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
})
主要コード
サブエージェント関数(agents/s04_subagent.py 110-128行目):
def run_subagent(prompt: str) -> str:
sub_messages = [{"role": "user", "content": prompt}]
for _ in range(30):
response = client.messages.create(
model=MODEL, system=SUBAGENT_SYSTEM,
messages=sub_messages,
tools=CHILD_TOOLS, max_tokens=8000,
)
sub_messages.append({"role": "assistant",
"content": response.content})
if response.stop_reason != "tool_use":
break
results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": str(output)[:50000]})
sub_messages.append({"role": "user", "content": results})
return "".join(
b.text for b in response.content if hasattr(b, "text")
) or "(no summary)"
s03からの変更点
| Component | Before (s03) | After (s04) |
|---|---|---|
| Tools | 5 | 5 (base) + task (parent) |
| Context | Single shared | Parent + child isolation |
| Subagent | None | run_subagent() function |
| Return value | N/A | Summary text only |
設計原理
このセッションでは、fresh messages[] 分離はコンテキスト分離を近似する実用手段だ。新しいmessages[]により、サブエージェントは親の会話履歴を持たずに開始する。トレードオフは通信オーバーヘッドで、結果を親へ圧縮して返すため詳細が失われる。これはメッセージ履歴の分離戦略であり、OSのプロセス分離そのものではない。サブエージェントの深さ制限(再帰スポーン不可)は無制限のリソース消費を防ぎ、最大反復回数は暴走した子処理の終了を保証する。
試してみる
cd learn-claude-code
python agents/s04_subagent.py
試せるプロンプト例:
Use a subtask to find what testing framework this project usesDelegate: read all .py files and summarize what each one doesUse a task to create a new module, then verify it from here