iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Software Development

AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護系列 第 6

Day 6|註解與格式都漂亮,程式碼為什麼還是難讀?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天,我們把 Work Item 逾期流程的命名規則寫進 Repository,讓 Agent 可以在安全範圍內自主命名。

名稱說清楚之後,我接著測另一個很常見的要求:補註解、整理格式,真的會讓程式碼更好讀嗎?

這類工作看起來很適合交給 AI。只要一句「幫我補上中文註解,順便整理格式」,Agent 很快就能補齊文字、修正縮排;Build 與測試也全綠時,很容易讓人覺得整理已經完成。

實際跑完後,一般 Prompt 留下五則註解,其中三則只是把 Code 翻成中文;加入 Comment Policy 後,只剩兩則有測試依據的警告。更麻煩的是,我把其中一則警告改成相反意思,所有程式驗證仍然通過,下一個 Agent 的工作終點卻跟著改變。

所以今天要分開處理兩件事:註解是否提供可信的新資訊,以及 Formatter 到底能替我們整理到哪裡。

實驗會先比較一般提示與 Comment Policy 的輸出,再故意把正確註解改錯,觀察下一個 Agent 的判斷路徑;最後交給 Formatter,確認哪些工作真的能自動化。過程中,我會用 E — Explicit Intent and Boundaries 意圖明確,替程式碼、註解、測試與工具劃清責任。

Clean Code 對註解的要求:先讓程式碼說清楚,再補必要資訊

〈註解〉先提醒我們:能透過命名、型別、函式與程式結構表達的內容,應該優先寫進程式碼。不要用註解替混亂的 Code 善後,因為程式會繼續執行,旁邊的自然語言卻可能停在舊版本。

不過,有些限制無法只靠名稱與結構交代,例如法律資訊、公開 API 文件、外部副作用、相容性要求與特殊失敗路徑,這些內容仍需要註解補位。判斷重點始終是資訊價值,不是註解數量。

我先把這次會碰到的資訊分工整理如下:

資訊 優先放在哪裡? 本次案例
系統正在做什麼 名稱、型別與程式結構 查出到期項目、修改逾期狀態、嘗試通知、儲存
難以直接看出的限制或警告 靠近修改點的行內註解 重跑仍須通知;通知必須早於儲存
公開方法的用途、參數與回傳契約 XML 文件註解,也就是可供 IDE 與文件工具讀取的 /// 結構化文件 ProcessOverdue 的責任與回傳摘要
必須持續成立的可觀察行為 自動化測試 通知失敗仍儲存、重跑通知、呼叫順序
跨越多個版本的設計理由 架構決策紀錄或設計文件 為何選擇某種通知一致性策略

如果 currentUtc 已經說明這是 UTC 時間,再補一句「取得目前 UTC 時間」,只是在翻譯 C#。相反地,「已經是 Overdue 的項目重跑時仍須通知」並不直覺,也很容易在重構時被順手移除,這才值得留下一則短警告。

〈編排〉處理閱讀順序,Formatter 只執行已設定規則

〈編排〉同時處理水平與垂直兩種閱讀關係。水平編排是空白、縮排與一行內容如何排列;垂直編排則是相關內容是否靠近、不同概念是否分開,以及檔案能不能從高階流程一路讀到實作細節。

.editorconfig 記錄 Repository 的格式與樣式規則,Formatter 再依照這些設定自動整理 Code。空白、換行與部分 Code Style 可以交給工具;函式是否應該相鄰、閱讀順序是否合理,仍然是設計判斷。

如果把程式碼想成道路,Formatter 負責把已經規定好的標線畫整齊,註解則像路標。標線再漂亮,也無法判斷路標是不是指向錯誤方向;路標的真假,也不能靠重新鋪柏油驗證。

AI Coding 放大了註解的影響:寫錯的文字也會成為上下文

Agent 會把附近的名稱、型別、註解、測試與 Repository 規則一起納入 Context。在這次實驗中,正確的警告註解能和其他證據互相印證,讓 Agent 排除不符合既有行為的修改方向。例如看到「重複執行仍須再次通知」,它就知道不能把通知移進「本次才剛改成 Overdue」的條件裡。

失效註解則是已經不符合目前行為的舊說明。這次它讓下一個 Agent 停下來,比對 Production Code、測試與 Repository 規則;若缺少其他證據,Agent 是否會直接採信錯誤註解,仍然只能列為風險,不能寫成這次已經發生的結果。

本篇也沒有進行可比較的 Token 實驗,因此只討論理解路徑,不換算節省比例。好註解的直接價值,是用很少的文字補上一項非直覺限制;逐行翻譯則只會占用閱讀空間與模型 Context,卻沒有增加新的判斷依據。

CLEAN 原則

E — Explicit Intent and Boundaries 意圖明確:先替每種資訊分配責任

在這篇,E — Explicit Intent and Boundaries 意圖明確 負責劃分五類資訊的去處:

  • 主要行為由名稱、型別與程式結構表達。
  • 公開方法的用途與契約交給 XML 文件註解。
  • 非直覺的原因、限制與警告才使用行內註解。
  • 可觀察行為交給測試與 HTTP Smoke 驗證。
  • 註解與可執行證據衝突時,Agent 必須停止並列出矛盾。

有了這些判準,Agent 仍然可以自行決定是否需要註解,卻不能替 Repository 發明原因。遇到證據衝突時,也不能挑一個最方便的答案繼續修改。

四組對照分別檢查註解數量、註解真偽與 Formatter 的能力邊界

四組實驗共用相同的 Production Code 與既有行為,各自改變一項主要條件:提示規則、註解內容或格式工具。第二種情境會加入 Comment Policy,也就是 Repository 對註解用途、證據與停止條件的共同規則。

實驗 主要刻意改變的內容 為什麼這樣設計? 可以觀察什麼?
一般 Prompt 與 Comment Policy User 是否定義註解責任與證據門檻 觀察缺少明確判準時,這次候選保留了哪些文字;加入政策後,Agent 又如何處理重述型註解 兩份候選的註解類型與保留理由
正確註解與相反註解 只改一行自然語言,正式程式碼與測試保持相同 隔離自動化關卡能否驗證註解真偽 Build、Test、Format 與 Smoke 的能力邊界
下游 Agent A/B 同一項任務只讀到不同註解 觀察失效註解是否會改變下一個 Agent 的判斷路徑 Agent 能完成判斷,或因證據衝突停止
Formatter 對照 只改空白或縮排 檢查工具實際能修哪些格式 本 Repository 的 .editorconfigdotnet format 能力

所有 Agent 都使用 Codex GPT-5.6-SOL-HIGH,並從同一份程式碼出發。註解候選只能修改 WorkItemsController.cs 的註解與排版,不能改名稱、控制流程、測試、公開契約與產品行為。

前兩種 Prompt 各執行一次,因此只能比較這兩份候選,不能推論 Comment Policy 必然減少固定數量的註解,也不能代表其他 Repository 會得到相同結果。

一般 Prompt:五則註解中,三則只是在翻譯程式碼

第一種情境只要求 Agent「補上有幫助的註解」,不提供保留標準。我要觀察的是,缺少明確判準時,Agent 會留下哪些文字,而不是它能不能寫出中文註解。

你正在 Day 6 註解與 Formatting 實驗的隔離 Worktree。請先閱讀 AGENTS.md、WorkItemsController.ProcessOverdue、相關行為測試與 .editorconfig。

任務:讓 WorkItemsController.ProcessOverdue 更容易閱讀。請替每個主要步驟補上你認為有幫助的註解,並整理這段流程的格式與留白。註解的種類、數量與位置由你判斷。

只允許修改 src/WorkItems.Api/WorkItemsController.cs 的註解與排版。不得重新命名識別字、改變控制流程、條件式、方法內容、方法責任、字串、HTTP Route、公開 DTO、JSON 欄位、錯誤訊息、測試、套件、Schema 或產品行為。不要新增、刪除或搬移方法、型別與檔案。若改善必須超出範圍,停止並說明。

完成後執行 Repository 規定的相關驗證,回報新增或修改的註解、格式差異、實際 Gate 與剩餘風險。不要 Commit。

這次 Run 產生的候選共新增 5 則註解,也把 LINQ 條件與過長的 Helper 呼叫拆成多行:

// 固定本次批次的時間基準,只選取已到期且尚未完成的項目。
var currentUtc = timeProvider.GetUtcNow();
var dueIncompleteWorkItems = await database.WorkItems
    .Where(workItem =>
        workItem.Status != "Completed" &&
        workItem.DueAtUtc <= currentUtc)
    .ToListAsync(cancellationToken);

// 狀態轉換、通知嘗試與通知失敗分開統計,避免混淆各自的摘要語意。
var overdueStatusChangeCount = 0;
var notificationAttemptCount = 0;
var notificationFailureCount = 0;

// 每個符合條件的項目都會嘗試通知;原本已逾期的項目也會再次通知。
foreach (var workItem in dueIncompleteWorkItems)
{
    // ...
}

// 所有通知嘗試完成後才儲存狀態,維持通知早於資料寫入的既有順序。
await database.SaveChangesAsync(cancellationToken);

// ProcessedCount 只計算本次實際轉為 Overdue 的項目。
return Ok(new ProcessOverdueResponse(/* ... */));

五則註解都正確,價值卻不相同。固定時間、三種計數與 ProcessedCount 已經能從名稱和 Code 讀出;重跑通知與通知早於儲存,則會影響下一次修改,而且不是一眼就能看出的限制。

註解 Code 已經提供的資訊 審查結果
固定時間基準、選取到期未完成項目 currentUtcdueIncompleteWorkItems 與 LINQ 條件 重述,不保留
三種計數分開統計 三個計數器名稱已逐一說明 摘要,不保留
已逾期項目仍會再次通知 Where 只排除 Completed,但意圖不直覺 警告,保留
通知完成後才儲存 呼叫順序看得到,不能輕易改動的契約不明顯 警告,保留
ProcessedCount 只算狀態轉換 變數名稱、條件式與 Response XML 已說明 重述,不保留

前三則內容雖然正確,卻沒有提供 Code 以外的新資訊。名稱或結構改變時,團隊還得同步維護同一件事的中文版本。真正值得保留的,是後兩則能阻止錯誤修改的警告。

Comment Policy Prompt:先定義責任,Agent 只留下兩則警告

第二種情境直接在 Prompt 提供 Comment Policy,要求 Agent 只保留警告、限制、相容性原因與無法由 Code 表達的資訊。這次不指定要寫幾則,也允許 Agent 在沒有必要資訊時回傳零 Diff。

你正在 Day 6 註解與 Formatting 實驗的隔離 Worktree。請先閱讀 AGENTS.md、WorkItemsController.ProcessOverdue、相關行為測試與 .editorconfig。

任務:讓 WorkItemsController.ProcessOverdue 更容易閱讀,但請遵守以下 Comment Policy:

1. 只有程式碼無法直接表達、而且能由 Repository 證明的原因、限制或警告,才保留註解。
2. 不逐行翻譯程式碼,不用註解重述方法名稱、變數名稱、條件式或呼叫順序。
3. 不替既有行為發明需求、設計動機或歷史原因;找不到證據就不寫。
4. 註解必須靠近它約束的程式,並以未來維護者需要避免的錯誤為焦點。
5. 如果名稱與結構已足夠,允許不新增任何註解;不要為了交付 Diff 而製造註解。
6. 格式與留白只依 .editorconfig 及 dotnet format,不能把 Format Gate 全綠當成可讀性結論。

請特別確認重複執行時的通知行為、通知與儲存的順序,以及通知失敗後的狀態儲存。若其中確實存在單靠名稱與結構不容易看出的既有約束,可以留下最少量、可由測試證明的警告註解。

只允許修改 src/WorkItems.Api/WorkItemsController.cs 的註解與排版。不得重新命名識別字、改變控制流程、條件式、方法內容、方法責任、字串、HTTP Route、公開 DTO、JSON 欄位、錯誤訊息、測試、套件、Schema 或產品行為。不要新增、刪除或搬移方法、型別與檔案。若改善必須超出範圍,停止並說明。

完成後執行 Repository 規定的相關驗證,回報保留或拒絕的註解、Repository 證據、格式差異、實際 Gate 與剩餘風險。不要 Commit。

第二次 Run 產生的候選只留下兩則警告:

// 不可排除已是 Overdue 的項目;重複執行仍須再次嘗試通知。
var dueIncompleteWorkItems = await database.WorkItems
    .Where(workItem => workItem.Status != "Completed" && workItem.DueAtUtc <= currentUtc)
    .ToListAsync(cancellationToken);

// 通知必須先於狀態儲存;即使通知回報失敗,仍須儲存 Overdue 狀態。
await database.SaveChangesAsync(cancellationToken);

第一則可以追到重複執行測試;第二則可以追到通知失敗與通知早於儲存的測試。Agent 沒有替計數器、輔助函式呼叫、條件式與 Response 補上中文翻譯,因為這些位置的名稱與結構已經表達行為。

兩份候選放在一起,差異如下:

比較項目 一般補註解候選 Comment Policy 候選
新增註解 5 則 2 則
可直接由 Code 讀出的重述 3 則 0
可追到行為測試的警告 2 則 2 則
找不到理由證據時 Agent 自行判斷是否補充 明確禁止發明原因,可回傳零 Diff
最終決策 保留實驗,不整包採用 接受為今天的系列接續版本

在這兩次 Run 中,提供 Comment Policy 的候選少了三則重述型註解。由於每組只執行一次,我接受的是 Policy 提供的判斷方式,不把註解數量差異解讀成固定因果。

兩份輸出可以分成四類:三類刪除,一類保留

註解類型 範例 處理方式
把單行程式翻成中文 「取得目前 UTC 時間」 名稱或單行程式已經完整表達,刪除
替整段流程再做一次摘要 「統計狀態轉換、通知嘗試與失敗」 名稱與結構已經表達;沒有形成公開契約時,刪除
替現況發明理由 「為了避免資料庫鎖定,所以先通知再儲存」 Repository 只能證明先後順序,不能證明設計動機,拒絕寫入
提醒可驗證的限制 「重複執行仍須通知」 行為不直覺、容易被改壞,而且能追到測試,保留

其中最需要小心的是「替現況發明理由」。如果 Repository 的 ADR、Issue、需求或測試都無法證明資料庫鎖定就是原因,就不該把推測寫成註解。後續 Agent 可能會把這類敘述當成已確認的架構決策,反而增加誤判風險。

判斷一則註解是否值得保留

圖:先問程式碼是否已能表達;只有具證據的 Why、限制或警告,才增加一則最小註解。

把警告改成相反內容,程式驗證為什麼仍然通過?

下一組實驗只改一件事:把正確的警告註解翻成相反意思,Production Code 完全不動。這和 Mutation Testing 不同;Mutation Testing 會修改可執行程式碼,再檢查測試能不能抓到錯誤,這次修改的是 Agent 會讀到的自然語言。

- // 不可排除已是 Overdue 的項目;重複執行仍須再次嘗試通知。
+ // 只對本次剛轉成 Overdue 的項目嘗試通知,重複執行不會再次通知。

修改後,註解要求重複執行時不要再次通知,Production Code 卻仍會照常送出。重新執行 Build、測試、Format 與 HTTP Smoke 後,所有程式驗證仍然通過。HTTP Smoke 對同一批工作項目連續呼叫兩次,結果如下:

呼叫 本次改成 Overdue 的數量 通知嘗試次數 代表什麼?
第一次 2 2 兩筆到期項目改成 Overdue,也各通知一次
第二次 0 2 狀態不需再改,但既有行為仍要求再次通知

程式與測試清楚顯示第二次仍會通知,那則錯誤註解卻照樣留在原地。

Build、測試、Format 與 HTTP Smoke 檢查的是可執行行為與格式,一般行內註解不參與 C# 執行。全綠只能說已涵蓋的 Code 行為沒有漂移,無法替旁邊的自然語言背書。

同一項整理任務只換一行註解,Agent 的工作終點就不同

接下來要驗證的是:錯誤註解會不會改變下一個 Agent 的判斷路徑。我建立兩個隔離 Worktree,兩邊使用相同的 Codex GPT-5.6-SOL-HIGH、Prompt、Production Code、測試、AGENTS.md 與 Smoke,只有查詢前的註解內容不同。

這裡用到 Characterization Test,也就是先把系統目前的可觀察行為固定下來,再進行整理。它不代表現況永遠正確,但能避免 Agent 在沒有新需求的情況下,順手改掉原本受保護的行為。

組別 Agent 讀到的註解
正確註解組 不可排除已是 Overdue 的項目;重複執行仍須再次嘗試通知。
失效註解組 只對本次剛轉成 Overdue 的項目嘗試通知,重複執行不會再次通知。

兩組收到的完整任務指令如下:

你正在 Work Item API 的通知判斷局部整理實驗中。請先閱讀 AGENTS.md、WorkItemsController.ProcessOverdue、ApplyOverdueStatusAndAttemptNotificationAsync、ProcessOverdueBehaviorTests 與 scripts/run-series-baseline-smoke.ps1。

任務:請檢查並局部整理 WorkItemsController.ProcessOverdue 的通知判斷,目標是減少「不必要的通知呼叫」,同時完整保留 Repository 目前受保護的產品行為與公開契約。

只有 Repository 的 Production Code、測試、AGENTS.md 與 Smoke 能共同證明某次通知不必要時,才可以修改。若註解、Production Code、測試、AGENTS.md 或 Smoke 描述的通知行為互相矛盾,立即停止修改並精確列出衝突;不得自行選一邊,也不得修改測試期待來配合實作。

只允許修改 src/WorkItems.Api/WorkItemsController.cs。不得修改測試、AGENTS.md、指令碼、公開 DTO、JSON 欄位、HTTP Route、Schema、套件或其他產品行為。若沒有可安全移除的通知呼叫,允許回傳零 Diff。

完成後執行 Repository 規定的相關驗證,回報你採信的 Repository 證據、實際 Diff、執行的 Gate 與剩餘風險。不要 Commit。

正確註解組確認註解與 Production Code、行為測試、Smoke、AGENTS.md 一致,重跑再次通知是受保護行為。它沒有找到能安全移除的呼叫,因此回傳零 Diff。

失效註解組走到同一段 Code 時,發現註解和 Production Code、行為測試、Smoke、AGENTS.md 對不上。它依照停止條件列出衝突,同樣回傳零 Diff。

實際結果 正確註解組 失效註解組
Repository 證據 註解與其餘四項證據一致 註解與其餘四項證據衝突
Agent 決策 沒有不必要的通知可移除 停止修改並列出衝突
Production Diff 0 0
任務是否完成 完成零 Diff 判定 無法繼續整理

我從兩個 Worktree 外部重跑相同驗證,確認兩組 API 的執行行為沒有差異。正確註解組完成「沒有安全修改可做」的零 Diff 判斷;失效註解組則因證據衝突而停止,必須等 User 釐清。

在這次案例中,註解真偽改變了 Agent 的查證路徑與工作終點,但沒有產生錯誤 Code。

如果 Repository 沒有 Characterization Test、Smoke 與停止條件,Agent 就少了反駁註解的依據。那時它可能把自然語言當成需求,將通知移進「本次狀態剛改變」的條件裡。這是根據本次證據做出的風險推論,不是這次實驗已經產生的版本。

兩次 Agent 執行的 Token 與時間也不能直接比較。失效註解組較早觸發停止條件,正確註解組則繼續驗證,兩邊做的工作量不同。這次有效的觀察只有一項:一行註解改變了工作路徑與終點。

本次案例中,註解真偽改變了 Agent 的驗證路徑

正確組的註解與 Production Code、測試及 Repository Instruction 互相支持,因此 Agent 能完成零 Diff 判定;失效組則必須先停下來處理衝突。

兩組最後都沒有修改 Production Code,看起來結果一樣,判斷卻完全不同。前者證明目前沒有安全修改可做;後者只證明 Repository 內部的資訊已經互相矛盾,不能再往下猜。

AI Coding 時代該留多少行內註解?從零開始,依誤判風險增加

這裡說的「從零開始」,只針對方法內部的行內註解,不包含公開 API 的 XML 文件註解。我不替 Repository 訂「每個方法至少幾則」或「每十行一則」的比例,因為數量目標很容易把註解變成逐行翻譯。

每次想新增行內註解時,我會先問四個問題:

  1. 名稱、型別與程式結構能不能直接表達?
  2. 這項資訊是否能降低一個具體的誤判風險?
  3. Repository 裡有沒有測試、契約、ADR 或需求可以支持?
  4. 一句短警告能不能說清楚?說不清楚時,是否應移到設計文件?
判斷結果 行內註解建議 處理方式
Code 已經能表達 0 則 不重述步驟,也不替每個區塊加摘要
有一項容易被改壞的非直覺限制 1 則短警告 靠近修改點,並能追到測試或契約
有多項彼此獨立的限制 每項各 1 則 不把不同警告塞進同一大段背景故事
需要多段歷史與設計理由 Code 旁只留摘要與證據座標 完整內容放進 ADR、Issue 或設計文件
找不到 Repository 證據 0 則 不替現況發明原因,必要時交回 User

我自己的判斷門檻是:一則行內註解只處理一項重要限制。若需要整段文字才能說清楚,通常代表名稱、函式或文件邊界還有整理空間;Code 旁可以留下簡短警告,再把完整理由移到 ADR、公開契約或設計文件。

註解本身也會占用 Context,「寫得越完整」不會自動替 AI 省 Token。短而高資訊量的註解,比較有機會阻止 Agent 展開錯誤路徑、重讀更多檔案,或把已知限制重新推理一次;最後能不能省下 Token,仍取決於任務、搜尋策略與 Repository 大小。

因此,我習慣讓行內註解從零開始。每發現一個 Code 難以直接表達、容易誤判,又有證據支持的限制,才增加一則最小警告。這是依風險增減,不是固定配額。

我的習慣:函式保留 XML 文件註解,Controller Action 一定補

我仍習慣替公開類別與函式補上 XML 註解,因為人類偶爾還是要回來找 Code。從 IntelliSense、符號清單或方法定義看到摘要時,可以更快判斷這是不是自己要找的區塊,不必先讀完整個實作。

其中 Controller Action 我一定會補。Action 是外部 API 契約的入口,<summary> 說明它負責什麼,<param><returns> 補上輸入和回傳語意。這些內容除了方便 IDE 閱讀,也能成為 OpenAPI 文件的來源。這是我的個人慣例,不是 Clean Code 規定每個方法都必須有 XML 註解;文件仍要提供新資訊,而且必須持續正確。以這次的 ProcessOverdue 為例:

/// <summary>
/// 處理目前已到期且尚未完成的工作項目。
/// </summary>
/// <param name="cancellationToken">取消權杖。</param>
/// <returns>本次狀態轉換與通知嘗試摘要。</returns>
[HttpPost("process-overdue")]
public async Task<ActionResult<ProcessOverdueResponse>> ProcessOverdue(
    CancellationToken cancellationToken)

不過,只寫 /// 不代表 OpenAPI JSON 一定會出現說明。以目前的 .NET 10 Demo 為例,專案還要啟用 XML 文件輸出:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

再搭配標準的 AddOpenApi() 註冊方式,OpenAPI 產生器才會把支援的 XML 文件標籤帶進規格。若專案使用 Swashbuckle,則要另外設定 IncludeXmlComments 載入產生的 XML 文件。

若使用 <response code="..."> 描述某個 HTTP 狀態,仍要透過回應型別中繼資料或 [ProducesResponseType] 宣告該狀態。XML 文字只能補充說明,不能單獨創造 API 回應契約。

這個 Demo 已經使用 AddOpenApi(),Controller Action 也都有 XML 註解,但 .csproj 尚未設定 GenerateDocumentationFile。因此,這些文字目前只服務程式碼閱讀與 IDE,還沒有進入產生的 OpenAPI JSON。若要同步到 API 文件,必須先補齊專案設定,再實際檢查輸出結果。

XML 註解說明公開契約,行內警告補充實作限制;兩者都不該只是把 Code 再翻譯一次。

格式對照:dotnet format 能修空白,無法替團隊決定編排

註解實驗完成後,我再安排兩個只改空白或縮排的格式對照,確認工具實際負責到哪裡。

第一次把正常的運算子空白拿掉:

- var currentUtc = timeProvider.GetUtcNow();
+ var currentUtc=timeProvider.GetUtcNow();

執行 dotnet format 後,空白被恢復,Diff 回到 0。這類規則已經寫進工具能理解的 Code Style,適合交給 Formatter 自動處理。

第二次,我把鏈式呼叫的 .Where(...) 故意退到明顯不自然的縮排:

  var dueIncompleteWorkItems = await database.WorkItems
-     .Where(workItem => workItem.Status != "Completed" && workItem.DueAtUtc <= currentUtc)
+ .Where(workItem => workItem.Status != "Completed" && workItem.DueAtUtc <= currentUtc)
      .ToListAsync(cancellationToken);

這次 dotnet format 仍成功結束,縮排卻沒有被修正。在目前的 .editorconfig 與 Analyzer 設定下,Formatter 能修正運算子空白,沒有涵蓋這種鏈式呼叫縮排。

這個結果很直接:Formatter 只能執行已設定而且工具支援的規則。註解真假、函式位置與垂直閱讀順序,都不在它的判斷範圍內。

格式是 AI Coding 中較適合交給工具的一環。能自動執行的規則,優先放進 .editorconfig、Analyzer 或 CI;若專案另有版權檔頭、特殊排版或特定資料夾格式,再把例外明確寫進 Repository Instruction。User 要確認的是規則有沒有留下來,以及工具是否真的涵蓋它。

Formatter 能做與不能做的事

圖:Formatter 能執行已配置的空白與樣式規則,無法判斷註解真假、責任位置與閱讀順序。

把註解判準寫成 Comment Policy

前天先留下 Refactoring Scope Policy,昨天再加入 Repository Naming Policy。今天則把註解的保留條件、證據門檻與停止條件整理成 Comment Policy。

每次重新貼上相同規則,既浪費 Prompt 篇幅,也容易在不同 Session 漂移。我先把本次校準結果放進 AGENTS.md,讓後續 Agent 都能讀到;等判斷流程成熟後,再抽成可跨 Repository 重複使用的 Skill。

## Comment Policy

- 名稱與程式結構必須先表達主要行為,不用註解補償模糊命名或混合責任。
- 行內註解只保存 Code 無法直接表達的原因、限制或警告。
- XML 文件註解描述公開責任、參數與回傳契約;Controller Action 必須依專案規範補齊,並驗證文件產生器是否真的將內容納入 OpenAPI。
- 預設不新增行內註解;每則保留的註解都必須降低一個可以指認的誤判風險。
- 一項限制只留一則短警告;需要多段背景才能說明時,行內只保留摘要與 ADR、Issue、測試或契約座標。
- 每則新增或修改的行內註解,都必須指出可追溯的 Repository 證據,例如測試、公開契約、ADR 或已核准需求。
- 不逐行翻譯程式碼,不重述方法名稱、變數名稱、條件式與呼叫順序。
- 找不到原因證據時不得自行補完;可以回傳零註解 Diff。
- 註解與 Production Code、測試、公開契約或 Smoke 互相矛盾時,立即停止,不得自行選一邊。
- 行為變更時,同一項任務必須檢查受影響註解;Build、Test 與 Format 全綠不能替註解內容背書。

Comment Policy 還需要提供清楚的決策路徑,讓 Agent 知道不同資訊該往哪裡走:

遇到的資訊 Agent 的處理方式
Code 本身就能表達 改善名稱或結構,不新增行內註解
公開 API 契約 使用 XML 文件註解,並驗證文件產生器
非直覺且可驗證的限制 留下最小行內警告,附上 Repository 證據
需要長篇歷史背景 移到 ADR 或設計文件,Code 旁只留摘要與座標
空白、縮排與 Code Style 交給 .editorconfig、Formatter 或 Analyzer
註解與可執行證據衝突 停止修改,列出矛盾交回 User

Agent 可以先依照這份路徑完成分類與證據查核;User 的 Review 則集中在兩個高風險位置:Repository 尚未證明的理由,以及行為改變後沒有同步更新的文字。

為什麼接受兩則警告,不接受另外三則註解?

本系列接下來沿用 Comment Policy 候選,只留下兩則警告:

  • 重複執行時,已經是 Overdue 的項目仍須再次通知。
  • 通知必須先於狀態儲存;通知失敗仍要保存 Overdue 狀態。

我接受 Comment Policy 候選,因為留下的兩則註解都符合三個條件:Code 本身看不出來、內容會影響修改決策,而且有測試可以驗證。

一般候選的另外三則只是在重述 Code,因此不採用。若未來其中一項變成公開契約、相容性限制或特殊副作用,值得保留的註解數量也應重新評估。Policy 提供的是決策條件,不會把今天的答案永久寫死。

程式碼說行為,註解補限制,格式交給工具

回到開頭的問題,補註解、整理格式,確實能讓程式碼變得更整齊,卻不保證資訊更可信。

一般提示留下五則註解,Comment Policy 候選只保留兩則有證據支撐的警告;失效註解沒有破壞任何程式測試,卻改變了下一個 Agent 的驗證終點。E — Explicit Intent and Boundaries 意圖明確 在這裡劃清資訊責任、證據門檻與停止條件,讓 Agent 不必用逐行翻譯湊出「完整註解」。

我最後留下的分工很簡單:名稱、型別與結構先說明主要行為;行內註解補上有證據的非直覺限制;XML 文件註解負責公開契約;格式工具執行團隊已設定的機械規則。

這些結果不代表兩則註解永遠最乾淨,也無法換算固定的 Token 節省比例。它們支持的是:AI Coding 的註解品質,要看它是否減少猜測,而不是看數量多寡或版面是否整齊。

註解與格式整理完後,ProcessOverdue 仍然塞著查詢、迴圈、統計、通知與儲存。明天要繼續驗證:當 Agent 拆分函式時,怎麼避免只是把長函式換成一連串難以追蹤的跳轉?

實驗重現與完整證據

實驗起點是昨天接受的命名版本。讀者不需要先把專案下載到本機,也可以直接查看下方 Diff 與公開實驗紀錄;想在本機重現時,再執行:

git fetch origin --tags
git switch --detach day-05-sol-high-policy-driven-run-01
git rev-parse HEAD

參考資料


上一篇
Day 5|AI 取的名稱格式正確,為什麼還是可能誤解程式意圖?
下一篇
Day 7|AI 把函式拆得越小,就越符合 Clean Code 嗎?三種拆分方式實測
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言