讓 AI agent 寫 code 久了,會發現一個固定毛病:你只要一個能跑的小東西,它給你一個帶設定檔、抽象層、還預留三個未來擴充點的「框架」。它當然會寫,毛病出在它的預設值——多寫一點比較安全

最近讀了一個叫 Ponytail 的開源專案,專門治這個。它的招牌語是一句很有態度的話:

The best code is the code never written.

我把它的原始碼 clone 下來實際跑過一遍 hook,這篇是拆解之後的筆記——它哪裡有意思、哪裡要保留懷疑。

它不是工具,是一份「規則」

第一個容易誤會的點在它的形態。Ponytail 把一份規則集注入成 agent 自己的 system context,agent 讀進去之後直接「就是」懶模式。它不是一個你去呼叫的 library,也沒有一個「外部第二意見」在旁邊讓 agent 查詢引用——規則直接長在 agent 身上。

所以你不會看到 agent 回你「根據 Ponytail……」。它只是行為變了:動手前先停下來想一輪「這真的需要寫嗎」。

核心:一道 YAGNI 階梯

整套東西的心臟是一道階梯。動任何 code 前,從上往下走,停在第一個成立的那一階:

1. 這東西需要存在嗎?     speculative = 跳過,一行說明 (YAGNI)
2. 標準庫能做嗎?         用它
3. 平台原生功能涵蓋嗎?   <input type="date"> 勝過 picker library
4. 已裝的依賴能解嗎?     用它,不為幾行能解的事新增依賴
5. 一行能解嗎?           那就一行
6. 真的都不行才:         寫能動的最小實作

兩階都成立就取較高的那階。第一個能動的懶解,就是對的解。

這道階梯本身沒什麼新鮮——<input type="date"> 勝過自己刻 date picker、CSS 能做就別動 JS、DB constraint 勝過 application code,這些每個資深的人都念過。會念跟做得到是兩回事。多數人是 code review 時才想起這些原則,Ponytail 的價值在於把它前移成 agent 每次動手前的反射動作,趁還沒寫下去就攔下來。

真正聰明的地方:零指令生效

我原本以為這種東西要每次手動呼叫,結果不是。它靠兩個 Claude Code hook:

Hook行為
SessionStart每次開 session,把整份規則當隱藏 context 注入
UserPromptSubmit只在你打 /ponytail... 切換等級時才動作,平常完全 no-op

換句話說,正常工作流是零指令:裝好、開 session 就生效,每個回覆都在規則內。規則自己還帶了一句 “ACTIVE EVERY RESPONSE. No drift back to over-building”——明擺著就是怕 agent 聊著聊著又飄回過度設計。

我直接 node hooks/ponytail-activate.js 跑過驗證:

  • 預設 full mode → stdout 吐出完整規則集
  • off → 只吐一個 OK,不注入任何東西

注入格式還會依 host 分流:Claude Code 走 raw stdout,Codex 走結構化的 additionalContext。設計上是認真要跨多個 agent stack 用的。

一個會踩到的啟動時機坑

規則靠 SessionStart hook 注入,而這個 hook 只在 startup / resume / clear / compact 觸發。意思是——你 session 中途裝它,當下這個 session 不會生效/plugin install/reload-plugins 都不觸發 SessionStart。要 /clear 或開一個新 session 才會上線。

確認生效的方法:flag 檔 ~/.claude/.ponytail-active 會出現(沒生效時這檔不存在)。

三個強度,差別比想像中小

Ponytail 有 lite / full / ultra 三個 mode,但有件 README 沒講、要去翻 SKILL.md 才知道的事:三個 mode 的核心階梯和規則完全一樣,差別只在一行強度描述和一個示範。

用「要不要加 cache」當例子最清楚:

  • lite:照你要求做了,但補一句「FYI,lru_cache 一行就夠」,選擇權給你
  • full(預設):直接 @lru_cache(maxsize=1000),略過自訂 cache class
  • ultra:profiler 沒喊就不要 cache,手刻一個 TTL cache class 是個 bug farm

可以看出來態度是逐級變兇的:lite 是禮貌提醒,ultra 會直接當場質疑你的需求本身。

最值得抄的設計:把「偷懶」變成可追蹤的債

這是我覺得整個專案最有想法的部分。

少寫 code 有個風險:你略過的東西沒人記得。Ponytail 的解法是——每走一個捷徑,就在原地留一行註解:

# ponytail: global lock, per-account locks if throughput matters

然後一個 /ponytail-debt 指令把散落各處的 ponytail: 標記收成一張帳。讓「之後再說」不會默默變成「永遠不做」。

但這裡有一條我覺得最關鍵的判準,用之前一定要先搞清楚:

債只能開在「規模 / 彈性」軸,絕不能開在「正確性 / 安全」軸。

可以標的:「現在用 global lock,吞吐量遇到瓶頸再換 per-account」「現在 O(n²),資料量大再換索引」。這些版本現在就是對的、能跑的,只是天花板在規模。

不准標的:跳過 input validation、省掉 error handling、砍掉 security 或 a11y。這些是硬紅線,不能拿一行註解搪塞過去。

所以 ponytail: 標的是「最簡單但正確的版本 + 標註它何時該長大」,跟「壞掉的版本,承諾以後補」要分清楚。這個區分很重要,差一點就變成幫技術債找藉口。

它的論點還有一層我蠻認同:不標不代表沒債。過度設計是「預先加了用不到的東西」,那是看不見的死複雜度;Ponytail 留下的「欠著之後可能要加」,則是可以 grep 出來的 conditional TODO。最危險的債,是那種沒人記錄、看起來又像刻意設計的。 把取捨喊出來,正好對著「|| 默默吞掉錯誤」那種 silent-fail 反向操作。

兩個要保留警覺的失敗模式

Ponytail 自己其實也誠實寫了它會怎麼壞,我覺得值得抄下來:

  1. 「追蹤」只是承諾,不是保證。 你要是從不跑 /ponytail-debt、或跑了也不處理,那它就只是給技術債貼了張好看的標籤而已。
  2. 「故意」和「無能」的判斷權在 agent 手上。 草率的實作有可能被包裝成「刻意的 simplification」。所以 ponytail: 註解該當成一個需要 review 的宣稱,不是免死金牌。

那些「省 80% code」的數字,先別信

專案頁面寫了一串很漂亮的數字:少寫 80–94% 的 code、快 3–6 倍、省 47–77% 的 API 開銷。

這些是專案自報、沒有獨立複驗的,當行銷宣稱看就好。我自己的判斷是:Ponytail 真正的價值在機制設計——每個 turn 注入規則、那道 YAGNI 階梯、可追蹤的債務帳本、還有一條明確的「什麼時候不准偷懶」護欄。這套設計成不成立,跟那串百分比準不準是兩回事。

也誠實補一句:前面講的「實測」是 clone 原始碼、直接執行 hook 看注入產物,確認機制如它所說在運作,這跟「裝起來日常用了一週」是兩種驗證強度,我能背書前者、還不能背書後者。

拿它掃一個真實 repo:9 條建議,7 條是誤判

光看機制不夠,我把 /ponytail-audit 拿去掃了一個正在生產跑的後端 repo(一個量化回測引擎),看它在真實 codebase 上的成色。

結果它一口氣找出 9 條「過度設計」,估計可以砍掉約 256 行。數字很漂亮。問題是,我接著用另一個模型(讓它實際打開每個被點名的檔案逐條 adversarial 複審)走一遍,9 條裡有 7 條站不住腳,被退回。最後真正落地的只有 2 條,而且都是貨真價實的死碼。

被退回的 7 條,誤判的原因很有代表性:

  • 「把 OrderedDict 換成 dict 就好」——它沒看到別處有一行 isinstance(metric, OrderedDict) 的硬判斷,還有測試綁著這個契約。換下去,某些 metric 會默默不進報表。
  • 「client 跟 server 的這幾個 descriptor 重複,合併吧」——這兩邊的結構早就分岔了(server 那側多扛了風險欄位),合併等於去動序列化邊界,高風險。
  • 「這 4 個 requirements.txt 是重複的,刪掉」——其中 3 個被 Dockerfile 直接 COPY 進去裝,CI 還有一支腳本在驗它們的一致性。刪了 build 直接爆。
  • 「這些單行 property 收斂成 __getattr__——那些 property 是策略的公開 API、也是文件的落點。收益低、傷到的是介面邊界。

看出共通點了:ponytail 看的是「這段 code 的形狀像不像多餘的」,它讀不到的東西才是真正決定能不能砍的——runtime 的 isinstance 契約、序列化邊界、build/CI 的隱性依賴、對外的 API 介面。這些不在它眼前的檔案裡,它就當作不存在。

所以這趟跑下來,結論很清楚:ponytail-audit 的輸出是一份待查清單,每一條都得有人(或另一個會實際讀檔的模型)去複審它的正當性,不能直接照單砍。它在那個 repo 上的「誤判率」高到我不會讓它的判斷直接落地。這也直接決定了我下面的用法。

我自己怎麼用:預設關,按需問

讀完之後,我刻意沒讓它常駐。

我對「always-on 自動改寫 agent 行為」這件事有點疑慮——我要的是被動諮詢:規則待在旁邊,等我問才開口,不要在背後默默替我做決定。所以我的設定是預設 off,每個 session 不注入。需要的時候才用它幾個獨立指令:

  • /ponytail-review——只審當前 diff 有沒有過度設計,回我一張「該刪清單」
  • /ponytail-audit——審整個 repo 的肥肉
  • /ponytail-debt——把欠的債收成一張帳

要它持續作用才開 /ponytail full,完事 /ponytail off。主導權留在自己手上,看完它的建議,採不採納我自己決定。

對我來說這是比較舒服的距離:它像一個會在我開口時給出好意見的顧問,主導權始終在我這邊。

寫在最後

Ponytail 裡大半的規則我其實本來就在用——最小變更、結論先行、小步保持可運行。這些不算新觀念。它真正帶來的是兩件偏冷門的功夫:把這些原則工具化(每個 turn 自動注入,不靠自律),以及讓偷懶可以被 auditponytail: 債務帳本)。

最好的 code 是沒寫的那行——這句話誰都會講。難的是讓一個預設想多寫的 agent,每次動手前都真的停下來問一次。