跳转至

從零打造一個專案:我的 Agent Delivery Harness

這篇屬於 AI Agents 系列,聚焦在 coding agent 外圍的交付 Harness:issue 合約、worktree 隔離、測試感測器、subagent 角色邊界,以及讓 agent 產出更值得信任的 review gate。

這套 Agent Delivery Harness 的起點,是今年初讀到的幾篇談 coding agent 工作流程的文章。裡面講的一些做法我看了蠻有感,就拿手邊的專案照著試。一試就踩了不少坑:agent 興沖沖把一個 issue 一口氣寫完,測試事後才補,有時候連測試都被它自己改成「會過」;兩個任務在同一個 checkout 上互相蓋掉對方的檔案;跑到一半才發現環境根本沒裝好。

這些問題大多跟模型聰不聰明無關。模型可以很會寫 code,但如果沒有一套規矩把它框住,它還是會在錯的工作目錄裡寫、在不清楚的需求上猜、在沒有驗證的情況下宣布完成。

後來我把這套做法陸續用在公司內部好幾個專案上,邊做邊改。這篇寫給已經在用 Claude Code、GitHub Copilot coding agent 這類工具,但開始感覺「能寫」和「能信任」之間還差一段流程的人;如果你正在找 AI coding 入門,這篇會太偏流程設計。

我用的「Harness」這個詞,脈絡上接近 LangChain 那篇 The Anatomy of an Agent Harness 裡的 Agent = Model + Harness:模型本身只是一部分,模型外面那些 prompt、tool、狀態、執行環境、檢查迴路和約束,才讓它變成可以做事的 agent。Anthropic 在 Effective harnesses for long-running agents、Martin Fowler 在 Harness engineering for coding agent users 裡,也都在處理類似問題:怎麼讓 agent 不只會動手,還能穩定、可追蹤、可修正地交付。

這篇要講的 Harness,是一組放在 repo 裡的腳本、Copilot 指令檔、subagent prompt 和流程約束。它不像 framework 會替你規定產品架構,也不像 agent runtime 負責執行模型;它管的是 coding agent 的交付節奏。

Harness 不替你產生產品功能。它規範 agent 怎麼開始一個 issue、怎麼拆任務、怎麼寫測試、怎麼驗證、怎麼審查、怎麼開 PR。每個任務開一個獨立的 git worktree,需求鎖在 GitHub issue,本地檢查腳本當感測器,實作與驗證交給不同 subagent。下面我先講一個 issue 走完長什麼樣子,再回頭看這個 repo 的結構,最後帶你從空目錄走到第一個 PR。

一個 issue 走完是什麼樣子

先看大圖。這套 Harness 的核心是一個固定的 issue 生命週期:

flowchart TD
    A["GitHub issue"] --> B["start-issue.sh<br/>預檢 + 開 worktree"]
    B --> P["Stage 1 規劃<br/>planner 出計畫 + 拋出待拍板問題"]
    P --> H{"人類輸入 gate<br/>待拍板問題都解決了?"}
    H -- 否 --> Q["人類回答決策"]
    Q --> H
    H -- 是 --> F["指揮者寫 feature_list.json<br/>一次挑一個 passes:false 功能"]
    F --> T1["tester 建立/確認 RED sensor"]
    T1 --> I["implementer 寫最小產品碼改動"]
    I --> T2["tester 驗 GREEN<br/>通過才標 passes:true"]
    T2 --> More{"還有未完成功能?"}
    More -- 有 --> F
    More -- 無 --> R["Stage 3 審查<br/>code reviewer 全新脈絡重讀 diff"]
    R --> C["Stage 4 收尾<br/>review-gate → create-pr → merge-pr → finish-issue"]

這張圖背後有四個設計決定。

GitHub issue 是需求合約。 本地草稿檔和聊天記錄都不能取代它。要做什麼、驗收條件是什麼,通通寫在 issue 上,用 gh issue view <N> --comments 抓。人類在規劃階段補充的決策,如果會改變需求或驗收條件,就應該回到 issue comment;handoff、sensor 結果、修復紀錄這類執行過程,才記在 progress.md。權威順序是 GitHub issue 與 issue comments,再來才是 feature_list.jsonfeature_list.json 只是執行清單,不能覆蓋 issue。

每個 issue 一個 worktree。 start-issue.sh 會幫你開一條 feature/issue-NN-<slug> 分支,放在 ../<repo>-worktrees/issue-NN。兩個 issue 同時進行,也不會在同一個工作目錄打架。

本地腳本就是感測器。 這裡的「感測器」不是什麼神祕東西,就是可以重複執行、用 exit code 表態的檢查:shell test、語言測試、lint、typecheck、CI smoke。agent 可以說它做完了,但感測器不過,就不算。

實作跟驗證分給不同角色。 指揮者(conductor)是主對話裡的 coding agent,負責拆 issue、選下一個功能、記錄 handoff、提交、開 PR。真正寫功能與寫測試的,是它另外叫出來的 subagent。這個分工的重點很務實:不要讓同一個 agent 一邊寫產品碼、一邊把測試改到會過,最後自己宣布成功。

角色先點名

後面 Stage 1 到 Stage 4 我都用這幾個短名字稱呼它們。這個 repo 的實作很 GitHub Copilot 取向:角色 prompt、skills、instructions 都放在 .copilot/ 底下。不過 lifecycle 本身不必綁死在 Copilot;如果你用的是 Claude Code,也可以把同樣的角色分工、feature list 和感測器搬成 Claude 的 prompt、command 或專案文件結構。每個分身的 prompt 都是一份檔案,放在 .copilot/agents/ 底下,想看它實際被交代了什麼,直接翻對應的 .agent.md 就好。

  • 指揮者(conductor): 主對話裡的 coding agent,從頭管到尾。它選 issue、拆 feature、在每個階段之間決定下一步、提交、開 PR,但在 issue workflow 啟動後,不直接寫某個功能的產品碼或測試。它沒有獨立 prompt 檔,行為寫在 .copilot/instructions/workflow-tiers.instructions.mdAGENTS.md 裡。
  • planner(規劃者): 只做研究跟規劃。讀現有程式碼、想清楚要動哪些檔案、產出計畫,還要列出「需要人類拍板的問題清單」。它不寫產品碼,也不寫 feature_list.json。prompt 在 .copilot/agents/planning-subagent.agent.md
  • tester(測試者): 一次只針對一個功能,先建立或確認能表達需求的 RED sensor,也就是目前應該失敗的測試或檢查。實作完成後,它再跑同一組 sensor 驗 GREEN。只有真的過了,它才有權把功能標成 passes:true。prompt 在 .copilot/agents/test-subagent.agent.md
  • implementer(實作者): 一次只接一個功能,做剛好讓 RED sensor 變綠的最小產品碼改動。它不准寫測試,不准自己標 passes:true。prompt 在 .copilot/agents/implementation-subagent.agent.md
  • code reviewer(審查者): 最後的總審。它帶著全新脈絡上場,只拿到目標、驗收條件、改了哪些檔案,然後自己重讀 workspace,獨立判斷規格、測試、品質和角色界線。prompt 在 .copilot/agents/code-review-subagent.agent.md

這裡最容易誤會的是 Stage 2。它不是「implementer 先寫完,tester 事後補測試」。真正的 loop 是:指揮者挑一個 passes:false 功能,tester 先建立或確認 RED sensor,implementer 再做最小產品碼改動,最後 tester 驗 GREEN。這樣才跟前面踩過的坑切開。

Stage 0:start-issue.sh 鋪好場子

整套 Harness 是 issue 驅動的。你先在 GitHub 上開一個帶明確驗收條件的 issue,然後從主 checkout 啟動它:

./scripts/start-issue.sh 1
cd ../<repo>-worktrees/issue-01

start-issue.sh 會先跑預檢,環境綠了才開分支跟 worktree。接著它會在 .copilot-tracking/issues/issue-NN/ 底下鋪好一份空的 feature_list.json 模板跟 progress.md。注意這時候 issue 還沒被拆。start-issue.sh 只負責開好場子,真正的拆解要等下一階段規劃完、問題都問清楚之後,才由指揮者填進去。

Stage 1:規劃與人類輸入 gate

第一個上場的是 planner。它只做研究跟規劃,不寫任何程式:讀現有的程式碼、看有沒有可以沿用的既有做法、把要動的檔案跟步驟想清楚,最後產出一份計畫文件放在 .copilot-tracking/plans/ 底下。

除了計畫,planner 還要明確列出「需要人類拍板的問題清單」。哪些決策它不該自己擅自決定,就得先問過人。計畫跟問題清單出來後,指揮者會停下來,把那些問題轉給我,停在一道「人類輸入 gate」。問題沒清空之前,誰都不准開始拆 feature list。

等我把問題都回答、確認方向之後,才由指揮者照這份已確認的計畫,把 issue 拆進 feature_list.json。這份清單是接下來整個 loop 的執行索引,但需求的最終依據始終是 GitHub issue 本身。

feature_list.json 大概長這樣。剛拆好的時候,每個功能都是 passes: false

{
  "issue": 1,
  "features": [
    {
      "id": "F1",
      "title": "init.sh 偵測到 go.mod 時跑 gofmt 檢查",
      "steps": [
        "在 go.profile.sh 的品質閘門加上 gofmt -l",
        "有未格式化的檔案時讓 init.sh 非零退出"
      ],
      "regression_sensor": "tests/scripts/test_go_profile.sh",
      "e2e_sensor": "",
      "passes": false,
      "verification": ""
    }
  ]
}

而且 passes:true 不能只是嘴上說過。收尾用的 check-feature-list.sh 會檢查 JSON 結構,也會要求每個 passes:true 的功能都有非空的 verification。這是很小的規矩,但可以擋掉「沒真的驗過就先標過關」這種偷吃步。

Stage 2:實作與驗證 loop

Stage 2 是整套流程裡最重要的一段。它一次只處理一個 passes:false 功能,跑完一輪再挑下一個,直到全部變綠。

每一輪是這樣走:

  1. 指揮者選一個功能。 它準備好 issue 背景、feature steps、declared sensor,然後把驗證工作交給 tester。
  2. tester 建立或確認 RED sensor。 測試者先寫或更新測試,並確認它在目前狀態下會失敗。這一步是在確認「我們真的有抓到問題」,不是事後補文件。
  3. implementer 寫最小產品碼改動。 實作者只動產品碼,目標是讓剛才那個 sensor 變綠。它不碰測試,也不宣布完成。
  4. tester 驗 GREEN。 測試者重新跑 declared sensor。只有感測器真的通過,它才把 passes 翻成 true,並把驗證方式寫進 verification
  5. 指揮者記錄 handoff。 誰做了什麼、跑了什麼、結果如何,都記到 issue 的進度紀錄裡。

兩個 subagent 分開,是刻意的。寫實作的那個不能順手把測試改軟;寫測試的那個也不能偷寫產品碼。這不是說 prompt 就能提供完美的沙盒隔離,但它把責任界線攤在檯面上,後面的 code review 和進度紀錄也會專門檢查這件事。

Stage 3:整體審查

所有功能都綠之後,還不能直接開 PR。code reviewer 會帶著全新的脈絡上場。它沒看過前面的規劃討論,也沒看過實作過程,只拿到「這個 issue 的目標、驗收條件、改了哪些檔案」。其餘全靠自己重讀 workspace。

這樣做是要讓它的判斷獨立於產生這份 diff 的那串對話。它一次看四件事:有沒有符合規格、測試與感測器夠不夠、程式品質、以及有沒有踩到角色分工的界線。嚴重問題得先修掉、重審,才往收尾走。

審查相關的 skill 也集中在這裡,而不是讓每個分身都去碰品質判斷。像 find-brute-force 抓 hack、吞掉的錯誤、hardcode;find-duplicates 抓複製貼上的重複邏輯;find-over-design 抓過度抽象;dead-code-detection 抓這次動到的死碼風險;sync-docs 抓文件漂移;public-exposure-audit 則在 public repo 場景掃祕密、個資、雲端 ID、客戶素材。這些 skill 都針對這次 diff 引入的問題,不是順手做全庫大掃除。

Stage 4:收尾

收尾這段我設計得比較囉嗦,因為這裡最容易出包。

先用 ./scripts/review-gate.sh approve 把目前的 HEAD 記下來,當成審查通過的標記。create-pr.sh 開 PR 之前會檢查一次;如果你又 commit 或 rebase 改動了 HEAD,就得重新批准。這避免「審過的是 A,送出去的是 B」。

開 PR 這一步背後配一支 create-pr skill,負責把標題和內文寫成樣、連回 issue、把驗收條件帶進描述,而不是丟一個空 PR 出去。PR 開出去之後,merge-pr.sh 會先確認 Harness CI 的 smoke 流程是綠的才執行 merge。合併完回到主 checkout 跑 finish-issue.sh,它會清掉 worktree,再驗一遍功能清單有沒有做完。

不過這裡我會建議採用漸進式信任。剛把 Harness 接進一個 repo 時,讓 agent 開 PR 就先停住,交給人類 review;等幾輪之後,你對它的 feature list、sensor、review gate 都有信心了,再允許它在所有 gate 綠燈、PR checks 綠燈時自己跑 merge-pr.sh。腳本支援 CI 綠後 merge,但團隊不一定要第一天就把自主權開到最大。

這裡要講清楚:那個 CI smoke 流程不是 CI/CD 交付,不是部署管線,也不是自動合併。它只跑 Harness 自己的 shell 感測器、檢查腳本能不能 parse、跑 shellcheck、驗一下 Copilot 設定檔的 frontmatter。它是一道「Harness 本身有沒有壞掉」的健康檢查,而且是合併前必須綠的硬條件。

還有一支看情況才出場的 security-audit skill。只要 issue 碰到 auth、Azure 佈建或資料搬移,收尾前就要用它把憑證處理、祕密外洩、權限、資料分級掃一遍。它屬於 inferential review sensor,不是 deterministic shell script;流程硬度來自後續處理:Critical / High 這類 findings 必須修掉並重跑相關檢查,不能只是列在報告裡。

這個 repo 的三層結構

看完 lifecycle,再回頭看 repo 結構就比較容易懂。我刻意把穩定的東西跟會換的東西分開,免得加一個語言就要動到核心流程。

第一層是 Core Harness。它管的是跟語言無關的生命週期:環境預檢、開 worktree、記錄每個 issue 的進度、審查閘門、收尾開 PR。這一層的行為被寫死在一份 docs/harness-contract.yml 合約裡,還有一支 tests/scripts/test_harness_contract.sh 在 CI 盯著。改了核心腳本卻沒同步改合約,測試就紅給你看。

第二層是語言設定檔(Language Profiles)。每個語言寫成一份 profiles/<id>.profile.sh,告訴 init.sh 怎麼偵測這個語言的專案、跑哪些檢查。目前內建 Python、Go、Node.js、Java、Ruby 五個。核心完全不認得任何一個語言的細節,它只負責把對應的 profile 載進來。

第三層在 repo 文件裡叫框架模板(Framework Templates),但我更直覺地把它理解成專案慣例(Project Conventions)。像 Python 底下的 FastAPI、Java 底下的 Spring Boot 這種專案級約定,應該放在採用這套 Harness 的專案自己的文件裡,不進核心。profile 可以給框架提示,但不強迫你用哪一個。

從零開始:把專案接上 Harness

假設你現在手上什麼都沒有,想用這套 Harness 開一個新專案,最短路徑是這樣。

第一步:拿到 Harness

最簡單的方式是直接把這個 repo 當成專案的根,clone 下來,之後你的程式碼就往 apps/packages/infra/ 放。scripts/profiles/ 維持原位就好。

如果你已經有一個專案,這件事比較像移植,不是安裝。你可以只搬需要的 Harness 內容:scripts/profiles/tests/.copilot/、那支 smoke workflow,以及 docs/ 裡的生命週期文件;但既有 repo 通常已經有自己的 CI、scripts、docs 和分支規則,搬過去後一定要調整路徑、gate 和團隊慣例。不要期待原封不動 copy 進去就能跑。

第二步:確認前置條件

硬性需求其實不多:macOS 或 Linux、登入好的 GitHub CLI,以及可以正常跑的 Git。Git 簽章不是 open source 專案都必備;目前 init.sh 對「沒有開 commit signing」也是警告,不是硬擋。不過如果你的公司或 repo policy 要求 verified commits,這件事就要在讓 agent commit / push / merge 前先設好。

gh auth login          # 必要,沒登入後面全部擋下來

Azure CLI 只有等你開始碰 Terraform 或雲端部署才需要,那時候改用 REQUIRE_AZ=1 ./scripts/init.sh。語言工具鏈,像 Python 的 uv,也是等那個語言的程式碼出現才需要;在那之前,預檢只會警告一下,不會擋你。

第三步:選一個語言

挑你這個專案第一個要寫的語言。每個語言用一個標記檔被偵測,跑各自的檢查:

語言 偵測檔 檢查
Python pyproject.toml ruff 格式、ruff lint、mypy、pytest
Go go.mod gofmt、go vet、選用 golangci-lint、go test
Node.js package.json prettier、eslint、選用 tsc、測試腳本
Java pom.xml / build.gradle 選用 Spotless、Checkstyle 系列、測試
Ruby Gemfile standardrb 或 RuboCop、RSpec 或 Minitest

一個專案可以同時有好幾種語言,init.sh 會各自偵測、各自跑。.tf 檔還會多跑 terraform fmtvalidate。如果你現在還只有文件,像這個 repo 今天的狀態,那唯一要過的檢查就是對腳本跑 shellcheck

第四步:跑預檢

./scripts/init.sh

它會檢查 GitHub 登入、Azure 登入狀態、commit signing 設定,然後對偵測到的語言面跑同步跟檢查。硬性檢查沒過會帶著修復指示退出;軟性檢查,例如語言工具還沒裝、還沒有任何檢查項目,只會警告,讓你在還沒寫程式的階段照樣往前走。

預檢大概分成六關:必要工具、GitHub 登入、Azure 登入、commit signing、專案語言偵測、品質閘門。前面的硬性檢查沒過,後面的品質閘門根本不會跑。

第五步:開你的第一個 issue

先在 GitHub 上開一個帶明確驗收條件的 issue,然後從主 checkout 啟動它:

./scripts/start-issue.sh 1
cd ../<repo>-worktrees/issue-01

到這裡,場地就搭好了。接下來就是照前面的 Stage 1 到 Stage 4 走:先規劃,問清楚問題,拆 feature_list.json,再一個功能一個功能跑 RED/GREEN loop。

下一步:把 Harness 自己也納入評估

到這裡為止,守住品質的是腳本感測器、角色分工和 review gate。但這套 Harness 本身就是一個 agentic system,光靠一般單元測試不夠。skill 觸發對不對、subagent 有沒有守住界線、handoff 順序有沒有跑歪,都應該變成可評估的項目。

所以我下一步會把 evaluation 補起來:底層用 deterministic 的腳本生命週期測試守住 L0/L1;需要模型判斷的地方,再用 LLM-as-judge 或 mutation eval 去測角色邊界、prompt 注入、祕密外洩、簽章保留這些問題。完整的設計先放在 docs/evaluation/,第一個可執行切片從 docs/evaluation/l0-l1-solution/ 開始。這一段目前還是規劃,不是已經全部跑起來的東西。

為什麼值得這樣搞

老實說,第一次跑這套流程會覺得步驟有點多。我自己的體感是:它把我跟 agent 之間的模糊地帶都收掉了。需求模糊?issue 上寫清楚。環境沒裝好?預檢就擋下來。測試被偷工?角色分工讓它不容易混在一起。語言要擴充?加一份 profile,核心動都不用動。每一道閘門都對應到我以前真的被咬過的一個傷口。

往後退一步看,這幾年最大的變化是 coding agent 真的能自己扛事了。尤其去年底開始,Claude Code 這類工具越來越成熟,很多以前得自己一行一行寫的活,現在交給 agent 就跑掉了,開發速度也跟著快上一截。但能寫,不等於能放手。怎麼在不盯著它的前提下還信得過它的產出,怎麼把人類的介入再往下壓一點,才是真正值得鑽研的題目。

這篇講的東西,就是我把別人的經驗讀進去、自己動手做出來的結果。我已經拿它替公司內部好幾個新 project 做 kickoff。身為一個 FDE,我常常要在不同的新專案之間切換,每換一個就要重新搭一次規矩。這種時候 Harness engineering 對我幫助特別大:把那套規矩固化成一份可以搬著走的 Harness,換專案的成本就低很多。

如果你也常用 AI agent 寫程式,卻覺得它「很會寫但很難信任」,可以先拿這套 Harness 的某幾個片段試:把需求逼進 issue,用 worktree 隔離任務,讓測試跟實作分角色跑。光這幾件事,就能省掉不少回頭收拾的時間。完整 repo 在 agent-delivery-harness,裡面的 docs/getting-started.mddocs/HARNESS.md 都寫得比這篇細。