「被人逐行反駁的痛,和指控本身沒有關係;能把這兩件事分開的人,才留得下真正的證據。」
——《阿帕契開源審計錄》¹ 卷三·覆審篇
幕間†
白天,敘事者起身按桌:「不用盤了,我是狼人!」自爆翻牌出局。
二號神色漠然——他早就看出敘事者記錯了神職人數,卻故意沒有出言提醒。
法槌重重落下:「狼人自爆,天黑請閉眼!」
天亮了。法官的聲音從高處落下:「昨夜倒下一人,六號座位。」沒有身分,沒有遺言,只有一個座位號。六號,就是昨天在不屬於他的輪次拍出獵人身分的那個人。狼刀落在他身上;女巫在發言裡認了,毒也是她下的——同刀同毒,一個人倒下,一個被毒死的獵人是開不了槍的。我當時只認定一件事:死的就是真獵人。桌邊剩五個人:我、二號、七號——如今戴著警徽、票值一點五——還有女巫和獵人。獵人從頭到尾幾乎沒開口,只是把手按在桌上。
七號把昨夜寫好的指控書一段一段唸出來。桌邊沒有一個人道謝,他們直接開拆。女巫指出她時間線那一欄把某一夜標錯了;我也跟著丟了一個 -1,理由是「這份指控的語氣我不喜歡」——說穿了純粹是個人觀感,後來我自己想通,那是我的自尊在作祟,跟證據沒有半點關係。二號則只是點頭,附上一句「我同意剛才的說法」。每一行都被貼上一個 -1。七號握著羊皮紙的手指節泛白。
輪到二號發言,他先把上一位講者的最後一句話原封不動覆誦一遍——「『時間線對不上』,對,時間線對不上,所以我認為……」——然後才把自己的推論掛上去。他的每一句話,都是接在別人的句尾長出來的。
當你花了一整夜寫的東西被當眾拆成碎片,你要怎麼分辨:哪些留言是在幫你,哪些只是在傷你?
維護者和 PMC 沒有義務教你寫程式。他們大可以直接關掉你的 PR,一個字都不留。當他們願意逐行標記問題、寫下「這裡為什麼不行」,那是資深工程師的時間在免費灌進你的分支。把「他針對我」翻譯成「他指出了一個我沒看到的邊界」,你才拿得到這份免費諮詢。
情緒上會痛是正常的,因為你剛把幾個小時的心血攤在桌上。但那份痛跟程式碼的好壞沒有因果關係。第 40 行的一個 -1,是一句關於第 40 行的陳述,不是關於你這個人的判決。
舉個例子。你收到一句「這個設計會讓每個呼叫端都得先檢查一次 nil,你把複雜度推給了所有使用者」。第一層讀起來像是在說你不會設計。但把情緒濾掉,剩下的是一個具體的架構觀察:你的介面把不變條件的維護責任外包出去了。維護者花了三十秒寫這句話,等於免費幫你上了一課「介面應該讓正確的用法變成唯一容易的用法」。你要做的不是道歉三行,而是回一句「懂了,我把檢查收進建構子,讓回傳型別直接保證非 nil」,然後把那個 commit 貼上去。
會讓你難受的嚴厲留言,其實是好消息:代表對方還願意花時間。真正該擔心的是另一種狀況——你的 PR 開了兩週,沒有任何人留言,只是靜靜躺在那裡。在開源專案裡,被 Request Changes 的 PR 還有救,被無視的 PR 才是真的死了。
維護者的沉默通常有三種意思:說明寫得太糊,他看不懂又沒空追問;改動太大,他不知道從哪裡開始審;或者這件事的優先級根本排不進來。這三種你都能用昨天學的四段結構去拆解——把 PR 拆小、Context 補足、Problem 講清楚,沉默往往就會變成留言。所以與其害怕那些 -1,不如把它們當成對方還坐在牌桌上的證明;而面對沉默,你要做的不是等,是回頭把自己的提案改到別人願意開口。
收到一批 Request Changes,先別急著逐條回。先分類:
// Reviewer: "Request changes — this silently swallows the parse error and
// always returns nil. The caller cannot tell 'no evidence' from 'broken input'."
// Before: the signature already promises an error, but the body throws it away.
func ParseTimeline(raw string) (Accusation, error) {
a, _ := decode(raw)
return a, nil
}
// After: the error is actually propagated, not discarded.
func ParseTimeline(raw string) (Accusation, error) {
a, err := decode(raw)
if err != nil {
return Accusation{}, fmt.Errorf("parse timeline: %w", err)
}
return a, nil
}
這個修改看起來只是把被吞掉的 error放回去,但審查者真正教你的是:一個函式的簽章可以誠實地承諾了「我會回報錯誤」,實作卻悄悄毀約。這種洞見,你自己盯著程式碼看三天也未必想得到——也正因為簽章沒變,昨天七號在 PR 裡附的那個 TestParseTimeline_RejectsBrokenInput 才能原封不動地跑:改動前它會失敗(err 永遠是 nil),改動後它會通過。
回覆留言也有它的禮儀。逐條回,不要整批「都改好了」,讓審查者能一條一條核對。改了就貼對應的 commit 連結;不打算改的,說明為什麼,並把決定權留給對方。不要過度道歉,一句「謝謝指出」就夠了,長篇自責只會拖慢節奏。
你一定會遇到你認為錯的留言。第一直覺是寫一段辯護,證明自己原本的寫法沒問題——這幾乎總是錯的一步。辯護會把技術討論變成立場對峙,對方下一句就會為了不輸而加碼,來回三輪之後你們爭的已經不是程式碼了。
更便宜的做法是先問,不要先辯。一句「我想確認我理解對了:你擔心的是併發下的重入,還是單純的可讀性?」成本極低,而且經常會發現對方看到了你沒看到的東西,或者他其實誤讀了你的 diff。等你真的確認彼此在講同一件事、而你仍然不同意,再簡短說明理由,然後把結論留給對方——如果他是 committer,他的判斷就是 binding,你把想法講清楚就夠了。
怎麼分辨風格意見和架構意見? 風格意見通常一句「照專案慣例」就能關掉:命名、檔案位置、要不要提早 return、註解格式。架構意見會牽動介面、依賴方向、錯誤傳遞的邊界,改了它你得動好幾個檔案。把這兩種當成同一種處理,是新手最常見的內耗來源——為了一個變數名跟人吵三回合,真正該談的抽象邊界反而沒力氣談了。風格的照單全收,把子彈留給架構。

七號把留言分成「事實錯誤/風格偏好/架構疑慮」三類,本質上是在做一件事:承認一次 code review 裡混雜著好幾種完全不同性質的判斷,硬要一個人一次全部顧到,注意力必然分散。Claude Code 對深度審查採取的做法,恰好是把這件事顯性化——它可以同時派出多個審查視角,各自只盯著一個維度(例如「這裡是不是真的有正確性錯誤」與「這段邏輯是不是能被簡化、有沒有重複造輪子」),跑完之後才把各自的發現匯總、去重,交回一份結構化的結果,而不是要求單一審查者在一輪對話裡同時兼顧邏輯正確性與風格品味。
這跟七號的三分類是同一種紀律的兩種實作:與其祈禱一個人一次看完全部維度,不如先把維度拆開,各自看透,最後再合併結論。對開發者來說,這也回答了「一份留言到底該歸進哪個盒子」的困惑——如果連分工審查的機器都要把這些維度拆開處理,人類審查者混在一起處理只會更吃力。
七號趁著第一輪發言的空檔當場改稿。她的指控書比開場時短了一截:時間線那欄照女巫的指正重新對過,措辭裡的情緒字眼全部拿掉,連我那個不講理的 -1,她也把它對應的那句語氣整個改平。剩下的每一條都扛住了挑剔。她的指控不是被打爛,是被磨利了——留下來的那條「二號從來沒有第一個開口」,反而比原本更硬。
而二號覆誦別人句尾才能接話的毛病,在這一輪裡桌邊每個人都看見了——一個只會把自己的推論掛在別人句子後面的人,從來提不出屬於自己的第一句。這份磨利過的指控,已經準備好迎接第二輪。
這一輪的過程,其實就是一次壓縮版的 code review 週期:提案、被挑戰、分類、修改、再送審、收斂。差別只在於七號沒有 CI 幫她跑測試,她的測試就是全場的挑剔。能撐過這一輪的人,下一次提案會寫得更好;撐不過的人,會把每一次 Request Changes 都記成一次羞辱,然後再也不敢提 PR。
把 review 當成免費諮詢的人,三個月後會發現自己的第一版程式碼就已經接近維護者要的樣子;把 review 當成人身攻擊的人,會一直卡在同一個水準,因為他把最寶貴的回饋擋在門外。
城堡上空的月亮只剩一彎還沒被陰影吃掉。法官敲槌,宣布第一輪結束、進入第二輪發言,那句話卻在中途頓了一下,像有兩個人搶著用同一把嗓子把它講完。我盯著那輪快要全黑的月,第一次清楚地感覺到:今天會不一樣。
讀完這篇,你現在該做的是:下次收到 Request Changes,先回一句「謝謝,我看懂了」,再動手改;把每一條留言分進事實/風格/架構三個盒子,只在架構那個盒子上花力氣爭論。Google 工程實踐的 Reviewer 指南與 Developer 指南都有 code review 禮儀與協作溝通的專門章節可以照著練。
"responding to code review feedback" "separate code from ego" "request changes as free mentorship" "open source maintainer review etiquette"
¹ 註:本書名為情境設定之虛構文獻,非真實歷史或開源紀錄。
† 註:這段幕間的時間點不在今天這一刻——Day 29 回頭說明。