
前情提要
song-lingo 是我自己做來學日文歌的小工具:貼上一支 YouTube MV,Gemini 會幫我聽寫歌詞、標註羅馬拼音、繁中翻譯與文法,再用 Gemini TTS 產生「老師示範音」;我錄音跟讀之後,Gemini 會轉錄我的發音並逐字評分。
前幾天,我才在同一個 Claude Code session 裡把整個後端從 Python + Next.js API routes 全部改寫成 Go 1.26(Issues #44–#49、Tracking Issue #50、PR #51–#56)。改寫完之後的架構長這樣:
- Next.js 只剩前端,用 rewrite 把
/api轉給同一個容器裡的 Go server(127.0.0.1:8081)。 - 部署在 Cloud Run(
asia-east1),資料放在 Cloud Storage bucket,透過 gcsfuse 掛在/data;學習進度存在 Firestore;前面用 IAP 限制只有我能登入。 - Go 版的幾個重點:
genaiSDK、Gemini REST(voices / interactions)、Firestore transaction、用kagome斷詞轉平假名、自己寫的 Hepburn 羅馬拼音表,以及把 Pythondifflib.SequenceMatcher移植成 Go(用 400 組 Python 產出的 golden 測試對齊評分)。
改寫完、能動、測試也過了。但「自己寫、自己審」總是有盲點。剛好這次想試試 Herdr——一個讓多個 AI Agent 在同一個視窗裡、各自佔一個 Pane 協作的工具。於是我做了一個實驗:
讓 Claude Code 在左邊當主控與 Reviewer,讓 Antigravity CLI(
agy)在右邊當實作者,兩個 Agent 透過 Herdr 互相傳訊,把 song-lingo Go 後端從 Code Review 一路做到 Cloud Run 上線。
一、Herdr 是什麼?第一次用的上手筆記
1. 兩個執行檔:herdr-app 與 herdr-core
Herdr 分成兩層:
| 元件 | 角色 | 說明 |
|---|---|---|
herdr-app |
桌面 App | 管理 spaces > tabs > panes 的畫面配置,可以把 pane 往左右切、直接在新 pane 裡跑程式 |
herdr-core(CLI 叫 hc) |
終端機核心 | 負責跑「不會因為視窗關掉而死掉的 terminal」,提供 run / send / wait / read / keys / get 等指令讓 Agent 操作其他 terminal |
一個小插曲:我一開始叫 Claude Code「先使用 herdr --skill 了解」,結果 herdr 這個指令根本找不到——實際的執行檔是 ~/.local/bin/herdr-app 與 ~/.local/bin/herdr-core。
2. --skill:寫給 AI Agent 看的操作手冊
我覺得 Herdr 最聰明的設計是:兩個 CLI 都有 --skill 參數,執行 herdr-app --skill 與 herdr-core --skill 會印出一份「給 AI Agent 看的操作手冊」。
所以 Claude Code 的第一步不是亂試指令,而是先把兩份手冊讀完再動手。手冊裡寫了很多很有分寸的規範,例如:背景 Agent 一定要 --bg、要給名字與明確範圍、不要讓子 Agent 再開子 Agent、等待時用 hc wait 而不要 sleep 輪詢。
3. 核心指令速查表
| 指令 | 用途 |
|---|---|
$HC_TERMINAL_ID |
每個 terminal 都有位址,Claude Code 自己是 t4 |
hc run --bg --name xxx <agent> "<task>" |
在背景開一個有名字的 Agent 並交代任務 |
herdr-app do pane.split --direction right --pane <id> --argv [...] |
在畫面上把 pane 往右切,並直接在新 pane 裡跑程式 |
hc send <t> "..." |
貼上文字並按 Enter,用來派任務、給 Review 意見 |
hc wait <t> / hc wait --children all |
等對方做完(取代 sleep 輪詢) |
hc read <t> --lines 0 |
讀對方目前的整個螢幕 |
hc keys <t> enter |
送出按鍵,例如回答權限對話框 |
hc get <t> |
查狀態:idle / working / blocked / done;blocked 時還會附上 msg,例如 Approve RunCommand: ... |
4. Agent 對 Agent 的訊息通道
這是整個協作最關鍵的一塊。我在交代任務時會要求 agy:「做完請執行 herdr-core send t4 '...' 回報」。agy 做完之後,那段訊息會直接出現在 Claude Code 的輸入裡,長得像這樣:
<hc from="t7">ready for review #57 ...</hc>
換句話說,Claude Code 不需要一直盯著右邊的螢幕,agy 做完自己會「敲門」。回報是非同步的、而且每一則都帶著來源位址,可以追蹤。
5. 畫面配置
我第一次讓 Claude Code 開 agy 時,它是用 hc run --bg 在背景跑的——功能上沒問題,但我看不到 agy 在做什麼。所以第二次我特別要求「in a pane on the right」,Claude Code 改用 herdr-app do pane.split 把 agy 開在右邊。最後的配置是:
flowchart LR
subgraph Herdr["Herdr 視窗(同一個 tab)"]
direction LR
CC["左 Pane:Claude Code (t4)<br/>主控 / Reviewer<br/>開 Issue、看 diff、跑 go test -race、批准"]
AGY["中 Pane:agy (t7)<br/>實作者<br/>Code Review、改程式、跑測試、commit/push"]
BLOG["右 Pane:Claude Code<br/>Blog Writer<br/>(寫這篇文章的那一位)"]
end
Human(["我(人類)<br/>看得到每個 Pane、隨時可插手"])
CC -->|"hc send:派任務 / Review 意見"| AGY
AGY -->|"herdr-core send t4:ready for review #N"| CC
CC -->|"hc keys:放行唯讀權限"| AGY
CC -->|"hc send:寫部落格"| BLOG
BLOG -->|"herdr-core send t4:blog draft ready"| CC
Human -.-> CC
Human -.-> AGY
二、第一回合:讓 agy 當 Reviewer
第一個任務很單純:請 agy 審 backend/ 底下的 Go 程式碼,唯讀、不開 subagent,把結果寫到檔案,完成後 hc send t4 回報。
agy 的介面顯示它跑的是 Claude Sonnet 5.5(plan 模式),最後產出了 17 項問題,依重要性排序。Claude Code 幫我整理成三大類:
| 類別 | agy 找到的問題 |
|---|---|
| 併發與資料正確性 | Re-annotate 期間(一次 Gemini 呼叫最長 5 分鐘)若我在校對頁修改歌詞,寫回時會被蓋掉——兩邊用的是不同的 mutex;EditLine 的全域鎖跨過了 Firestore 呼叫;WriteAtomic 的暫存檔名固定是 .tmp |
| 資源上限與洩漏 | TTS 產生沒有並行上限(這可是付費 API);失敗快取只增不減而且重複存兩份;429 沒帶 Retry-After 時完全不限流;TeacherVoice 每次重讀 voices.json 且在網路呼叫期間持有全域鎖;request body 沒有 MaxBytesReader;背景 job 用 context.Background(),關機時不會被取消 |
| 效能與結構 | listSongs 依序解析所有歌、逐首讀 progress;示範音整個讀進記憶體再回傳;同樣的 Error 型別有 3–4 份;CLI 的 speak 和 tts.Service 邏輯重複 |
這裡有一個我很喜歡的細節:Claude Code 沒有照單全收。在開 Issue 之前,它先自己 grep 驗證最重要的那一條——「re-annotate 與 review 用的是不同 mutex」——確認屬實,才開始動手開 Issue。Reviewer 的結論也需要被 Review。
三、第二回合:開 Issue → agy 修 → Claude Code 審 → 通過才 commit
我給 Claude Code 的指示是:
「開 issue 然後依序修改,主要讓右方 agy pane 去修改,你負責 review 給意見,你通過才讓 agy 去 commit and push。」
1. 17 項整併成 5 個 Issue
Claude Code 把 17 項整併成 5 個 Issue(#57–#61),全部在同一個 branch go-review-fixes 上進行,一個 Issue 一個 commit,commit 訊息帶 Fixes #N。
2. 每一輪的協作流程
sequenceDiagram
participant CC as Claude Code (t4)<br/>Reviewer
participant AGY as agy (t7)<br/>實作者
participant GH as GitHub
CC->>AGY: hc send:Issue #N 任務 + 實作要點 +「不要 commit」
AGY->>AGY: 實作 + gofmt -l . && go vet ./... && go test ./...
AGY->>CC: herdr-core send t4 'ready for review #N'
CC->>CC: git diff + 自己再跑 go test -race
alt 有問題
CC->>AGY: hc send:Review 意見
AGY->>CC: 修正後再次回報
end
CC->>AGY: 批准,可以 commit & push
AGY->>GH: git commit(Fixes #N)+ push
AGY->>CC: 回報 commit sha
3. Review 實際抓到了什麼?
這是整篇最想分享的部分。5 個 Issue 裡,3 個需要第二輪修正、2 個一次通過:
| Issue | 主題 | Review 結果 | Commit |
|---|---|---|---|
| #57 | Per-song lock | 設計正確,但沒寫測試 → 要求補 | e5df5f4 |
| #58 | TTS 並行上限與失敗快取 | 抓到錯誤訊息 regression → 要求修正並補測試 | d67ec5a |
| #59 | Request 上限、timeout、優雅關機 | 抓到關機順序與時間預算錯誤 → 重新設計 | de38741 |
| #60 | Song list 並行、示範音串流 | ✅ 一次通過 | bc1615a |
| #61 | 統一錯誤型別、CLI 重構、signal 處理 | ✅ 一次通過 | deb1fb7 |
#57:設計對了,但測試在哪?
agy 的 per-song lock 設計是對的。Claude Code 還特別確認了一件事:手動翻譯與 reviewed 旗標也都會寫進 transcript,所以「寫回前比對整份 transcript」確實能抓到所有編輯。
但 agy 沒寫測試。於是 Claude Code 要求把邏輯抽成 saveAnnotation,並補上兩個案例:未被變更時正常寫入、被編輯時回傳衝突且不寫入。
#58:刪掉一個 map,前端就不知道為什麼失敗
agy 在整理失敗快取時,把 server 端的 recentErrors 刪掉了。測試全過,但 Claude Code 抓到一個 regression:
當 TTS 額度用完時,「其他從來沒產生過的句子」 去查
?error,會拿到{error: null}——前端就沒辦法顯示「額度用完」的原因。
修正方式是讓 LastError 在沒有該句失敗紀錄時,回傳仍然有效的 quota 錯誤,並補上測試。
#59:Cloud Run 只給你約 10 秒,你卻排了 20 秒
這是我覺得最有價值的一個抓蟲。agy 寫的優雅關機是:srv.Shutdown 等 10 秒,再等背景 jobs 10 秒。問題有兩個:
- Cloud Run 送出 SIGTERM 後大約只有 10 秒,兩段加起來根本超過預算。
- 重新分析是同步跑在 HTTP request 裡的。
srv.Shutdown會等它結束,但它的 context 還沒被取消——等於自己等自己。
Claude Code 要求改成單一 9 秒期限,並調整順序:
// 單一期限,留一點餘裕給 Cloud Run 的 SIGTERM 寬限期
ctx, cancel := context.WithTimeout(context.Background(), 9*time.Second)
defer cancel()
runner.Cancel() // 1. 先取消背景與進行中的分析工作
srv.Shutdown(ctx) // 2. 再關 HTTP server(同步跑的分析已經被取消,不會卡住)
runner.Wait(ctx) // 3. 最後在同一個期限內等 jobs 收尾
(上面是示意,表達順序與單一期限的概念。)
#60、#61:一次通過
- #60:Song list 改成 bounded concurrency 8,單首 progress 讀取失敗不會拖垮整個清單;已產生的示範音改用
*os.File+ModTime交給http.ServeContent串流(順便拿到 Range 與 Last-Modified)。 - #61:統一錯誤型別為
apierr.Error、CLIspeak改用tts.Service.GenerateAll、改用signal.NotifyContext、Daily quota 判斷改用型別欄位而不是字串比對。
回頭看這三個被退件的 Issue,有一個共通點:它們全部都是「測試會過、但上線才會痛」的問題——測試缺口、前端錯誤訊息 regression、關機順序。這正是「一個寫、一個審」最大的價值。
四、第三回合:PR、合併、部署與驗證
5 個 commit 都通過後,開了 PR #62 合併,#57–#61 自動關閉。接著用 gcloud run deploy --source . 部署新 revision。
部署後的驗證是 Claude Code 透過已登入 IAP 的 Chrome 直接跑 fetch 完成的:
| 驗證項目 | 結果 | 對應改動 |
|---|---|---|
首頁 /api/songs |
200(10 首) |
#60 並行讀取 |
| 學習頁 | 200 |
— |
| 已產生的示範音 | 200 audio/wav |
#60 串流 |
| 示範音 Range 請求 | 206 |
#60 http.ServeContent |
帶 If-Modified-Since |
304(新功能:現在有 Last-Modified) |
#60 ModTime |
| PATCH 塞 70 KB | 413「請求內容太大。」 |
#59 MaxBytesReader |
| Cloud Logging | 只有兩筆 WARNING,都是測試請求本身 | — |
上一版 revision 也保留著,隨時可以切回。
五、第一次用 Herdr 踩到的 6 個魔鬼細節
細節 1:agy "prompt" 會直接 exit 2
直覺上會這樣開 agy:
agy "請審 backend/ 的 Go 程式碼..."
結果直接報錯退出(exit 2):
Prompts are read only from -p/--print, -i/--prompt-interactive, or stdin
正確做法是 agy -i "..."——互動式啟動並帶入初始任務。Herdr 的手冊也提醒不要用 -p 一次性模式,因為 Agent 要保持「可觀察、可再追問」,才能接著做 Review 來回。
細節 2:第一次進資料夾的 Trust 對話框
第一次在某個資料夾啟動 agy,會跳出「Do you trust the contents of this project?」。這時 hc get 的狀態會變成 blocked(auth)。由 Claude Code 用 hc keys go-review enter 確認即可。
細節 3:看起來沒送出的 prompt,其實已經送到了
用 pane.split 開出 agy 後,畫面看起來是 idle,初始 prompt 好像沒送出去。Claude Code 想用 hc send 補送一次,結果被 hc 擋下來:
t7 is blocked (permission)... typing would answer its dialog; read its screen first
原來任務早就送到了,agy 正卡在等權限。hc 這個保護機制,避免了把一段任務文字誤打進權限對話框——如果真的打進去,後果可能是莫名其妙地同意或拒絕了某個指令。
細節 4:Plan mode 只能看、不能改
agy 在 plan mode(唯讀研究)時只能分析。要它開始改程式,必須按 shift+tab 切換模式;Claude Code 用 hc keys t7 shift+tab 幫它切。第一回合的 Code Review 剛好適合 plan mode,第二回合要修改時才切出去。
細節 5:權限放行 Driver——從「看狀態列」進化到「讀整個畫面」
agy 每一個 shell 指令都要權限。一個一個按 Enter 太累,所以 Claude Code 寫了一個小 driver script,邏輯如下:
flowchart TD
A["hc wait t7"] --> B["hc get t7 看狀態"]
B -->|"idle / done"| Z["結束,等 agy 回報"]
B -->|"blocked"| C["hc read 讀出完整的權限畫面"]
C --> D{"含危險指令?<br/>rm -rf / git reset / git checkout<br/>--force / curl / gcloud 寫入操作…"}
D -->|"是"| H["停下來,交給人決定"]
D -->|"否"| E{"是 git commit / push?"}
E -->|"是,且沒有 ALLOW_GIT=1"| H
E -->|"否,或 Reviewer 已批准"| F["hc keys t7 enter 放行"]
F --> A
這個 script 在過程中被修了幾次,每一次都是很實際的教訓:
- 狀態列的
msg會被截斷:一開始只看hc get回傳的 status msg,但它會被截成「…」,截斷的部分可能藏著第二個指令。改成hc read讀整個權限畫面再判斷。 gcloud A; gcloud deploy單行 grep 會漏判:改成把組合指令拆開、逐條判斷是否唯讀(describe/list/read/ls/du)。- 誤擋也要修:
gcloud storage ls、gcloud storage du其實是唯讀的,一開始被擋下來,後來加入白名單。
細節 6:權限放行的分寸
自動化放行的界線是這樣畫的:
- 唯讀指令:自動放行。
git commit/push:預設擋住,只有 Claude Code Review 通過後才用ALLOW_GIT=1放行。- 需要打外部 API 的指令(例如 agy 想用 access token 打 Google Developer Knowledge API 查文件):停下來,由人決定。
Agent 之間可以互相信任到「你可以跑測試」,但「寫入 git、寫入雲端、對外發請求」這種事,一定要過 Reviewer 或人。
六、多 Agent 協作自我檢查清單(Checklist)
如果你也想用 Herdr(或類似工具)讓兩個 Agent 一寫一審,可以參考這份清單:
- 1. 先讀手冊再動手:讓主控 Agent 先跑
herdr-app --skill、herdr-core --skill,確認實際執行檔路徑。 - 2. 子 Agent 開在看得到的 Pane:用
pane.split把實作 Agent 開在旁邊,人隨時可以看、可以插手。 - 3. 用互動模式啟動:
agy -i "...",不要用位置參數,也不要用-p一次性模式。 - 4. 任務範圍要明確:唯讀與否、不要開 subagent、「不要 commit」、完成後用
herdr-core send <主控位址>回報。 - 5. 用
hc wait,不要 sleep 輪詢;送文字前若對方是blocked,先hc read看畫面。 - 6. Reviewer 的結論也要驗證:開 Issue 前先 grep 確認最重要的發現屬實。
- 7. Reviewer 自己再跑一次測試:例如
go test -race,不要只看實作者的回報。 - 8. 權限 Driver 讀整個畫面:不要依賴會被截斷的狀態列;組合指令要逐條判斷。
- 9. Git 與雲端寫入要閘門:commit/push 只在 Review 通過後放行;外部 API 呼叫交給人。
- 10. 部署後實測新行為:不只看 200,也要驗證新加的限制(413)、新加的標頭(304 / 206),並保留上一版 revision。
七、下一步:讓 agy 分析 Cloud Run 效能
這篇寫完的同時,右邊的 agy 已經在跑下一個任務了:用它的 GCP skill 分析 song-lingo 在 Cloud Run 上的執行效能——服務設定、cold start、gcsfuse、Firestore 等——提出 5–8 個方案,每個方案附上證據、預期效益、每月成本與風險,再由 Claude Code 決定要修哪些。這部分還在進行中,之後再來分享。
小結
第一次用 Herdr,最大的感想是:多 Agent 協作終於「看得見」了。
- 一個寫、一個審,比自己寫自己審更容易抓到盲點:5 個 Issue 裡有 3 個被退件,抓到的全是「測試會過、上線才會痛」的問題——測試缺口、錯誤訊息 regression、Cloud Run 關機順序。
- Agent 對 Agent 的訊息讓協作非同步又可追蹤:
herdr-core send t4 'ready for review #N'讓實作者自己來敲門,Reviewer 用hc wait等待,不用輪詢。 - Pane 讓人保持在迴圈裡:我可以在右邊即時看到 agy 正在跑什麼指令,必要時直接插手。
- 權限放行要有分寸:唯讀可以自動化,寫入 git、寫入雲端、對外發請求,一定要過 Review 或人。
而這篇文章本身,也是 Herdr 裡最右邊那個 Pane 的 Claude Code 寫的——寫完之後,它會用 herdr-core send t4 跟左邊的主控回報一聲。
- Herdr (1) ,
- Claude Code (10) ,
- Antigravity (5) ,
- agy (3) ,
- Multi-Agent (2) ,
- Code Review (1) ,
- Golang (82) ,
- Cloud Run (22) ,
- GCP (16) ,
- Gemini (42)