第4課:Subagent —— 拆解大任務(wù),上下文隔離

系列導(dǎo)讀

這是《12課拆解Claude Code架構(gòu)》系列的第 4 課。

前三課我們?cè)炝艘粋€(gè)有完整工具鏈和規(guī)劃能力的 Agent:第 1 課建了 Agent Loop,第 2 課加了 Tool Use dispatch,第 3 課用 TodoWrite 引入了顯式規(guī)劃。

但有一個(gè)問題一直在惡化——上下文膨脹。

第 4 課的格言:

"大任務(wù)拆小,每個(gè)小任務(wù)干凈的上下文"

這一課,我們給 Agent 裝上 task 工具,讓它能把子任務(wù)派發(fā)給獨(dú)立的 Subagent,用完即棄。


上下文膨脹:一個(gè)真實(shí)的痛點(diǎn)

假設(shè)你讓 Agent 回答一個(gè)簡(jiǎn)單問題:

"這個(gè)項(xiàng)目用什么測(cè)試框架?"

Agent 的工作過程:

第1輪: bash → cat package.json          (輸出: 200行JSON)
第2輪: bash → cat jest.config.js        (輸出: 30行配置)
第3輪: bash → cat tsconfig.json         (輸出: 50行JSON)
第4輪: bash → ls tests/                 (輸出: 15行文件列表)
第5輪: bash → head -20 tests/setup.ts   (輸出: 20行代碼)

5 輪下來,messages 數(shù)組里塞進(jìn)了 315 行工具輸出。但父 Agent 真正需要的答案只有一個(gè)詞:"Jest"

這不是個(gè)例。Agent 執(zhí)行的每一步——讀文件、跑命令、查日志——輸出都永久留在 messages 數(shù)組里。10 輪下來上下文可能漲到幾萬 token。50 輪下來,你可能已經(jīng)逼近上下文窗口的上限。

更要命的是,這些歷史輸出不只是占空間——它們會(huì)干擾模型的注意力。模型需要在一堆已經(jīng)過時(shí)的文件內(nèi)容中找到當(dāng)前任務(wù)的關(guān)鍵信息,準(zhǔn)確率和推理質(zhì)量都會(huì)下降。


解決方案:父子隔離架構(gòu)

核心思路極其簡(jiǎn)單:把子任務(wù)的臟活放到一個(gè)獨(dú)立的上下文里做,只把干凈的結(jié)果帶回來。

┌────────────────────────────────────────────────────────┐
│  父 Agent (messages[])                                 │
│                                                        │
│  User: "重構(gòu)這個(gè)模塊,先弄清楚測(cè)試框架"               │
│  Assistant: 我先調(diào)查測(cè)試框架 → tool_use: task          │
│                                                        │
│  ┌──────────────────────────────────────────────────┐  │
│  │  子 Agent (sub_messages[] — 獨(dú)立,用完即棄)      │  │
│  │                                                  │  │
│  │  User: "這個(gè)項(xiàng)目用什么測(cè)試框架?"                │  │
│  │  Assistant: → bash cat package.json  (200行)     │  │
│  │  Assistant: → bash cat jest.config.js (30行)     │  │
│  │  Assistant: → bash ls tests/         (15行)      │  │
│  │  Assistant: "項(xiàng)目使用 Jest 測(cè)試框架,配置..."    │  │
│  │                                                  │  │
│  │  ★ 整個(gè) sub_messages[] 在這里丟棄               │  │
│  └──────────────────────────────────────────────────┘  │
│                                                        │
│  tool_result: "項(xiàng)目使用 Jest 測(cè)試框架,配置在..."     │
│  Assistant: 好的,現(xiàn)在開始重構(gòu)... (繼續(xù)干凈地工作)     │
│                                                        │
└────────────────────────────────────────────────────────┘

關(guān)鍵機(jī)制:

  1. 父 Agent 擁有 task 工具 —— 可以把一個(gè)提示詞派發(fā)成子任務(wù)
  2. 子 Agent 用獨(dú)立的 sub_messages[] 啟動(dòng) —— 完全干凈的上下文
  3. 子 Agent 擁有除 task 外的所有工具 —— 能干活,但不能遞歸
  4. 只有最終文本返回給父 Agent —— 幾百行的中間過程,壓縮成幾句話
  5. 子 Agent 的消息歷史直接丟棄 —— 不污染父上下文

效果:父 Agent 的 messages 里只多了一條 tool_result,內(nèi)容是精煉的摘要。那 315 行中間輸出?消失了。


核心機(jī)制拆解

機(jī)制一:task 工具定義

給父 Agent 注冊(cè)一個(gè) task 工具,參數(shù)就是一個(gè)提示詞:

TASK_TOOL = {
    "name": "task",
    "description": (
        "Run a subtask in an isolated context. "
        "Use this for research, analysis, or any work whose "
        "intermediate output the parent does not need to see. "
        "Returns only the final text summary."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "prompt": {
                "type": "string",
                "description": "The task description for the subagent",
            }
        },
        "required": ["prompt"],
    },
}

這就是父 Agent 發(fā)起子任務(wù)的唯一接口。描述里明確告訴模型——這個(gè)工具適合"不需要看中間過程"的任務(wù)。

機(jī)制二:Subagent 循環(huán)

Subagent 本質(zhì)上就是一個(gè)迷你版的 Agent Loop,但有三個(gè)關(guān)鍵差異:

def run_subagent(prompt: str) -> str:
    """在隔離上下文中執(zhí)行子任務(wù),僅返回最終文本。"""
    sub_messages = [{"role": "user", "content": prompt}]
    
    # 子 Agent 可用的工具:除了 task 之外的所有工具
    sub_tools = [t for t in ALL_TOOLS if t["name"] != "task"]
    
    for turn in range(MAX_SUBAGENT_TURNS):  # 硬上限,防止失控
        response = client.messages.create(
            model=MODEL,
            system=SUBAGENT_SYSTEM,
            messages=sub_messages,
            tools=sub_tools,
            max_tokens=8000,
        )
        sub_messages.append({
            "role": "assistant", 
            "content": response.content,
        })
        
        # 子 Agent 決定不再調(diào)工具 → 任務(wù)完成
        if response.stop_reason != "tool_use":
            break
        
        # 執(zhí)行工具,收集結(jié)果
        results = []
        for block in response.content:
            if block.type == "tool_use":
                output = TOOL_HANDLERS[block.name](**block.input)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": output,
                })
        sub_messages.append({"role": "user", "content": results})
    
    # ★ 關(guān)鍵:只提取最終文本,sub_messages 整個(gè)丟棄
    return extract_text(response)

三個(gè)關(guān)鍵差異:

對(duì)比項(xiàng) 父 Agent 子 Agent
消息列表 全局 messages,持續(xù)累積 局部 sub_messages,用完即棄
可用工具 所有工具,包括 task 所有工具,排除 task
輪次限制 無硬上限(教學(xué)版) MAX_SUBAGENT_TURNS = 30

機(jī)制三:結(jié)果提煉

extract_text 函數(shù)極其簡(jiǎn)單——從 response 的 content blocks 里只取文本:

def extract_text(response) -> str:
    """從響應(yīng)中提取純文本,丟棄工具調(diào)用塊。"""
    parts = []
    for block in response.content:
        if hasattr(block, "text"):
            parts.append(block.text)
    return "\n".join(parts) if parts else "(subagent produced no text output)"

子 Agent 的最后一輪回復(fù)通常是自然語(yǔ)言摘要——"項(xiàng)目使用 Jest 測(cè)試框架,配置了 TypeScript 轉(zhuǎn)換器,覆蓋率閾值 80%"。這就是父 Agent 需要的全部信息。


完整代碼:父 Agent 的 dispatch 更新

在第 2 課的 dispatch map 基礎(chǔ)上,只需要加兩行:

# 工具列表:在原有工具基礎(chǔ)上加入 task
PARENT_TOOLS = BASE_TOOLS + [TASK_TOOL]

# dispatch map:加入 task → run_subagent 映射
TOOL_HANDLERS = {
    "bash":  run_bash,
    "read":  read_file,
    "write": write_file,
    "edit":  edit_file,
    "todo":  todo_write,
    "task":  lambda prompt: run_subagent(prompt),  # 新增
}

父 Agent Loop 本身一行不改。當(dāng)模型決定調(diào)用 task 工具時(shí),dispatch map 自動(dòng)路由到 run_subagent,執(zhí)行完把結(jié)果字符串作為 tool_result 追加到父 messages。

這就是第 2 課 dispatch 架構(gòu)的威力——加新能力只加注冊(cè),循環(huán)不碰。


實(shí)際運(yùn)行示例

給 Agent 一個(gè)復(fù)合任務(wù):

"分析這個(gè)項(xiàng)目的技術(shù)棧,然后在 README 里補(bǔ)充技術(shù)棧說明"

Agent 的執(zhí)行鏈路:

父 Agent:
  思考: 這個(gè)任務(wù)有兩步——先調(diào)研技術(shù)棧,再寫文檔。
        調(diào)研部分會(huì)產(chǎn)生大量中間輸出,用 task 隔離。

  tool_use: task(prompt="分析這個(gè)項(xiàng)目的技術(shù)棧,
            包括語(yǔ)言、框架、測(cè)試工具、構(gòu)建工具")

    ┌─ 子 Agent (獨(dú)立上下文) ─────────────────────┐
    │ bash → cat package.json         (200行)     │
    │ bash → cat tsconfig.json        (50行)      │
    │ bash → ls src/                  (20行)      │
    │ bash → cat vite.config.ts       (30行)      │
    │ bash → cat jest.config.js       (25行)      │
    │ bash → cat Dockerfile           (15行)      │
    │                                              │
    │ 最終回復(fù): "技術(shù)棧分析:                      │
    │   - 語(yǔ)言: TypeScript 5.3                     │
    │   - 前端: React 18 + Vite 5                  │
    │   - 測(cè)試: Jest + Testing Library             │
    │   - 構(gòu)建: Docker + GitHub Actions            │
    │   - 包管理: pnpm"                            │
    │                                              │
    │ ★ 340行中間輸出在此丟棄                      │
    └──────────────────────────────────────────────┘

  tool_result: "技術(shù)棧分析:語(yǔ)言 TypeScript 5.3..."
                (只有5行摘要進(jìn)入父上下文)

  思考: 拿到技術(shù)棧信息了,現(xiàn)在編輯 README。
  tool_use: read(path="README.md")
  tool_use: edit(path="README.md", ...)

  最終回復(fù): "已在 README.md 中補(bǔ)充了技術(shù)棧說明。"

對(duì)比效果:

指標(biāo) 不用 Subagent 用 Subagent
父上下文增長(zhǎng) +340 行工具輸出 +5 行摘要
模型注意力 被過時(shí)輸出稀釋 始終聚焦當(dāng)前任務(wù)
Token 消耗 每輪都帶歷史 子歷史隔離不累積

洞見:兩個(gè)關(guān)鍵設(shè)計(jì)決策

為什么禁止遞歸

子 Agent 沒有 task 工具,所以它不能再派生子子 Agent。這是刻意的限制。

表面原因:防止失控。 如果子 Agent 能再派子 Agent,一個(gè)寫得不好的提示詞可能導(dǎo)致無限遞歸,每一層都消耗 API 調(diào)用和 token。

深層原因:兩層就夠了。 實(shí)際使用中,父 Agent 拆解的子任務(wù)粒度已經(jīng)足夠小——"查測(cè)試框架"、"分析依賴關(guān)系"、"統(tǒng)計(jì)代碼行數(shù)"。這些任務(wù)不需要再拆解,一個(gè) Subagent 在 30 輪內(nèi)完全能搞定。

Claude Code 的生產(chǎn)實(shí)現(xiàn)也是這個(gè)設(shè)計(jì):Task 工具的子 Agent 禁止使用 Task。如果你需要更深的分解,正確的做法是讓父 Agent 拆出更多并列的子任務(wù),而不是讓子任務(wù)嵌套下去。

? 正確: 父 → [子A, 子B, 子C]      (寬度擴(kuò)展)
? 錯(cuò)誤: 父 → 子 → 孫 → 曾孫        (深度遞歸)

為什么丟棄子歷史

子 Agent 結(jié)束后,sub_messages[] 直接被垃圾回收。為什么不保留?

不是因?yàn)闆]用,而是因?yàn)樾詢r(jià)比不夠。

保留子歷史有兩個(gè)成本:

  1. Token 成本:子歷史留在父上下文里,后續(xù)每輪 API 調(diào)用都要帶上,反復(fù)付費(fèi)。
  2. 注意力成本:模型的注意力是有限資源。無關(guān)的歷史信息會(huì)稀釋模型對(duì)當(dāng)前任務(wù)的聚焦度。

而保留子歷史的收益呢?幾乎為零。父 Agent 在后續(xù)工作中需要引用"子 Agent 第 3 輪讀的那個(gè)文件的第 47 行"的概率極低。它需要的是結(jié)論,不是過程。

這和現(xiàn)實(shí)中的管理一樣——你派一個(gè)工程師去調(diào)研技術(shù)方案,你要的是一份摘要報(bào)告,不是他瀏覽過的每一個(gè)網(wǎng)頁(yè)的截圖。


Subagent 的系統(tǒng)提示詞

子 Agent 有自己的系統(tǒng)提示詞,比父 Agent 更簡(jiǎn)潔聚焦:

SUBAGENT_SYSTEM = """You are a focused research and analysis agent.

Your job is to complete the specific task given to you, then provide
a clear, concise summary of your findings.

Guidelines:
- Stay focused on the given task
- Be thorough but efficient
- End with a clear summary of findings
- Do not ask for clarification — work with what you have
"""

注意最后一條——"不要問澄清問題"。子 Agent 沒有和用戶交互的通道,它只能和工具交互。所以它必須根據(jù)提示詞自行判斷、自行行動(dòng)、自行總結(jié)。


輪次上限:為什么是 30

MAX_SUBAGENT_TURNS = 30

30 不是拍腦袋的數(shù)字:

  • 太小(<10):復(fù)雜調(diào)研任務(wù)可能需要讀多個(gè)文件、跑多個(gè)命令,10 輪不夠
  • 太大(>50):如果 30 輪都沒搞定,大概率是任務(wù)拆解粒度不對(duì),繼續(xù)跑只是浪費(fèi) token
  • 30 是經(jīng)驗(yàn)甜點(diǎn):覆蓋 95%+ 的合理子任務(wù),同時(shí)在失控時(shí)及時(shí)止損

生產(chǎn)環(huán)境可以根據(jù)任務(wù)類型動(dòng)態(tài)調(diào)整,但 30 是一個(gè)穩(wěn)健的默認(rèn)值。


五分鐘跑起來

# 進(jìn)入項(xiàng)目目錄
cd learn-claude-code

# 啟動(dòng)第四課
python agents/s04_subagent.py

啟動(dòng)后你會(huì)看到 s04 >> 提示符。以下是真實(shí)的運(yùn)行記錄:

任務(wù) 1:用子任務(wù)做調(diào)研

s04 >> Use a subtask to find what testing framework this project uses
> task (Find the testing framework used in this project):
  (子 Agent 獨(dú)立執(zhí)行:讀取 package.json、requirements.txt、
   tests/ 目錄、多個(gè)測(cè)試文件……全部在隔離上下文中完成)

  子 Agent 返回摘要 ↓

## Results
The project uses **two Python testing frameworks**:

| Framework | Version | Role |
|-----------|---------|------|
| **pytest** | 9.0.2 | Primary — used across tests/ and mypackage/tests/ |
| **unittest** | stdlib | Secondary — used in one file with TestCase style |

Notably, the web/ subdirectory has **no JavaScript testing
framework** configured.

父 Agent 只拿到了一段精煉摘要。子 Agent 讀了多少文件、跑了多少命令?父 Agent 不知道,也不需要知道。

任務(wù) 2:大規(guī)模文件摘要(并行子任務(wù))

s04 >> Delegate: read all .py files and summarize what each one does
> bash: (列出所有 25 個(gè) .py 文件)

> task (Summarize top-level .py files): ...
> task (Summarize mypackage .py files): ...
> task (Summarize agents s01-s06 .py files): ...
> task (Summarize agents s07-s12 and s_full .py files): ...
> task (Summarize tests and skills .py files): ...

Here's a complete summary of all 25 `.py` files:

## ?? Top-Level
| File | Summary |
|------|---------|
| hello.py | greet(name) function returning "Hello, {name}!" |
| greet.py | Minimal module with a single greet(name) function |

## ?? agents/ — Progressive Agent Harness (s01–s12)
| File | Summary |
|------|---------|
| s01_agent_loop.py | The Agent Loop — minimal core |
| s02_tool_use.py | Tool Dispatch — dispatch map |
| s03_todo_write.py | TodoWrite — planning system |
| s04_subagent.py | Subagents — context isolation |
| ... | (后續(xù) 8 個(gè) agent 文件的完整摘要) |

父 Agent 把 25 個(gè)文件分成 5 批,派出 5 個(gè)子任務(wù)分別處理。每個(gè)子 Agent 讀取多個(gè)文件并生成摘要,所有中間的文件內(nèi)容(幾千行)都在子上下文中丟棄,父 Agent 只拿到結(jié)構(gòu)化的匯總表格。

任務(wù) 3:創(chuàng)建并驗(yàn)證

s04 >> Use a task to create a new module, then verify it from here
> task (Create a new JavaScript utility module):
  (子 Agent 獨(dú)立創(chuàng)建 src/utils/stringUtils.js,
   包含 capitalize、reverseString、isPalindrome、truncate 四個(gè)函數(shù))

  子 Agent 返回摘要 ↓

> bash: cat src/utils/stringUtils.js
  (父 Agent 自己讀取文件驗(yàn)證內(nèi)容)
> bash: node -e "..." (逐個(gè)測(cè)試每個(gè)函數(shù))

? Module created and verified successfully!

| Function | Test Input | Expected | Actual | Status |
|---|---|---|---|---|
| capitalize | 'hello' | 'Hello' | 'Hello' | ? |
| reverseString | 'hello' | 'olleh' | 'olleh' | ? |
| isPalindrome | 'racecar' | true | true | ? |
| truncate | 'Hello, World!', 5 | 'Hello...' | 'Hello...' | ? |

注意分工:子 Agent 負(fù)責(zé)創(chuàng)建,父 Agent 負(fù)責(zé)驗(yàn)證。 創(chuàng)建過程中的所有中間輸出(文件寫入、目錄創(chuàng)建)留在子上下文里;父 Agent 拿到摘要后,在自己干凈的上下文中獨(dú)立驗(yàn)證結(jié)果。這就是"隔離但協(xié)作"。


變更表

組件 第 3 課 (TodoWrite) 第 4 課 (Subagent)
上下文模型 單一共享 messages[] 父 messages[] + 子 sub_messages[]
子任務(wù)機(jī)制 run_subagent() 函數(shù)
工具列表 bash, read, write, edit, todo +task (僅父 Agent)
上下文隔離 子任務(wù)獨(dú)立上下文,用完即棄
遞歸控制 子 Agent 禁止使用 task
輪次限制 子 Agent 30 輪上限
結(jié)果傳遞 僅最終文本返回父上下文
新增代碼 ~30 行 ~50 行

下一課預(yù)告

第 4 課解決了上下文膨脹,但 Agent 的能力范圍還是固定的——它只能用 system prompt 里寫死的知識(shí)。

第 5 課:Skills —— 動(dòng)態(tài)加載技能。讓 Agent 能根據(jù)任務(wù)需要,從外部加載專業(yè)知識(shí)和操作指南。就像一個(gè)工程師遇到陌生領(lǐng)域時(shí)翻文檔,而不是什么都靠腦子記。

# 預(yù)告:s05 的技能加載
def load_skill(skill_name: str) -> str:
    skill_path = f"skills/{skill_name}.md"
    return read_file(skill_path)

# 技能內(nèi)容注入系統(tǒng)提示詞
system = BASE_SYSTEM + "\n\n" + load_skill("python-testing")

從"硬編碼知識(shí)"到"按需加載",Agent 的知識(shí)邊界第一次變得可擴(kuò)展。


這是《12課拆解Claude Code架構(gòu):從零掌握Agent Harness工程》系列的第 4 課。關(guān)注Claw開發(fā)者,不錯(cuò)過后續(xù)更新。

完整代碼和交互式學(xué)習(xí)平臺(tái):github.com/shareAI-lab/learn-claude-code

如果這篇文章對(duì)你有幫助,歡迎轉(zhuǎn)發(fā)給你的技術(shù)團(tuán)隊(duì)。

系列目錄

  • 第1課:用20行Python造出你的第一個(gè)AI Agent
  • 第2課:給Agent加工具 —— dispatch map模式詳解
  • 第3課:TodoWrite —— 讓Agent先想后做:規(guī)劃系統(tǒng)
  • 第4課:Subagent —— 拆解大任務(wù),上下文隔離(本文)
  • 第5課:按需加載領(lǐng)域知識(shí)——Skill機(jī)制
  • 第6課:無限對(duì)話——上下文壓縮三層策略
  • 第7課:任務(wù)持久化——文件級(jí)DAG任務(wù)圖
  • 第8課:后臺(tái)執(zhí)行——異步任務(wù)與通知隊(duì)列
  • 第9課:Agent Teams——多Agent協(xié)作:團(tuán)隊(duì)與郵箱系統(tǒng)
  • 第10課:團(tuán)隊(duì)協(xié)議——狀態(tài)機(jī)驅(qū)動(dòng)的協(xié)商
  • 第11課:自治Agent——自組織任務(wù)認(rèn)領(lǐng)
  • 第12課:終極隔離——Worktree并行執(zhí)行
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容