chore: Spring AI 重构

This commit is contained in:
abel533
2026-03-25 00:15:00 +08:00
parent a9c71002d2
commit 2afa4712cb
124 changed files with 11777 additions and 3530 deletions
+59 -51
View File
@@ -1,4 +1,4 @@
# s04: Subagents
# s04: Subagents (サブエージェント)
`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
@@ -8,7 +8,7 @@
## 問題
エージェントが作業するにつれ、messages配列は膨張し続ける。すべてのファイル読み取り、すべてのbash出力がコンテキストに永久に残る。「このプロジェクトはどのテストフレームワークを使っているか」という質問は5つのファイルを読む必要があるかもしれないが、親に必要なのは「pytest」という答えだけだ。
エージェントが作業するにつれ、messages 配列は膨張し続ける。すべてのファイル読み取り、すべてのコマンド出力がコンテキストに永久に残る。「このプロジェクトはどのテストフレームワークを使っているか」という質問は5つのファイルを読む必要があるかもしれないが、親エージェントに必要なのは「pytest」という一言だけだ。
## 解決策
@@ -28,67 +28,75 @@ Parent context stays clean. Subagent context is discarded.
## 仕組み
1.`task`ツールを追加する。子は`task`を除くすべての基本ツールを取得する(再帰的な生成は不可)
1.エージェントに `task` ツールを持たせる。子は `task` を除くすべての基本ツールを持つ(再帰的な生成は不可
```python
PARENT_TOOLS = CHILD_TOOLS + [
{"name": "task",
"description": "Spawn a subagent with fresh context.",
"input_schema": {
"type": "object",
"properties": {"prompt": {"type": "string"}},
"required": ["prompt"],
}},
]
```
2. サブエージェントは`messages=[]`で開始し、自身のループを実行する。最終テキストだけが親に返る。
```python
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,
```java
// 親 Agent: 基本ツール + SubagentTool を持つ
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a coding agent. "
+ "Use the task tool to delegate subtasks.")
.defaultTools(
new BashTool(),
new ReadFileTool(),
new WriteFileTool(),
new EditFileTool(),
new SubagentTool(chatModel) // 親 Agent 専用
)
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)"
.build();
```
子のメッセージ履歴全体(30回以上のツール呼び出し)は破棄される。親は1段落の要約を通常の`tool_result`として受け取る。
2. サブエージェントは新しい `ChatClient` で起動し、独立したコンテキストを持つ。最終テキストだけが親に返る。
## s03からの変更点
```java
@Tool(description = "Spawn a subagent with fresh context. "
+ "Use for exploration or subtasks that might pollute the main context.")
public String task(
@ToolParam(description = "The task prompt") String prompt,
@ToolParam(description = "Short description", required = false)
String description) {
| 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 |
// 新しい ChatClient を作成 -- これが「コンテキスト隔離」のすべて
ChatClient subClient = ChatClient.builder(chatModel)
.defaultSystem("You are a coding subagent. "
+ "Complete the task, then summarize findings.")
.defaultTools( // 基本ツール、task なし(再帰防止)
new BashTool(),
new ReadFileTool(),
new WriteFileTool(),
new EditFileTool()
)
.build();
String result = subClient.prompt()
.user(prompt)
.call()
.content();
// 最終テキストだけを返し、子 Agent のコンテキストは破棄
return (result != null) ? result : "(no summary)";
}
```
サブエージェントは複数回のツール呼び出しを実行するかもしれないが、メッセージ履歴全体は破棄される。親が受け取るのは要約テキストだけで、通常の `tool_result` として返される。Spring AI の `ChatClient.call()` が内部でツールループを管理するため、手動でイテレーション回数を制限する必要はない。
## s03 からの変更点
| コンポーネント | 変更前 (s03) | 変更後 (s04) |
|----------------|------------------|---------------------------------------|
| Tools | 5 | 5 (基本) + SubagentTool (親側のみ) |
| コンテキスト | 単一共有 | 親 + 子隔離 (独立した ChatClient) |
| Subagent | なし | `SubagentTool.task()` メソッド |
| 戻り値 | 該当なし | 要約テキストのみ |
## 試してみる
```sh
cd learn-claude-code
python agents/s04_subagent.py
mvn exec:java -Dexec.mainClass=io.mybatis.learn.s04.S04Subagent
```
以下のプロンプトを試してみよう (英語プロンプトの方が LLM に効果的だが、日本語でも可):
1. `Use a subtask to find what testing framework this project uses`
2. `Delegate: read all .py files and summarize what each one does`
2. `Delegate: read all .java files and summarize what each one does`
3. `Use a task to create a new module, then verify it from here`