Nat 鍾承恩

實戰 2026 / 06 約 5 分鐘

別憑感覺重構:把『該先改哪裡』變成 agent harness 的一個確定性訊號

一、重構最貴的,不是動手改,是改錯地方

任何一個跑了幾年的 codebase,工程師都「感覺得到」哪裡有問題。但「感覺」會騙人:最大的檔案不一定最該改,最醜的角落可能半年沒人碰——重構它純粹是白工。

真正的成本不在敲鍵盤,在選錯目標:花三天重構一個沒人會再動的類別,而那個每週都被改、改一次就牽動十幾個檔的核心,你一直繞著它走。

所以動手前該回答的不是「怎麼改」,是「先改哪裡值得」——而這題不該憑直覺。

二、把「該改哪」變成一個可驗證的訊號

我要的不是又一份玄學報告,是一個可重跑、攤得開、改完能看分數有沒有降的客觀訊號。

定位一句話:把重構決策從個人直覺,變成 agent harness 裡的一個確定性分析關卡——它告訴你(或 agent)「先看這 20 個」,而且每個結論都附拆解、可被質疑、可被複算。

三、我怎麼建出來(機制)

跟著一個 running example 走:一支約 800 檔、跑了四年多的 Flutter App。

① 量「難改」,不是量「大」。 借 Ousterhout 的定義:複雜度 = 任何讓系統難理解、難改的結構,跟行數無關。他給了公式 C = Σ cₚ·tₚ:每部分的複雜度,乘上你花在它身上的時間。一個 2000 行但沒人碰的檔,tₚ≈0,對整體幾乎無害。該排序的是「住起來貴不貴」,不是「大不大」。 這也正是業界主流的 hotspot 模型(Adam Tornhill / CodeScene):分數 = 複雜度 × 變更頻率

② git 歷史就是「難改」的行為證據。 「多常改」「總跟誰一起改」靜態快照看不到,要挖 git log。變更頻率(churn)= 你實際花的時間;共同變更(總在同一個 commit 一起改的檔群)= 「改一個要動一片」的客觀證據。把時間維度加回只看結構的分析,這是 behavioral code analysis 的核心。

③ 正規化要抗離群、抗被玩。 多訊號融成一個分數,我用百分位(rank)而非 min-max——那支 2100 行的檔不會壓扁其他人;而且永遠把分項攤開,不塌縮成一個數字。因為指標一旦變成目標就失真(Goodhart:為衝覆蓋率寫空測試)。分數只拿來排序、引導注意力,不當 KPI 考核。

④ 象限揪出「複雜但冷」的未爆彈。 把複雜度×頻率相乘,當兩軸本身相關時,會把分數擠到對角線,反而藏住「結構很複雜、但最近沒人動」的檔。所以除了總分,再分四象限;那格 latent-risk 就是滴答作響、你卻沒在看的未爆彈,值得在重新動那塊功能前先拆雷。這呼應 Fowler 的 technical debt quadrant。

⑤ 誠實說限制:有些訊號機器測不準。 我本來想用「fan-in=0 = 死碼」找可刪的孤島——實測這支 App 有 46% 的 symbol fan-in=0,但幾乎全是活的(路由動態實例化、DI、codegen、enum 擴充都是靜態圖抓不到的用法)。教訓:靜態訊號是「候選」不是「判決」;死碼偵測得是一條 triage 漏斗,不是看一眼圖就好。同理,「view 不該有 logic」這種架構規則,可機械測的是「越層 import」(用依賴圖抓 view 直接碰 domain/service),但「build() 裡塞了業務邏輯」是語意的,得交給 LLM。把可機械的那半做成 fitness function,不可機械的那半別假裝測得到。

四、為什麼這是 harness 的一環,而不只是一支 lint

agent 能放手改 code 的前提,是它的判斷能被外部關卡擋住、被獨立複算。這支評分器扮的就是那個角色:一個確定性、可重跑、攤得開的分析訊號,讓「先改哪裡」不靠 agent(或人)自說自話,而是一個能被質疑、能被 diff 的客觀輸入。它跟「驗證要兩個獨立 context」「先有可量訊號才有可信進展」是同一套 harness 紀律——只是這次關卡管的是重構決策,不是測試通過與否。

限制與下一步

這是排序工具,不是真理:精確名次(第 7 vs 第 15)別當真,權重仍待校準,改名歷史會讓剛搬過家的熱檔暫時失蹤。它的用途是把注意力放對地方,這也是它唯一聲稱能做的事。下一步:把分層 fitness function 補齊、跑權重敏感度、死碼漏斗獨立成另一個工具。