這篇從一個版本錯置的經驗出發,討論工具設計中一個容易忽略的面向:工具接受自由輸入時,預設路徑如何影響使用者的決策。適用於 CLI、API、表單、自動化流程——任何需要使用者做選擇的介面。


背景:我們怎麼管理版本和工作項目

我們的專案用 semver(語意化版本)管理發布節奏。每個版本(如 v0.3.0)有明確的功能範圍,由數個提案定義——每個提案描述一組要交付的功能和邊界。版本內部再拆成多個工作項目(ticket),按批次排序執行(類似 Sprint,但以依賴順序而非時間框分批)。

版本的生命週期很單純:planned → active → completed。一個版本的所有 ticket 完成後,跑發布流程、打 tag、標記 completed。

圍繞這個流程,我們自建了兩個 CLI 工具:

工具用途
ticket create建立工作項目,指定歸屬版本
version-release版本發布(pre-flight 檢查、文件更新、打 tag)

這兩個工具在設計時,都選擇了「彈性優先」——接受任何合法輸入,不對使用者的選擇做判斷。

這個選擇在後來被證明是錯的。

版本語意:大版本和小版本的分工

semver 的 MAJOR.MINOR.PATCH 有明確的語意分工:

層級語意觸發條件
MAJOR(0.x → 1.0)不相容的 API 變更破壞既有介面
MINOR(0.3 → 0.4)新功能新增向後相容功能
PATCH(0.3.0 → 0.3.1)修復和改善bug fix(我們擴充涵蓋重構和流程改善)

版本號不只是標記——它決定了工作項目應該放在哪裡。一個 bug fix 放進 MINOR 版本,語意上等於說「這個 bug fix 和下一批新功能綁定發布」——多數情況下這不是你想要的。

版本管理只是其中一個場景——任何接受自由輸入的內部工具,只要輸入涉及分類或歸屬判斷,都可能有同樣的問題。我們的工具沒有表達這個語意,接下來的兩個事件是後果。

事件一:改善類工作放進了新功能版本

v0.3.0 發布了三個新功能。發布後的版本檢討發現了一個測試隔離問題,v0.3.1 做了 hotfix。

接下來要做根因分析和系統性防護。建立工作項目時,順手指定了 --version 0.4.0——v0.3.0 和 v0.3.1 都已發布,v0.4.0 是下一個功能版本,看起來是合理的選擇。

CLI 接受了這個輸入,沒有任何提示。

三張改善類的工作項目(根因分析、重構、規則文件)就這樣和 PostgreSQL Storage Backend(v0.4.0 的核心功能)混在一起。直到使用者檢視版本看板時才發現不對——改善類工作和新功能綁在同一個發布週期,語意混亂。

修正方式:建立 v0.3.2、遷移三張 ticket、重新發布。額外花了一輪操作成本。

事件二:已完成版本的幽靈

版本看板的異常不止一處。同一次檢視中,看板顯示 v0.2.0 有未完成任務。

查證後發現 v0.2.0(38 張 ticket 全部完成)、v0.2.1(7 張全完成)、v0.2.2(1 張已結案)三個版本在版本清單中仍標記為 active。它們在數個月前就該標為 completed,但沒有。

原因是版本發布工具的 pre-flight 檢查只看「當前版本的 ticket 是否完成」,不掃描「更早的版本是否有 active 殘留」。早期版本可能是手動發布的,跳過了狀態同步步驟。工具沒有補救機制,殘留就一直留著。

看板靜默地把這些版本顯示為「有未完成工作」,產生誤導。

為什麼會這樣:工具沒有 opinion

兩個事件的共通根因:工具在應該有立場的地方選擇了沉默。

建立工作項目時

ticket create --version 0.4.0 --type ANA --action "分析" — 工具知道這是一張分析類的 ticket,也知道 v0.4.0 的 scope 是 PostgreSQL Storage。但它不認為自己有責任判斷「分析類 ticket 放在新功能版本是否合理」。它只做格式驗證:版本號存在嗎?通過就建立。

發布版本時

發布工具的盲區更隱蔽。每次發布時,工具會檢查「這個版本的所有工作項目都完成了嗎?」——如果答案是「是」,就繼續打 tag、更新文件、推送。但它從不回頭看更早的版本:有沒有哪個舊版本的工作項目早已全部完成,卻一直沒被標記為「已完成」?這種殘留不影響當前發布,但會讓看板持續顯示「舊版本有未完成工作」,誤導每一個後續查看看板的人。

兩者都是「工具做了它被要求做的事,但沒做它應該做的事」。

工具什麼時候應該有 opinion?

不是所有情境都需要工具有立場。有一個簡單的判斷標準:

當存在一個「多數情況下正確的預設行為」時,工具應該把它表達出來。使用者可以覆蓋,但預設路徑應該引導正確做法。

這裡的 opinion 是建議而非阻擋——工具提示預設路徑,使用者可以覆蓋。這個區分很重要:阻擋式的 opinion(必須額外操作才能繞過)適合風險高的操作(如 force push to main、刪除生產資料);建議式的 opinion 適合歸屬判斷。錯誤成本不對稱決定了形式:建議錯了,使用者覆蓋一次,幾秒鐘;沉默錯了,事後修正,幾小時。只要建議的正確率不是極低,建議就比沉默划算。

這個邏輯不限於 CLI。API 的預設參數、表單的預選值、自動化流程的預設路由——任何使用者需要做選擇的介面,都有機會用預設行為表達 opinion。

改善類 ticket 放 patch 版本,在多數情況下是正確的。「多數情況下對」已經足夠讓工具表達立場:

1$ ticket create --type IMP --action "修復" --target "retry test"
2[建議]  ticket 為修復類,建議放 v0.3.2patch bump
3       而非 v0.4.0(下一個功能版本)
4       使用 --version 覆蓋此建議

前版本 status 掃描也是。已完成版本仍為 active 在所有情況下都是異常——工具不需要猜,只需要報告:

1$ version-release check
2[WARN] v0.2.0:38 張 ticket 全部完成但 status 仍為 active

為什麼使用者是 AI agent 時問題更嚴重

這個 pattern 在人類使用者身上已經存在——人類也會走阻力最小的路徑。但人類有跨次記憶:「上次放錯版本被糾正過,這次注意一下。」

AI agent 沒有這個。

每個 session 是一個全新的 agent,它讀到的是:版本清單中 v0.4.0 是 active、CLI 接受 --version 0.4.0、沒有警告。於是它每次都會用最直覺的選擇——當前 active 的最大版本。

上次的教訓不會自動傳遞到下次。除非教訓被固化成工具行為。

這把「工具應該有 opinion」從「建議做法」升級為「必要條件」:

  • 人類使用者:opinion 是提醒,有助於減少錯誤
  • AI agent 使用者:opinion 是最可靠的防線,因為工具在操作當下的即時引導是離決策點最近的攔截

工具的預設行為,就是團隊的實際流程

工具的預設行為,就是團隊的實際流程。

文件上寫「改善類工作放 patch 版本」沒有用——如果工具不引導,使用者會走工具預設的路徑。人類和 AI 都是。文件說的和工具做的不一致時,工具會贏。

但文件不是敵人。文件定義「應該是什麼樣」,傳遞設計理由和架構決策;工具實現「實際是什麼樣」。兩者不一致時,優先修工具。

如果你希望使用者做 X,不要寫文件說「請做 X」——把工具的預設行為設成 X。

這個原則適用於所有內部工具設計,不限於版本管理:

場景寫文件的做法改工具的做法
commit 前跑測試README 寫「請先跑測試」pre-commit hook 自動跑
PR 描述格式貢獻指南寫範本PR template 預填結構
改善放 patch 版本版本策略文件寫規則CLI 根據 ticket type 建議版本
API 環境參數文件寫「production 需額外確認」API 預設 staging,production 需顯式指定
表單必填欄位說明文字寫「建議填寫」欄位預設值 + 必填驗證

每一個「寫文件提醒使用者遵守操作規範」都是一個信號——工具的預設行為還有空間改善。看到這個信號時,優先評估能否把提醒轉化為工具的預設行為。

Rails 的「Convention over Configuration」是同一個觀念的先驅表達:框架用約定引導開發者走正確路徑,省去不必要的配置決策。有 opinion 的工具在必要決策時引導方向。兩者共通的是把判斷成本從「每次使用時」前移到「設計工具時」——一次判斷,永久生效。

回去檢查你的工具

  1. 列出你的工具中所有使用者需要做選擇的地方——CLI 參數、API 欄位、表單選項、流程分支
  2. 對每個問:有沒有「多數情況下正確」的預設值或建議值?
  3. 有的話,加建議式 opinion(提示預設 + 允許覆蓋)
  4. 檢查工具的清理路徑:有沒有前一次操作應該同步但沒有同步的狀態?
  5. 如果你的工具會被 AI agent 或自動化流程呼叫,上述每一項的優先級加倍——自動化沒有判斷力,它只走預設路徑