Claude Code 上的 Agent Orchestration — AI 自動產生高品質 .NET 測試(2)

為什麼 Orchestrator 是 Skill 而不是 Agent — 架構解析

Orchestrator 為什麼必須是 Skill,而不是 Agent?本文從 Claude Code 的平台限制談起,拆解 bypassPermissions、計時 Hook、Writer 分割與 JSON 交接這幾個關鍵設計,看這套 1+4 架構如何在 Claude Code 上順暢運作,以及四組 Orchestrator 的異同。

前言

上一篇看了整套方案的全貌 — 三層架構(4 個 Orchestrator Skill、16 個 Subagent、29 個 Agent Skills 與 dotnet-test 執行器)、四個 Orchestrator,以及那張「下一句指令、Orchestrator 接手委派四個 Subagent」的架構圖。

但全貌只回答了「長什麼樣」,還沒回答「為什麼這樣設計」。這一篇就深入設計細節 — 從最根本的一個決策講起:Orchestrator 為什麼必須是 Skill,而不是 Agent,再往下談 bypassPermissions、計時 Hook、Writer 分割、JSON 交接這些關鍵設計。


設計理念:為何 Orchestrator 必須是 Skill

這是整個架構的起點,也是最容易讓人困惑的地方。

Claude Code 的 Agent tool(用來委派 Subagent 的工具)有一個關鍵限制:它只能在主對話(main thread)中呼叫

假設我們很直覺地把 Orchestrator 定義成一個 Agent,會發生什麼事?當使用者啟動這個 Orchestrator Agent 時,它會在一個子對話中執行。而子對話裡無法再呼叫 Agent tool — 也就是說,這個 Orchestrator Agent 沒辦法再委派 Analyzer、Writer、Executor、Reviewer。整個 1 + 4 架構會在第一層就斷掉。

解法是把 Orchestrator 定義為 Skill

  • Orchestrator Skill 透過斜線指令(如 /dotnet-testing-orchestrator-unit)載入到主對話的 context
  • 主對話載入 Skill 後,主對話本身就扮演 Orchestrator 的角色,用自己的 Agent tool 依序委派四個 Subagent
  • 每個 Subagent 定義在 .claude/agents/*.md,由 Agent tool 自動載入並在各自的子對話中執行

換句話說,「Orchestrator 是 Skill、Subagent 是 Agent」不是隨意的選擇,而是 Claude Code 機制下唯一能讓 1 + 4 架構成立的設計。Skill 負責主對話的協調,Agent 負責子對話的執行 — 這個分層對應 Claude Code 的機制非常自然。也正是上一篇那張架構圖最上層「Orchestrator Skill 在主對話 context」的由來。


標準工作流程

知道了 Orchestrator 是 Skill、Subagent 是 Agent 之後,來看主對話載入 Skill 後,實際怎麼把四個 Subagent 串起來。每個 Orchestrator 的工作流程都遵循同樣的階段結構:

從 Phase 0 的前置清理開始,依序經過 Analyzer、Writer、Executor、Reviewer 四個核心階段,最後以 Phase 5 的後置清理收尾。流程裡有幾個分支值得注意 — Phase 2 前有個「要不要分割 Writer」的判斷、Phase 3 有最多三輪的修正迴圈、Phase 4 後有個「使用者是否套用修正建議」的決策點。這些分支背後的設計考量,就是接下來幾個關鍵設計決策要談的。


關鍵設計決策

除了「Orchestrator 是 Skill」這個根本決策外,還有幾個值得一提的設計。

bypassPermissions:讓 Executor 自主執行

每個 Subagent 的定義檔中都設定了 bypassPermissions: true

這個設定最關鍵的受益者是 Executor。它在執行 dotnet builddotnet test 時,如果沒有這個設定,Claude Code 會在每次 Bash 工具呼叫前彈出手動確認提示。對一個可能要跑「建置 → 測試 → 修正 → 再建置」多輪迴圈的 Executor 來說,每一步都要使用者按確認,工作流程根本無法順暢推進。設定 bypassPermissions: true 後,Subagent 在其工作範圍內可自主執行這些指令,讓整個工作流程的自動化程度更高。

計時 Hook:與 Orchestrator 解耦的可觀測性

想知道一次工作流程的時間花在哪,最直覺的做法是讓 Orchestrator 自己記時間。但這套方案改用 Hook 來做。

.claude/hooks/ 下有兩個腳本,分別掛在 PreToolUse 與 PostToolUse 兩個時機,攔截所有 subagent_typedotnet-testing- 開頭的 Agent tool 呼叫:

  • PreToolUse:在委派 Subagent 前記錄開始時間,注入「⏱ {subagent} 開始:HH:MM:SS」
  • PostToolUse:在 Subagent 完成後計算耗時,注入「⏱ {subagent} 完成(耗時 M 分 S 秒)」

這個做法的好處是跟 Orchestrator 完全解耦。Orchestrator 不需要手動呼叫 Bash(date) 來計時,時間資訊會自動出現在 Agent tool 的回傳結果中。而且 Hook 是可選的,沒安裝的話工作流程照樣正常執行,只是少了耗時顯示。計時這件事不會佔用 Subagent 的 context,也不會讓 Orchestrator 的邏輯變複雜。

Writer 分割策略:避免單一 Subagent 過載

這就是前面流程圖裡 Phase 2 那個分支的由來。當被測類別很大時(方法數 > 5 或情境數 > 20),單一 Writer 試圖在一次回應中產出所有測試,會超出 LLM 的輸出 token 上限而被截斷。

處理方式是:觸發分割,同時啟動兩個平行 Writer。分割用的是貪心演算法 — 把方法按情境數量降序排列,依序分配到目前情境總數較少的那一組,目標是讓兩組的情境數量盡量接近。同一個方法的所有測試案例絕不跨組拆分。

有一個例外:Validator 類別(繼承 AbstractValidator<T>)永遠不分割(forbidWriterSplit: true),因為驗證器的測試案例彼此關聯緊密,拆開反而會破壞一致性。

分割時還有一個容易被忽略的細節 — 多個 Writer 的風格必須統一using 的排列順序、AutoFixture 的初始化方式、FakeTimeProvider 的欄位命名與初始時間,所有 Writer 都必須完全一致,否則合併出來的測試檔案會風格混亂。

JSON 交接機制

四個 Subagent 各自在獨立的子對話中執行,彼此看不到對方的 context。那 Writer 怎麼知道 Analyzer 分析出什麼?答案是透過 .orchestrator/ 下的 JSON 檔案傳遞結構化資料,而非在 prompt 中嵌入完整內容:

{testProjectDir}/.orchestrator/
├── analysis/
│   └── {ClassName}.analysis.json        # Analyzer 寫入
├── {ClassName}.writer-result.json        # Writer 寫入
└── executor-result/
    └── {ClassName}.executor-result.json  # Executor 寫入

Orchestrator 在委派各 Subagent 時,只傳入交接檔案的路徑與必要的摘要數字,不嵌入完整 JSON。每個 Subagent 的 Step 0 會自行讀取上游的交接檔案取得完整資訊。這讓每個 Subagent 的 prompt 保持精簡。

清理策略上,四個工作流程一致:流程結束的 Phase 5 會把整個 .orchestrator/ 交接目錄清掉,不保留任何中介檔案。不過 AI agent 的執行帶有隨機性,不保證每次都清得乾淨,所以每個流程開頭的 Phase 0 都會先把測試專案裡殘留的 .orchestrator/ 清一次,確保這次從乾淨狀態開始。


四組 Orchestrator 對比

四組 Orchestrator 共用同樣的架構骨架,差異在於各自的 Subagent 系列與領域特化。

面向UnitIntegrationAspireTUnit
觸發指令/dotnet-testing-orchestrator-unit...-integration...-aspire...-tunit
Subagent 系列dotnet-testing-*dotnet-testing-advanced-integration-*dotnet-testing-advanced-aspire-*dotnet-testing-advanced-tunit-*
分析入口被測類別WebAPI Controller 端點AppHost Program.cs被測類別 + 框架偵測
測試基礎建設簡單 Test ClassWebApiFactory + TestBase + CollectionAspireAppFixture + TestBaseOutputType=Exe 專案
Docker 需求不需要需要需要不需要
Executor 特化標準建置測試Docker 檢查 + Production Code 修正授權Docker + Aspire workload 檢查、超長 timeoutdotnet run 執行

幾個值得注意的特化點:

  • Integration 的 Executor 有「Production Code Bug 修正授權」:整合測試是端對端驗證,測試失敗的根因可能在 Production Code。Executor 被授權修正 Controller 路由設定、驗證邏輯錯誤、HTTP 回應格式等問題(但不得修改業務邏輯、新增功能或改資料庫 Schema),且修正後必須明確標記。
  • Aspire 的 Executor 要做雙重環境檢查:除了 Docker,還要確認 .NET Aspire workload 已安裝,而且因為啟動整個分散式環境耗時較長,timeout 設定也比其他組長。
  • TUnit 用 dotnet run 而非 dotnet test:TUnit 基於 Source Generator,測試專案的 OutputType 必須是 Exe,執行方式與其他三組根本不同。

但無論差異多大,1 + 4 的架構、四階段流程、JSON 交接、Writer 分割策略這些骨架,四組是完全一致的。


安裝架構說明

這套方案的安裝以一個 Python 腳本為核心。它負責把整套架構部署到目標專案:

  • 複製 .claude/skills/ 下的 4 個 Orchestrator Skill,以及 Executor 依賴的 dotnet-test 測試執行器 Skill
  • 複製 .claude/agents/ 下的 16 個 Subagent 定義檔
  • (可選)安裝 .claude/hooks/ 的計時 Hook

外部的 29 個 Agent Skills 則由 dotnet-testing-agent-skills 另行提供,安裝到 .claude/skills/ 供 Subagent 載入。

詳細的安裝步驟、一鍵安裝與手動安裝的差異、各種 Orchestrator 的觸發與使用,會在下一篇使用指南完整說明。


小結

這套架構的核心,是一個由平台限制推導出來的決策:因為 Agent tool 只能在主對話呼叫,所以 Orchestrator 必須是 Skill 而非 Agent。其餘幾個設計都圍著這個起點:bypassPermissions 讓 Executor 自主執行、Hook 提供解耦的計時、Writer 分割避免單一 Subagent 過載、JSON 交接讓每個 Subagent 的 prompt 保持精簡。

四組 Orchestrator 之間骨架完全一致,差異只在領域特化 — 架構是共用的模板,真正不同的是填進去的領域知識。

下一篇,我們會從架構走向實作 — 完整的安裝與使用指南。


參考資源

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力