這是讀 LangChain Choosing the Right Multi-Agent Architecture 的筆記,不是實戰報告 —— 四模式的分類和效能數字都出自那一篇,我沒有自己重跑。會整理成這篇是因為「Multi-Agent」這個詞被用得太鬆,而那篇把它拆成四種在狀態管理上真的不同的東西,這個切法有用。
先講結論:四種模式裡,Claude Code 只暴露兩種(Subagents 和 Skills),而它們的差別幾乎全在 context 要不要隔離。其餘兩種(Handoffs、Router)是給對話型產品用的,寫 coding agent 用不到。
四種模式的差別在哪
| 模式 | 核心特徵 | 狀態管理 | 執行方式 |
|---|---|---|---|
| Subagents (Supervisor) | 中心化編排 | 主 Agent 有狀態,Subagents 無狀態 | 可並行 |
| Handoffs | 對等轉移 | 狀態跨 Agent 持久化 | 序列 |
| Router | 並行分派 | 無狀態 | 並行 |
| Skills | 動態加載 | 單一 Agent 累積上下文 | 序列/並行 |
| 模式 | 適合 | 不適合 |
|---|---|---|
| Subagents (Supervisor) | 任務可分解 + 子任務獨立 | 需要 agent 間長時序記憶 |
| Handoffs | 流程中切換專精角色(客服 → 技術支援) | 任務無明確接力點 |
| Router | 純分派(問題分類後分發) | 需要彙整跨 agent 結果 |
| Skills | 動態工具加載 | 多角色協作 |
真實系統會混用:一個 Supervisor 底下,每個 Subagent 本身可能又是 Skills 模式。Claude Code 就是這樣 —— Agent tool 指定 subagent_type 是 Subagents,Skill tool 動態載入 .md 是 Skills,兩者並存。
Subagents(Supervisor)
也稱 Hierarchical、Orchestrator-Workers、Manager-Worker。
flowchart TD U[用戶] --> S[Supervisor] S --> A[Sub A] & B[Sub B] & C[Sub C] A & B & C -.彙整.-> S
所有請求經過 Supervisor 分派,Sub Agents 各自獨立執行後回報,由 Supervisor 彙整。關鍵性質是每個 Sub Agent 有自己的 context 窗口,主 session 只收到它的回報,不收它讀過的一百個檔案。
這是它真正的賣點,也是它唯一無法替代的地方。代價有三個,而且都不小:
- 每次互動多一次 model call(要經過 Supervisor)
- Sub Agents 無記憶。同一個 subagent 被叫第二次時,不知道第一次做過什麼
- Supervisor 會變瓶頸 —— 所有分派決策都在它身上,它的 context 反而是最容易爆的
第二點是實務上最容易低估的。context 隔離同時也是資訊隔離:主 session 拿到的只有 subagent 願意寫進回報的那幾行。如果任務的難處在於「要邊做邊改判斷」,隔離會讓你付兩次成本 —— subagent 摸索一輪、寫個摘要、主 session 看不懂、再派一次。所以會分派的前提是問題邊界已經穩定;還在摸的東西留在主 session 反而便宜。
適用場景:個人助理(協調日曆、郵件、CRM)、研究系統(分派給領域專家)、需要平行執行的多領域查詢。
Skills(技能加載)
也稱 Progressive Disclosure。
flowchart TD U[用戶] <--> SA[Single Agent] SA -.load.-> K1[Skill A] & K2[Skill B] & K3[Skill C]
本質上是單一 Agent,啟動時只知道 skill 的名稱和描述,判斷要用哪個才把完整 prompt 載進來。所以它不太算 Multi-Agent,那篇原文用的詞是 quasi-multi-agent。
跟 Subagents 剛好互補:好處是單一 agent、好 debug、agent 直接跟用戶對話,不用經過摘要這層失真;壞處是載進來的 skill 內容會留在對話歷史裡累積,而且 skill 之間沒有邊界,載了三個就三份 prompt 同時在影響同一個模型。
實務上的分界線很清楚:要的是行為約束就用 Skills(規則要影響主 session 的判斷,不能隔離),要的是大量讀檔就用 Subagents(讀完的東西不該留在主 context)。
Handoffs 與 Router
這兩種是對話型產品的模式,coding agent 用不到,簡單記一下。
Handoffs(對等轉移、Relay):所有 agent 地位平等,當下只有一位活躍,轉移時把累積的上下文交給下一位,活躍的那位可以直接跟用戶對話。適合分段收集資訊的客服流程、有前置條件的工作流。缺點是序列執行,無法平行處理多領域查詢,而且上下文會一路長大。
Router(並行分派、Query Decomposition):Router 把任務拆成子查詢並行發給專精 agent,最後由獨立的 Synthesizer 彙整。無狀態,每次請求獨立。適合企業知識庫這種一次性的多來源查詢,不適合多輪對話 —— 它沒有記憶。
flowchart TD U[用戶] --> R[Router] R --> A[Agent A] & B[Agent B] & C[Agent C] A & B & C --> SY[Synthesizer] SY --> U2[用戶]
那篇給的效能數字,以及它能推到多遠
原文列了各模式的 model call 次數與 token 用量。我把它併成一張表:
| 模式 | 單次請求 | 重複請求 | 多領域查詢 |
|---|---|---|---|
| Subagents | 4 次 | 4 次 | 5 次 / ~9K |
| Handoffs | 3 次 | 2 次 | 7+ 次 / ~14K+ |
| Router | 3 次 | 3 次 | 5 次 / ~9K |
| Skills | 3 次 | 2 次 | 3 次 / ~15K |
這些數字的條件原文沒有交代:哪個模型、什麼任務、跑幾次取平均、prompt 多長,都沒寫。所以能讀出來的只有量級關係,不是可移植的基準:
- 多領域查詢下,Skills 的 15K 對 Subagents 的 9K —— Skills 多用約 67%,換個方向講是 Subagents 省下約 40%。這是 context 累積 vs context 隔離的必然結果,方向可信
- Handoffs 的重複請求從 3 次降到 2 次,是狀態保留省掉的那次分類。這也是結構必然
- 至於 9K 到底是 9K 還是 20K,換個任務就不一樣,別拿這個數字做容量規劃
這類比較常見的做法是給每個模式打星等分數,這篇刻意不打。同一個模式在不同工作型態下的分數差很多 —— 「平行執行」對批次讀檔是滿分優勢,對需要邊做邊改判斷的任務是零。固定星數只會把這個落差抹平,上面兩張差異表已經夠決定要用哪個。
選哪個
| 你的需求 | 模式 |
|---|---|
| 多個獨立領域,要平行執行,而且問題邊界已經穩定 | Subagents |
| 要的是行為約束,規則得影響主 session 的判斷 | Skills |
| 序列工作流,有狀態轉移,agent 全程跟用戶對話 | Handoffs |
| 獨立垂直領域,平行查多來源再合成 | Router |
反過來說,什麼時候不要升級:單一 agent 的 context 還撐得住、任務還在摸索階段、或者你分不出子任務的邊界在哪 —— 這三種情況下加 agent 只會讓 debug 變難。Multi-Agent 解的是 context 和分工的問題,不是能力問題。
下面的程式碼是示意,不是可跑的
這幾段是把上面的狀態管理差異寫成 pseudo code 幫助理解,函式名是我編的,不對應任何框架的真實 API。要實作請直接看框架文件。
Subagents —— 重點在 context={},每次都是新鮮的:
def subagents(task):
subtasks = supervisor.plan(task)
results = []
for st in subtasks:
results.append(run_subagent(st, context={})) # 無狀態
return supervisor.synthesize(results)
Handoffs —— 重點在 context 跨 agent 保留:
def handoffs(task):
state = {"agent": "a", "context": {"task": task, "history": []}}
while not done(state):
r = get_agent(state["agent"]).process(state["context"])
state["context"]["history"].append(r.summary)
if r.needs_handoff:
state["agent"] = r.next_agent # context 跟著走
return state["context"]["history"]
Skills —— 重點在載入的內容留在 history 裡:
def skills(task, agent_context):
# 啟動時 agent_context 只有 [{name, description}, ...]
name = pick_skill(task, agent_context["skills"])
if name:
agent_context["history"].append(load_skill(name)) # 累積,不會被回收
return agent.process(task, agent_context)
參考
- Choosing the Right Multi-Agent Architecture —— 四模式分類與本文所有效能數字的唯一來源
- Anthropic — Building Effective Agents —— Orchestrator-Workers 的原始描述,以及「用最簡單能解決問題的做法」那條原則
- arXiv:2604.18071 —— 分析 70 個公開 agent 專案,把 subagent architecture 列為五個核心設計維度之一