只動你必須動的地方。只清理你自己造成的混亂。
編輯既有程式碼時:
當你的修改產生了孤兒程式碼時:
檢驗標準: 每一行修改都應該能直接追溯到使用者的需求。
LLM 有一個自然的傾向:看到「不夠優雅」的程式碼就想改進。當你要求修復一個 bug,模型會順便改引號風格、加型別提示、加入 docstring、重構變數命名⋯⋯這些「改進」在技術上可能是對的,但會:
關鍵心態:把「配合現有風格」當作專業的表現。資深工程師會說:「這段程式碼不夠優雅,但既然它運作正常,我不會動它。」
def validate_user(user_data):
+ """Validate user data.""" # 加了 docstring
+ email = user_data.get('email', '').strip() # 改了取值方式
+ # Validate email # 改了註解
+ if '@' not in email or '.' not in email.split('@')[1]:
raise ValueError("Invalid email")
+ username = user_data.get('username', '').strip()
+ if not username: # 加了驗證
+ raise ValueError("Username required")
+ if len(username) < 3: # 加了新驗證
+ raise ValueError("Username too short")
def validate_user(user_data):
# Check email format
- if not user_data.get('email'):
+ email = user_data.get('email', '')
+ if not email or not email.strip():
raise ValueError("Email required")
# Basic email validation
# Check username
if not user_data.get('username'):
raise ValueError("Username required")
return True
到互動範例頁看這個原則的 ❌/✅ 對照:
def verify_token(token):
- parts = token.split('.')
+ header = base64.urlsafe_b64decode(parts[0]) # 新增
+ alg = json.loads(header).get('alg')
+ if alg is None or alg.lower() == 'none': # 修漏洞
+ raise InvalidToken('alg none rejected')
- ... # 既有 60 行驗證邏輯
+
+ # 順手整理:把 tab 改成 4 空格、加型別提示、
+ # 加 docstring、把變數更名為更「清楚」的名字
+ # (共 40 行與漏洞無關的改動)
# 30 分鐘後:
# - 漏洞修好了 ✅
# - 但整個 module 的 diff 有 120 行
# - reviewer 必須在 120 行裡找出真正的那 5 行
1. 編碼前思考(原則 1):
→ 漏洞面有多大?`alg: none` 之外,是否也接受 `HS256` 但密鑰為空的組合?
→ 提問確認,但先以最小修復範圍回覆。
2. 簡潔優先(原則 2):
→ 最小修復是「拒絕不安全演算法」,一個條件判斷就夠。
3. 精準修改(原則 3):
→ 只在驗證邏輯入口加 4 行,其餘 60 行一個字都不動:
def verify_token(token):
parts = token.split('.')
header = json.loads(base64.urlsafe_b64decode(parts[0]))
alg = header.get('alg', '').lower()
if alg == 'none' or alg not in ALLOWED_ALGS: # +4 行
raise InvalidToken('unsafe algorithm')
...
4. 目標驅動(原則 4):
→ 先寫重現漏洞的測試(`alg: none` 的 token 應被拒絕)
→ 套用修補 → 測試通過 → 既有測試全綠
「順便整理」的部分,我寫在 PR 描述裡建議,
但不在這個 PR 動手——分開處理才有乾淨的責任邊界。
模型的訓練資料裡,「重構 PR」與「code review 讚美清理」的樣本非常多——它們獎勵「改進」。於是模型學到一條潛規則:把程式碼改得更好 = 更好的輸出。問題是模型的目標函式裡沒有「diff 最小化」這一項:它優化「改進的總量」,而不是「改進的相關性」。
更深一層是關聯性判斷的失敗:模型難以區分「相鄰程式碼」(adjacent,同一區塊)與「相關程式碼」(associated,與本需求有因果關係)。空郵件 bug 與 username 驗證邏輯相鄰、卻不相關——模型在 token 層面把它們當作同一段落順手重寫,這個重寫不是「決定」,而是生成的慣性。
成本不對稱:對模型來說,「重寫整段」比「精準改 2 行」更容易生成(生成的 token 少、上下文一致)。但對人類 reviewer 來說,這兩個選項的成本剛好相反:2 行的 diff 審計 30 秒,120 行的 diff 審計 40 分鐘。原則 3 就是要把模型的「低成本路徑」扳回人類的「低成本路徑」。
使用者說「只改這一行」。Agent 回應「好的」,也真的只改了那一行——但它把那一行裡的三個變數名全部換成「更好的名字」。行數沒有蔓延,語義卻蔓延了:下游呼叫方依賴的變數名被悄悄改掉,編譯器幫忙攔下一個,另兩個在 runtime 才爆炸。真正的違反:精準是「最小語義變更」,不是「最小行數變更」。變數名、簽名、回傳值——這些「一行裡的小事」同樣在原則 3 的管轄範圍內。
| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
| diff 出現沒人要求的 docstring | 文檔衝動——「看到就補」的習慣 | 只接受能直接追溯到需求的變更,其餘退回 |
| 引號/空白/縮排被改 | 格式化慣性,可能還混入 formatter 自動執行 | 要求配合現有風格;提醒「格式化不在本次需求內」 |
| 修一個 bug 卻動了 3 個函式 | 順帶重構——把「相關」誤當「相鄰」 | 要求最小 diff;每個變更都要能回答「這一行是為了修哪個需求?」 |
| 變數/函式被改名 | 命名衝動——即使名稱不在需求範圍內 | 明確聲明「名稱不是本次需求」,改名單獨開 PR |
| 「看起來沒用」的舊程式碼被刪 | 死程式碼誤判——刪除權責被越界 | 提醒規則:「提出來,但不要刪」;先問使用者再處理 |
先釐清: 1. 「漏單」的定義?是回調完全沒收到,還是收到了但處理失敗? 2. 影響範圍:所有支付方式?只有特定閘道? 3. 時間線:什麼時候開始的?有沒有 trigger event? → 確認:回調有收到,但處理時 Redis 逾時導致訂單狀態沒更新。 → 這是「處理失敗」不是「沒收到」,解法完全不同。
1500 行 legacy 認證 + 支付模組,我只改 3 行:
def handle_payment_callback(payload):
order_id = payload['order_id']
- status = redis.set(f"order:{order_id}", payload['status'], ex=30)
+ status = redis.set(f"order:{order_id}", payload['status'],
+ ex=3600) # 從 30 秒改為 1 小時
if not status:
raise PaymentError("Redis write failed")
return {"received": True}
刻意沒動的:
- 1497 行的既有認證邏輯 — 與本需求無關
- callback 路由的格式(混合 tab/空白) — 配合既有風格
- 相鄰 5 行的另一個 bug(已知但未被要求修) — 提出但不動
不用 Redis 陣列、不用重試佇列、不用 dead letter queue。 一行數字從 30 改成 3600——這就是「剛好夠」。 如果日後 Redis 逾時問題再次出現(第二個真實案例), 再提重構方案。今天不猜測。
計畫與驗證: 1. 寫重現測試:付款回調 → Redis 寫入 → 等 35 秒 → 驗證值仍在 驗證:修復前測試失敗(30 秒逾時),修復後通過(3600 秒) 2. 套用修復 → 驗證:測試通過 3. 既有支付測試 → 驗證:全綠無回歸 4. 手動驗證:在測試環境做一筆真實支付 → 等 1 分鐘 → 確認訂單狀態正確 PR 描述附上:變更追溯表(3 行 → 全部追溯到「修復 Redis 逾時」) + 想順手整理的地方(寫在建議裡,不在此 PR 動手)。
效能:順帶重構引入的「改進」可能悄然改變效能特徵。例如:把同步改為 async(「順手最佳化」)可能改變並發模型,引入 race condition;把 for 迴圈改成 list comprehension(「順手優化」)可能在大資料集上因為中間 list 分配而更慢。更根本的是:無關改動增加了 diff 大小,增加了 code review 時間,增加了 CI 執行時間——這些都是隱性效能成本。
品質:精準修改是品質的第一道防線。每個不必要的改動都是一個潛在的回歸:改了變數名,下游可能依賴舊名;改了引號,某些語言的字串比較行為可能改變;改了 return value 的型別,序列化可能失敗。品質的定義不是「改完更漂亮」,而是「改完行為完全符合預期,零副作用」。
安全:安全修復必須可審計——這是合規的基本要求。一個包含 5 行安全修補 + 40 行格式整理的 PR,在安全審計時等於「大海撈針」。審計者需要在 30 秒內確認「這次只改了漏洞相關的行」。更嚴重的是:格式改動可能悄悄改變安全行為(例如把 if not token 改成 if token is None,在某些邊界條件下行為不同)。精準修改在安全場景是硬性要求,不是建議。
Agent 修復一個 SQL injection 漏洞,同時「順手」把 if not user_input 改成 if user_input is None。原來的寫法會擋住空字串 ""(因為 not "" 為 True),改成 is None 後空字串不再被攔截——攻擊者可以送 ' OR '1'='1 前面加一個空字串觸發不同路徑。修了舊漏洞,順手開了新漏洞。
| 面向 | 原則 3 · 精準修改 | 相關原則 | 差異說明 |
|---|---|---|---|
| 核心動作 | 只碰必須碰的,diff 每行可追溯到需求 | 原則 2(簡潔優先) | 兩者都反對「多做」:原則 3 反對的是「改了不該改的檔案/行」(範圍蔓延),原則 2 反對的是「寫了不必要的程式碼」(過度工程)。一個管「動什麼」,一個管「寫什麼」。 |
| 時機 | 正在修改既有程式碼時 | 原則 1(編碼前思考) | 原則 1 是「開始前的閘門」;原則 3 是「過程中的護欄」。原則 1 防止方向錯,原則 3 防止範圍溢出。 |
| 對抗的偏誤 | 格式化慣性 + 死程式碼誤判 + 命名衝動 | 原則 4(目標驅動) | 原則 3 管「改了多少」,原則 4 管「改了對不對」。兩者搭配:改得精準(3),且每步可驗證(4)。 |
| 最大價值 | 讓 PR 可被審計,讓回歸風險趨近於零 | 原則 2(簡潔優先) | 原則 2 用「最少程式碼」降低複雜度;原則 3 用「最小 diff」降低審計成本。兩者共同目標:用最少的變更達到需求。 |