$ cat ~/writing/2026-04-26-multi-agent-patterns.md

讀 LangChain 的 Multi-Agent 四模式分類:Supervisor、Handoff、Router、Skills

DATE2026·04·26
TAGSAI · Agents
READING3 min

這是讀 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 用量。我把它併成一張表:

模式單次請求重複請求多領域查詢
Subagents4 次4 次5 次 / ~9K
Handoffs3 次2 次7+ 次 / ~14K+
Router3 次3 次5 次 / ~9K
Skills3 次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)

參考