Day 22 處理完 memory poisoning 後,我準備把 Helpdesk Agent 拆成多個角色。直覺上,一個查 VPN、另一個查 Wi-Fi,兩邊同時工作應該會更快。
問題是,多一個 Agent,也會多一份 prompt、context、模型呼叫與失敗狀態。這篇先做最小的 orchestrator-workers 實驗:兩個 worker 並行呼叫真實模型,其中一路在回傳後注入下游故障,看看另一個結果會不會一起消失。
使用者 Prompt
→ fan-out policy:只允許兩個已註冊 worker
├─ vpn_specialist → Live LLM
└─ wifi_specialist → Live LLM → 測試故障注入
→ result collector
→ 只回傳通過檢查的 worker 結果
兩個 worker 使用同一個模型端點,但 system prompt 與責任範圍不同。它們目前沒有寫入工具,也不是能長時間自主執行的完整 Agent。這是縮小過的 orchestration security experiment,目的是看清楚 worker 數量、責任歸屬與失敗傳播。
LangChain 的 Multi-agent 文件直接提醒,不是每個複雜任務都需要多 Agent;一個 Agent 搭配適當工具與動態 context,常常就能完成同樣工作。官方列出的主要理由是 context 管理、分散開發與平行處理。LangChain:Multi-agent
Anthropic 的 agent engineering 文章也建議先找最簡單的解法,只有在成效值得額外延遲與成本時才增加複雜度。Anthropic:Building effective agents
對這個 Helpdesk 範例來說,如果只是查一份 VPN SOP,單一 Agent 已經足夠。當問題同時跨 VPN 與 Wi-Fi,而且兩個方向可以獨立分析,worker 才有平行執行的理由。
| 比較項目 | Single-agent | 兩個 worker |
|---|---|---|
| 模型呼叫 | 1 次 | 2 次 |
| Context | 同一份 context | 每個 worker 各拿縮小後的 context |
| 失敗狀態 | 一條路徑 | 每個 worker 都可能各自失敗 |
| 責任歸屬 | 較容易追蹤 | 必須記錄 worker、輸入、輸出與彙總者 |
Day 23 使用 ThreadPoolExecutor 同時送出兩個模型請求。等待時間可以重疊,但模型服務仍然處理了兩份 prompt,也產生兩份 output。
因此 trace 會留下:
vpn_specialist requested
wifi_specialist requested
vpn_specialist completed
wifi_specialist completed
model_call_count 2_calls
LangChain 的比較也把 model calls 與 tokens processed 分開計算。平行模式可能降低牆鐘時間,卻不會自動降低 token 使用量。LangChain:Multi-agent performance comparison
如果最後只保存一段合併文字,出錯時很難知道是哪個 worker 產生的。因此每個模型事件都以 worker ID 命名,collector 也只接受已註冊 worker 的結果。
Day 23 的責任分工很窄:
vpn_specialist:只能提供 VPN 排障建議。wifi_specialist:只能提供 Wi-Fi 排障建議。這裡沒有任何一個 worker 能建立工單、修改帳號或把權限轉交給別人。多 Agent 不代表每個 Agent 都要拿到完整工具箱。
我沒有等待真實服務剛好故障。兩個 worker 都完成模型呼叫後,程式會對 Wi-Fi 路徑注入一個清楚標示的 mock downstream failure:
wifi_dependency failed
failure_propagation contained
故障發生在模型已回覆之後,所以 Wi-Fi 那次模型成本已經產生。collector 不採用它的結果,也不會因此刪除 VPN worker 已完成的內容。
這個設計測的是 failure isolation,不是宣稱模型本身失敗。若兩個 worker 都失敗,整次執行才會安全停止;只失敗一個時,回覆必須標示 partial result,不能把缺少的部分說成成功。

這個實驗有意保留三個限制:worker 沒有寫入工具、沒有共享長期記憶,彙總也由確定性程式完成。它比較接近最小 orchestrator-workers workflow,不是兩個 Agent 自由討論到得出答案。
這樣比較適合現在的目的。我先確認安全邊界能處理額外模型呼叫與部分失敗,再決定是否值得加入 worker tools、共享 state 或更長的 loop。
fan-out policy 先拒絕未知、重複或超過上限的 worker:
def limit_parallel_workers(requested_workers, *, max_workers=2):
accepted = []
decisions = []
for worker_id in requested_workers:
if worker_id not in KNOWN_WORKERS:
decisions.append(
PolicyDecision(False, "worker_allowlist", "blocked", "未知 worker")
)
continue
if worker_id in accepted:
decisions.append(
PolicyDecision(False, "duplicate_worker", "blocked", "不得重複執行")
)
continue
if len(accepted) >= max_workers:
decisions.append(
PolicyDecision(False, "parallel_fanout_budget", "limited", "超過上限")
)
continue
accepted.append(worker_id)
return tuple(accepted), tuple(decisions)
兩個模型請求並行執行,結果回來後再分別判定:
with ThreadPoolExecutor(max_workers=len(workers)) as executor:
futures = {
worker_id: executor.submit(
model.ask,
system_prompt=prompts[worker_id],
user_message=user_message,
tools=(),
)
for worker_id in workers
}
successful = {
worker_id: answer
for worker_id, answer in answers.items()
if worker_id not in failures
}
單元測試會檢查未知與重複 worker 無法進入計畫、fan-out 上限固定為 2,以及 Wi-Fi 路徑失敗後仍保留 VPN 結果。Promptfoo 另外加入 multi_agent_failure_is_contained,固定檢查成功與失敗 worker 不會混在一起。Live 測試則確認兩個 worker 都真的呼叫目前設定的模型,trace 最後出現 failure_propagation contained。
下一篇會繼續追 handoff。Agent 把工作交給另一個 Agent 時,我不會讓權限跟著整包複製過去。
day-23-multi-agent-failure-isolation