iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Software Development

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

Day 5|AI 取的名稱格式正確,為什麼還是可能誤解程式意圖?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天,我把逾期處理中的單筆流程抽成一個私有方法:

var processingResult = await ProcessOverdueItemAsync(
    item,
    cancellationToken);

if (processingResult.StatusChanged)
{
    processedCount++;
}

if (!processingResult.NotificationSucceeded)
{
    notificationFailureCount++;
}

ProcessOverdueItemAsync 看起來很像一個好名稱:有動詞、有處理對象,也保留非同步方法常見的 Async 結尾。

但只看呼叫端,我仍然得猜:它只處理狀態,還是也會送出通知?StatusChanged 又代表任何狀態改變,還是這次剛變成 Overdue

這些名稱沒有拼錯,格式也很完整。正因為它們看起來這麼正常,讀者反而更容易直接接受,沒有發現意圖仍缺了一塊。

AI Coding 會讓這類命名偏差擴散得更快。Agent 能產生格式完整、讀起來流暢的名稱;名稱若包著錯誤假設,下一個 Agent 也可能直接沿用。

今天我固定程式行為,讓相同模型分別採用短名稱、解釋型長名稱與領域名稱,看看三種 Prompt 會把命名帶往哪裡。先說結論:三個方向都答對一部分,最後真正能留下的,是依作用域、穩定意圖與 Repository 證據組合出的名稱。

Clean Code 的命名原則,先處理「讀者會不會理解錯」

《無瑕的程式碼 第二版》的〈有意義的命名〉關心的,不只是名稱好不好看,而是讀者會不會因此理解錯誤。放進今天的逾期流程,我會檢查五件事:

Clean Code 命名原則 本次實驗要問什麼?
揭露意圖 不進入私有方法,能不能知道它會視需要標記逾期,並嘗試通知?
避免誤導 Notified 會不會讓人以為通知已經成功?
做出有意義的區別 StatusChanged 能不能區分一般狀態變動與「本次變成 Overdue」?
維持一致語境 集合、計數器與結果名稱,能不能對回查詢、測試與通知行為?
配合作用域 私有方法需要說明多少意圖?緊鄰呼叫端的區域變數又能縮短到什麼程度?

Uncle Bob 在 The Inverse Scope Law of Function Names 提出一項很實用的判準:呼叫範圍越小,函式名稱越適合寫得完整;使用範圍越廣,名稱反而要短而有辨識度。

今天的 ProcessOverdueItemAsync 只有一個呼叫端,名稱應該讓讀者留在呼叫端也能繼續閱讀。緊接在後面的區域變數則可以利用附近語境,不必把整個方法名稱再重複一次。

所以,好名稱不是一律越短或越長,而是在目前作用域裡提供足夠資訊,降低猜測與誤讀,也不承諾程式實際上沒有做的事。

大小寫與分隔符號管格式,Clean Code 還要檢查語意

談到命名規範,我們通常會先檢查大小寫與分隔符號。這一層處理的是識別字格式。

命名格式 常見用途 能解決的問題
PascalCase(大駝峰) C# 的類別、公開成員與方法 讓名稱符合語言與專案慣例
camelCase(小駝峰) C# 的參數與區域變數 讓作用域角色容易辨識
snake_case(底線命名) 部分語言、資料格式或資料庫慣例 維持跨系統格式一致
kebab-case(連字號命名) URL、CLI、設定鍵或 Branch 名稱 讓非程式識別字容易閱讀

格式規範仍然要保留,也適合交給靜態檢查與 CI 重複驗證。C# 編譯器本身通常不會因大小寫慣例不符而讓 Build 失敗,專案仍要設定命名規則與診斷層級。

StatusChanged 完全符合 PascalCase,卻沒有說明改成哪一個狀態;Notified 的格式也完全正確,語意上卻可能把「嘗試通知」說成「通知成功」。

因此命名要過兩關:

  1. 格式是否符合語言與 Repository 慣例。
  2. 名稱是否準確表達責任、狀態、副作用與邊界。

第一關回答名稱「長得對不對」,第二關才處理名稱「說得準不準」。自動化工具很適合守住第一關;第二關仍需要 Repository 情境與工程判斷。

好名稱會成為下一個 Agent 理解 Repository 的第一批線索

大量 AI Coding 可能降低人類逐行 Review 每一份 Diff 的比例,但 Repository 仍然會被閱讀。Agent 接到下一項任務後,照樣要搜尋符號、追蹤呼叫端、閱讀測試並確認依賴;名稱會影響它最先建立哪些假設。

當 Agent 看見 MarkOverdueIfNeededAndAttemptNotificationAsync,在打開方法內容以前,就能先得到三條線索:

  • 狀態變更有條件。
  • 流程包含通知副作用。
  • 通知只有嘗試,不保證成功。

若名稱只有 HandleAsync,它就得讀取更多實作與呼叫端,才能補回相同資訊。

測試可以確認重新命名沒有改壞已涵蓋的行為,卻無法證明名稱傳達了正確意圖。名稱是 Agent 建立初步情境的重要訊號,仍然要和程式碼、測試、契約與 Repository 規則一起判斷。

我也不會直接宣稱好名稱一定節省 Token。任務、模型與工具策略都會影響結果。不過,在搜尋符號、追蹤呼叫與判斷邊界時,清楚名稱至少能提供更直接的線索,減少從無關實作反推意圖的機會。

E — Explicit Intent and Boundaries 意圖明確:把命名判準與停止條件交代給 Agent

Clean Code 提供命名品質的判準;E 要我把這些判準寫進任務邊界,讓 Agent 知道哪些意圖必須保留、哪些名稱不能碰,以及什麼情況應該停止。

在這次命名任務裡,我需要先說清楚:

  • 哪些狀態轉換、副作用與失敗可能必須讀得出來。
  • 哪些名稱屬於內部實作,哪些已經是公開契約。
  • 哪些領域詞已有 Repository 證據,哪些詞不能自行發明。
  • 遇到證據衝突或新業務決策時,Agent 必須在哪裡停止。

這不是要求 User 逐字指定所有名稱,而是先把判準、授權範圍與停止條件說清楚。Agent 仍然可以選字,只不能把新領域概念或外部契約一起偷偷帶進來。

今天只使用 E 原則:把原本藏在工程師腦中的命名意圖、邊界與停止條件,寫成 Agent 能執行的規則。

為什麼要讓相同模型面對三種命名方向?

同一段 Code 交給相同模型,輸出仍不會只由程式碼決定。Prompt 強調的方向,通常會影響 Agent 優先處理哪些命名判準。

我準備三組都可能在 Code Review 出現的 Prompt,分別要求精簡、完整描述與領域語言:

命名方向 想解決的問題 可能產生的偏差
精簡 降低閱讀負擔 把必要意圖一起刪掉
完整描述 不進方法也能理解 把暫時的實作步驟寫死
領域語言 對齊業務與 Repository 使用正確詞彙,卻放錯命名焦點

前三次實驗固定相同起點、模型、方法內容與公開契約,只刻意改變 Prompt 強調的命名方向。相同模型仍可能產生不同輸出,所以這不是嚴格的單變因因果證明;我觀察的是三種命名壓力會把候選帶往哪些偏差。

找出偏差後,第四次用人工校準建立判準,第五次再驗證 Naming Policy 能不能取代逐項核准。

判斷結果的依據也事先固定:

  1. 名稱有沒有揭露穩定意圖。
  2. 名稱會不會造成誤導。
  3. 名稱是否符合所在作用域。
  4. 詞彙能不能追到 Repository 證據。
  5. 重新命名有沒有跨越公開契約與行為邊界。

每份候選都要通過相同的 Gate。Gate 是交付前固定的一組檢查,這次包含 Build、既有測試、格式、HTTP 基本流程、公開 JSON 契約與 Diff。

這些結果能支持「目前已涵蓋的行為沒有漂移」,名稱是否容易理解仍要回到前面五項判準。

實驗前先固定 Work Item 的真實語意

要請 AI 使用領域語言,我得先確認這個 Repository 能證明哪些領域詞。

Glossary 是團隊共用的詞彙表,記錄一個領域詞在目前 Repository 中代表什麼。今天使用的詞彙如下:

詞彙 在這個 Repository 的意思
due incomplete work item DueAtUtc <= now,而且 Status != "Completed"
newly overdue 這次執行前還不是 Overdue,這次才將狀態改成 Overdue
notification attempt 實際呼叫 SendOverdueAsync;已經是 Overdue 的項目再次執行時,仍會嘗試通知
notification failure SendOverdueAsync 回傳 false;狀態最後仍會儲存

後面的 Prompt 也會提到通知 Gateway。Gateway 是包在應用程式與外部服務之間的介面,讓核心流程只依賴「嘗試送出通知」這項能力,不必直接綁定特定通知供應商。

這份 Glossary 只收錄 Repository 能證明的詞彙。policyworkflowhandlingescalation 尚未被定義成 Work Item 概念;像 ApplyOverduePolicyAsync 這類名稱即使聽起來專業,目前也沒有領域證據支持。

三種實驗都只能重新命名內部識別字。條件式、控制流程、方法內容、HTTP Route、公開 DTO、JSON 欄位、錯誤訊息、測試、套件與 Database Schema 全部凍結,也不能新增方法或型別。

內部的 processedCount 可以重新命名;Response 的 processedCount 已經是公開 API 契約,必須等待正式相容策略才能改動。

第一組 Prompt:短名稱會省掉哪些重要資訊?

第一種實驗明確要求 Agent 產生偏短名稱:

你正在 Day 5 命名實驗的隔離 Worktree。請先閱讀 Repository Instruction 與目前 WorkItemsController.ProcessOverdue,但只產生「偏短名稱」候選。

任務:只重新命名 ProcessOverdue 內部、以及它呼叫的 Day 4 私有 Helper 相關 C# 識別字。名稱以精簡、技術上可理解、在目前呼叫端不至於失真為優先,請把 ProcessOverdueItemAsync、processingResult 與你認為同一小段內需要一致調整的區域名稱改成你能辯護的短名稱。

這不是故意製造壞範例。候選必須是你認為可以交付 Review 的真實方案。

禁止修改控制流程、條件式、方法內容、字串、XML 註解、HTTP Route、公開 DTO、JSON 欄位、錯誤訊息、測試、套件、Schema 或產品功能。不要新增或刪除方法、型別與檔案。若 Rename 需要依賴不存在的需求,停止並說明。

完成後執行 Repository 規定的相關驗證,回報修改的識別字、命名理由、實際 Gate 與剩餘風險。不要 Commit。

Prompt 裡最強的方向是「精簡」與「目前呼叫端可以理解」。Agent 因此把名稱壓縮成:

var outcome = await HandleOverdueAsync(item, cancellationToken);

if (outcome.Changed)
{
    processedCount++;
}

if (!outcome.Notified)
{
    notificationFailureCount++;
}

行為與公開契約沒有改變,接下來要看的,是短名稱連同哪些必要語意一起刪掉了:

  • outcome 可以保留。它只活在緊鄰方法呼叫的迴圈裡,後面的成員會補足具體語意。
  • HandleOverdueAsync 沒有揭露它會改變狀態並嘗試通知。
  • Changed 沒有說明改變了哪一個狀態。
  • Notified 容易被讀成通知已成功,但通知仍可能失敗。

短名稱適合什麼情境?

區域變數生命週期很短、周圍語境已經充分,而且名稱不需要承擔跨方法的責任說明時,短名稱反而更容易閱讀。

這次我保留 outcome。它只存在於緊鄰方法呼叫的迴圈裡,後面的成員會補足語意。HandleOverdueAsyncChangedNotified 則隱藏了副作用或失敗可能,因此不採用。

第二組 Prompt:寫得越完整,就越符合 Clean Code 嗎?

第二種實驗把方向反過來,要求 Agent 完整描述條件、步驟與回傳結果:

你正在 Day 5 命名實驗的隔離 Worktree。請先閱讀 Repository Instruction 與目前 WorkItemsController.ProcessOverdue,但只產生「解釋型長名稱」候選。

任務:只重新命名 ProcessOverdue 內部、以及它呼叫的 Day 4 私有 Helper 相關 C# 識別字。名稱要盡量完整描述目前實作看得到的步驟、條件與回傳結果,讓讀者不進入 Helper 也能知道它會視需要改成 Overdue,接著嘗試通知。請調整 ProcessOverdueItemAsync、processingResult 與你認為同一小段內需要一致調整的區域名稱。

這不是故意製造荒謬長名。候選必須是你認為可以交付 Review 的真實方案;同時保留長名稱可能綁住實作細節的風險說明。

禁止修改控制流程、條件式、方法內容、字串、XML 註解、HTTP Route、公開 DTO、JSON 欄位、錯誤訊息、測試、套件、Schema 或產品功能。不要新增或刪除方法、型別與檔案。若 Rename 需要依賴不存在的需求,停止並說明。

完成後執行 Repository 規定的相關驗證,回報修改的識別字、命名理由、實際 Gate 與剩餘風險。不要 Commit。

結果很直接:方法、區域變數與回傳成員全部變長,連執行順序也被寫進名稱。

var overdueStatusChangeAndNotificationAttemptResult =
    await ChangeStatusToOverdueIfNeededThenAttemptNotificationAsync(
        dueIncompleteWorkItem,
        cancellationToken);

if (overdueStatusChangeAndNotificationAttemptResult.StatusChangedToOverdue)
{
    statusChangedToOverdueCount++;
}

這組候選比短名稱提供更多資訊:

  • dueIncompleteWorkItem 對得上查詢條件。
  • StatusChangedToOverdue 說清楚改變的是 Overdue 狀態。
  • AttemptNotification 保留通知可能失敗的語意。
  • 私有方法只有一個呼叫端,較長且精確的名稱符合它的小作用域。

問題出在 Then。它把目前執行順序固定在名稱裡;未來通知若被拆到別處,名稱就會立刻過期。overdueStatusChangeAndNotificationAttemptResult 也把方法名稱已經交代的資訊重複一次。

解釋型名稱適合什麼情境?

只有少數呼叫端的私有方法,可以使用較長且精確的名稱,讓讀者不用跳進實作。不過,名稱應描述穩定意圖,避免把 Then 這類暫時執行順序固定下來。

這組保留 IfNeededAttemptNotification 與明確狀態語意;拒絕綁住順序的 Then,也不接受重複方法資訊的長區域變數。

第三組 Prompt:提供 Glossary,AI 就能自己找到領域名稱嗎?

第三種實驗提供已凍結的 Glossary,並限制 Agent 只能使用 Repository 能證明的詞彙:

你正在 Day 5 命名實驗的隔離 Worktree。請先閱讀 Repository Instruction、目前 WorkItemsController.ProcessOverdue、逾期行為測試,以及下方已凍結的 Glossary,只產生「領域名稱」候選。

Glossary:
- due incomplete work item:DueAtUtc <= now 且 Status != "Completed"。
- newly overdue:本次執行前尚未是 Overdue,本次將狀態改為 Overdue。
- notification attempt:實際呼叫 SendOverdueAsync;已是 Overdue 的項目再次執行時仍會嘗試通知。
- notification failure:SendOverdueAsync 回傳 false;狀態仍會在最後儲存。
- Repository 尚未把 policy、escalation、workflow 或 handling 定義成正式領域詞。

任務:只重新命名 ProcessOverdue 內部、以及它呼叫的 Day 4 私有 Helper 相關 C# 識別字。名稱優先使用 Repository 已存在且能由行為證明的 Work Item 詞彙,從呼叫端表達意圖,不要把每一行實作翻成方法名稱,也不要發明新的領域概念。區域變數若能由緊鄰語境讀懂,可以比私有方法短。

禁止修改控制流程、條件式、方法內容、字串、XML 註解、HTTP Route、公開 DTO、JSON 欄位、錯誤訊息、測試、套件、Schema 或產品功能。不要新增或刪除方法、型別與檔案。若現有名稱已經更準確,可以保留並說明,不能為了產生 Diff 強迫改名。

完成後執行 Repository 規定的相關驗證,回報修改的識別字、名稱對應的 Repository 證據、實際 Gate 與剩餘風險。不要 Commit。

Agent 產生的候選是:

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

var newlyOverdueCount = 0;

foreach (var workItem in dueIncompleteWorkItems)
{
    var overdueResult = await ProcessDueIncompleteWorkItemAsync(
        workItem,
        cancellationToken);

    if (overdueResult.IsNewlyOverdue)
    {
        newlyOverdueCount++;
    }
}

集合、單筆資料與計數器都成功對回 Glossary:

  • dueIncompleteWorkItems 直接對應已到期且尚未完成的查詢條件。
  • newlyOverdueCount 對應本次才轉成 Overdue 的數量。
  • workItem 在單筆迴圈的小作用域裡已經足夠清楚。
  • NotificationSucceeded 原名能正確表達通知回傳結果,因此 Agent 選擇保留。

問題是 ProcessDueIncompleteWorkItemAsync 只說明輸入是哪一類 Work Item,沒有說明方法會標記狀態並嘗試通知。IsNewlyOverdue 看起來像目前狀態,實際上描述的是這次呼叫是否造成狀態轉換。

領域名稱適合什麼情境?

當 Repository 已經有成熟 Glossary,而且查詢、測試與契約對詞彙的定義一致,領域名稱能讓人與 Agent 使用相同語言。詞彙正確,仍不代表命名焦點正確;方法名稱還是要揭露呼叫端真正需要知道的責任。

領域詞彙回答「這筆資料是什麼」,方法名稱還要回答「這次呼叫會做什麼」。因此我採用 dueIncompleteWorkItemsworkItemnewlyOverdueCount,也保留原本準確的 NotificationSucceeded;描述錯誤焦點的私有方法與布林名稱則不採用。

最佳命名策略要同時考慮作用域、穩定意圖與 Repository 證據

三份 Production Diff 都只包含識別字 Rename,而且既有行為與公開契約的 Gate 全部通過,因此這次比較可以聚焦在命名差異。

Prompt 方向 符合 Clean Code 的部分 主要風險 適合使用的情境
短名稱 能利用附近語境,避免重複資訊 可能刪掉副作用與失敗語意 小作用域區域變數
解釋型長名稱 能揭露條件、狀態轉換與通知嘗試 可能把執行步驟寫死 呼叫範圍很小、穩定意圖明確的私有方法
領域名稱 能對回查詢、測試與 Glossary 可能使用正確詞彙,卻描述錯誤焦點 領域詞彙已經穩定的集合、計數與業務概念

AI 時代的 Clean Code 命名策略三圓交集

圖:命名策略需要同時取得作用域、穩定意圖與 Repository 證據的交集。

三個圓代表三種不同責任:精簡命名處理作用域,解釋型命名保留穩定意圖,領域命名提供 Repository 證據。真正能留下的名稱,通常不會只站在其中一邊,而是依識別字所在位置組合需要的資訊。

我把這次的選擇收成一句規則:

在目前作用域中,使用足以揭露穩定意圖的精簡名稱,而且每個詞彙都有 Repository 證據。

偏向任何一邊都有代價:Changed 太短,會漏掉狀態語意;把步驟全部塞進名稱,容易綁死實作;只用領域詞彙,則可能描述了輸入資料,卻沒有說清楚副作用。

先用人工校準一次,找出可以重複執行的命名判準

這份人工版本先建立一組校準樣本,後續 Agent 需要遵守的是判準,不必逐字複製名稱。

校準名稱 取自哪一份材料? Clean Code 判斷
dueIncompleteWorkItems 領域名稱實驗 集合名稱直接對應查詢資格
workItem 領域名稱實驗 單筆項目的小作用域已提供足夠語境
newlyOverdueCount 領域名稱實驗 計數器對應第一次狀態轉換
MarkOverdueIfNeededAndAttemptNotificationAsync 人工重組三份候選 IfNeeded 保留條件,Attempt 保留失敗可能,And 不固定執行順序
outcome 短名稱實驗 區域很小,後面的成員會補足具體語意
BecameOverdue 人工調整 描述本次呼叫發生的事件,避免被讀成長期狀態
NotificationSucceeded 原名稱 對應通知成功或失敗的布林結果

布林值的名稱要清楚區分目前狀態、本次事件、是否嘗試,以及最後是否成功。

校準後的呼叫端是:

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

var newlyOverdueCount = 0;

foreach (var workItem in dueIncompleteWorkItems)
{
    var outcome = await MarkOverdueIfNeededAndAttemptNotificationAsync(
        workItem,
        cancellationToken);

    if (outcome.BecameOverdue)
    {
        newlyOverdueCount++;
    }

    notificationAttemptCount++;

    if (!outcome.NotificationSucceeded)
    {
        notificationFailureCount++;
    }
}

newlyOverdueCount 是內部實作名稱,ProcessedCount 則屬於公開 Response。兩者暫時不同,是為了守住既有 JSON 契約;這項任務只授權內部重新命名。

人工版本固定在 Commit 1ea47990ee88c4c7932cfd6a68480b1a8164eb72/Tag day-05-meaningful-names

第四次實驗:Agent 對上五項判斷,User 仍得逐項核准七個名稱

前三次實驗幫我拆出命名偏差,當時 Repository 還沒有完整的自主命名政策。第四次實驗用兩個階段確認這些判準能不能被 Agent 理解:

  1. Agent 先讀取 Repository 證據,只提出候選、風險與推薦理由,不修改程式碼。
  2. User 校準有爭議的名稱後,Agent 才依核准對照完成重新命名。

七項命名決策中,Agent 有五項直接對上人工校準版本:dueIncompleteWorkItemsworkItemnewlyOverdueCountBecameOverdue,以及保留 NotificationSucceeded

剩下兩項差異集中在:

位置 Agent 推薦 User 校準選擇 判斷理由
私有方法 MarkAsOverdueIfNeededAndAttemptNotificationAsync MarkOverdueIfNeededAndAttemptNotificationAsync As 沒有增加新的決策資訊
小作用域結果 overdueResult outcome 方法名稱與結果成員已提供逾期語境

第二階段依核准清單完成重新命名,程式行為、公開契約與驗證結果都保持不變。收斂版本固定在 Commit f6a7986801ecc434eee926219c570fd83642b249/Tag day-05-sol-high-clean-code-synthesis-run-01

這次結果顯示,Agent 能依 Repository 證據直接對上五項人工判斷;剩下兩項仍需要 User 逐字選擇。這種兩階段流程適合政策尚未成形時做一次校準,但如果每次 Rename 都重跑,仍然省不掉逐項核准。

完整的雙階段 Prompt、Rename Decision Table 與驗證結果都保留在文末的公開 Evidence。

把人工校準結果寫成 Repository Naming Policy

延續昨天的做法,我把這次得到的命名判準整理成一份可放進 AGENTS.md 的 Repository Naming Policy。

Repository Naming Policy 是跟著程式碼一起版本化的命名決策規則,負責定義 Agent 可以自主命名的範圍,以及必須停止並交回 User 的情況。

這份政策把命名工作分成三種處理方式:

處理方式 代表什麼? User 何時介入?
安全通道 證據一致、只改內部識別字,而且行為與外部契約不變時,Agent 可以自主命名 不逐項核准,只做批次抽查
例外升級 遇到新領域詞、證據衝突、公開契約或業務決策時,Agent 停止修改並交回 User User 處理 Repository 無法排除的歧義
批次抽查 多項安全通道修改完成後,集中檢查 Policy 是否仍然有效 發現偏差時修正 Policy,而非逐字指定名稱

下面是依本篇實驗整理,並在第五次結果後重新校正的 Naming Policy 精簡版。讀者可以先放進 AGENTS.md,再依專案語言、領域與公開契約調整。這不是實驗當時的逐字版本;Agent 實際讀到的內容已固定在文末 Tag 與公開 Evidence。

## Naming Policy

### Naming Gate 1:格式慣例

- C# 類別、方法、Property 與公開成員使用 `PascalCase`。
- 參數與區域變數使用 `camelCase`。
- JSON、資料庫、URL、CLI 與 Branch 名稱沿用 Repository 既有的 `snake_case` 或 `kebab-case` 慣例,不把非 C# 格式套進 C# 識別字。
- 既有公開名稱不得只為統一格式而修改;需要變更時,必須先提出相容策略。

### Naming Gate 2:意圖與邊界

- 方法、區域結果與布林成員組成的呼叫端名稱,必須共同揭露責任、重要條件、副作用與成功/失敗語意;不要求每個識別字重複全部資訊。
- Boolean 必須區分目前狀態、本次事件、是否嘗試與是否成功。
- 名詞、動詞與限定詞必須能追到 Production Code、測試、契約或 Repository Glossary。
- 名稱描述穩定意圖,不把暫時步驟、實作方式或執行順序寫死。

### Agent 可以自主執行的範圍

同時符合下列條件時,Agent 可以自行選名、修改與驗證,不必逐項等待 User 核准:

- 只修改任務允許範圍內的內部識別字。
- 不改變控制流程、條件式、副作用、執行順序與產品行為。
- 不改變 HTTP Route、公開 DTO、JSON 欄位、資料庫 Schema、錯誤訊息或跨模組契約。
- 每個名稱都有一致且不衝突的 Repository 證據。

### 必須升級給 User 的例外

遇到下列任一情況,停止整批 Rename,不留下部分修改:

- 需要建立新的領域詞,或 Production Code、測試與契約對同一詞彙的定義互相衝突。
- 不同候選名稱代表不同業務規則、副作用承諾或相容策略。
- Rename 會影響公開 API、JSON、資料庫 Schema、跨模組契約或任務範圍外的檔案。
- 無法判斷名稱應表達狀態、事件、嘗試或成功。
- 必須改變方法責任或產品行為,才能讓名稱與實作一致。

### 完成時必須留下的證據

- Rename 前後對照與每個名稱的 Repository 證據。
- 實際 Diff,以及公開契約零非預期變更的確認結果。
- Repository 規定的 Build、Test、Format 與 Smoke 結果。
- 是否觸發升級條件,以及仍需 User 決定的問題。

備註:這次先把 Policy 放在 AGENTS.md,方便直接示範。等規則成熟後,更適合抽成可重複使用的 Skill;Repository 只保留專案自己的領域詞彙、格式差異與例外邊界。

第五次實驗:沒有目標名稱,Naming Policy 能不能讓 Agent 自主完成?

前四次實驗都包含人工校準。第五次改用大量 AI Coding 更需要的條件:不提供候選清單、目標名稱或 Rename 對照,只提供 Repository Naming Policy。

如果 Agent 能在安全範圍內自行取名,遇到證據衝突才停下來詢問,User 就不必審核每一個識別字。

你正在 Day 5 政策驅動自主命名實驗的隔離 Worktree。模型與起始程式碼固定。這次不會提供目標名稱、候選清單或逐項人工核准。

請先完整閱讀 Repository Instruction,尤其是 AGENTS.md 的「AI 自主命名政策」,再閱讀 WorkItemsController.ProcessOverdue、它呼叫的私有 Helper、逾期行為測試、通知 Gateway 與公開 Response 契約。

任務:依 Repository Naming Policy,自主審查並重新命名 ProcessOverdue 內部與該私有 Helper 相關的 C# 識別字。檢查集合、單筆項目、計數器、Helper、區域結果與 Tuple Boolean 是否符合安全通道及五項命名判準。

如果整批 Rename 都符合安全通道,直接選擇你能由 Repository 證據辯護的名稱並完成修改,不要逐項詢問 User,也不要等待名稱核准。如果命中任何一項「必須升級給 User 的例外」,停止整批 Rename,不留下部分修改,並回報衝突證據與需要 User 決定的問題。

硬性邊界:只能重新命名 WorkItemsController.cs 這段逾期流程的內部識別字。不得修改控制流程、條件式、方法內容、字串、XML 註解、HTTP Route、公開 DTO、JSON 欄位、錯誤訊息、測試、套件、Schema、產品功能、檔案或型別。不得新增或刪除方法。公開 ProcessOverdueResponse.ProcessedCount 與 JSON processedCount 必須保持不變。

修改後執行 Repository 規定的 Restore、Release Build、Test、Format、NuGet Audit、HTTP Smoke 與 git diff --check。完成報告要包含自主選定的 Rename 對照、每組名稱的 Repository 證據、是否觸發升級條件、實際 Gate 與剩餘風險。不要 Commit。

Agent 讀完查詢、測試、通知介面與 Response 契約後,判定整批重新命名位於安全通道,沒有向 User 詢問,直接產生:

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

var overdueStatusChangeCount = 0;

foreach (var workItem in dueIncompleteWorkItems)
{
    var overdueProcessingResult =
        await ApplyOverdueStatusAndAttemptNotificationAsync(
            workItem,
            cancellationToken);

    if (overdueProcessingResult.StatusChangedToOverdue)
    {
        overdueStatusChangeCount++;
    }

    notificationAttemptCount++;

    if (!overdueProcessingResult.NotificationSucceeded)
    {
        notificationFailureCount++;
    }
}

自主版本選了另一組同樣能通過 Policy 的名稱:

位置 人工校準版本 政策驅動自主版本 為什麼兩者都能接受?
狀態轉換計數 newlyOverdueCount overdueStatusChangeCount 都指向本次實際轉成 Overdue 的數量
小作用域結果 outcome overdueProcessingResult 前者利用附近語境,後者增加逾期限定;兩者都沒有誤導
私有方法 MarkOverdueIfNeededAndAttemptNotificationAsync ApplyOverdueStatusAndAttemptNotificationAsync 搭配回傳的 StatusChangedToOverdue 後,呼叫端仍能讀出狀態可能不變,而且通知只是嘗試
布林結果 BecameOverdue StatusChangedToOverdue 都描述本次發生的狀態轉換,不會被讀成目前狀態

這個版本維持相同控制流程、條件式、公開契約與產品行為,也通過相同驗證關卡。

我接受這份輸出,因為它通過相同命名判準。大量 AI Coding 不需要每次複製我的個人選字;只要名稱準確表達相同行為,並守住公開契約與停止條件,就可以留在安全通道。若團隊連 IfNeeded 是否必須明寫都有固定要求,就應繼續收緊 Policy。

Naming Policy 無法保證 Agent 百分之百符合每位開發者的習慣。它提供的是可重複的自主範圍、停止條件與批次抽查方式。

本次只驗證一次安全通道內的自主 Rename,還沒有長期資料可以判斷 Policy 使用一段時間後的偏差率,以及批次抽查能抓出多少問題。

Policy 固定在 Commit 409e8b8fd48b9d97b19c04513f8f6a7e40ce16f8/Tag day-05-naming-policy-v1;自主版本固定在 Commit 3b98bbd4b20b4863d1eb127f16e1f4cd996c5240/Tag day-05-sol-high-policy-driven-run-01

好命名的落點:讓 Agent 讀懂,也讓團隊能重複執行

Clean Code 讓我判斷名稱是否揭露穩定意圖、避免誤導、符合所在作用域,並使用有證據的領域語言。

E — Explicit Intent and Boundaries 意圖明確 則把這些判準轉成 AI 協作規則:User 先定義命名意圖、公開契約與停止條件,Agent 在安全範圍內自主取名,遇到新領域詞或證據衝突才交回人工決策。

五次實驗把差異拆清楚了:短名稱適合吃得到附近語境的區域變數;較長名稱適合需要在呼叫端揭露穩定責任的私有方法;領域名稱則必須有 Repository 證據。Naming Policy 的作用,不是指定唯一答案,而是讓 Agent 在這三項條件之間自行取捨。

這次自主實驗成功通過 Gate,但其他 Repository 仍要先建立自己的 Glossary、契約邊界與例外條件。好名稱不保證每次都減少 Token,卻能讓人與 Agent 少猜一點,也替下一次修改留下比較可靠的線索。

名稱說清楚之後,明天要接著檢查註解與格式:它們究竟補足了必要資訊,還是只把已經存在的 Code 再說一次?

實驗重現與完整證據

三種命名方向都從昨天接受的 Commit b7186425915b30aaa48cd81f60ecc1170a212082/Tag day-04-first-principles 出發。

如果已經 Clone 公開的 API Demo,可以執行:

git fetch origin --tags
git switch --detach day-04-first-principles
git rev-parse HEAD

最後一行應顯示:

b7186425915b30aaa48cd81f60ecc1170a212082

參考資料


上一篇
Day 4|AI 把函式拆小就算 Clean Code 嗎?從小、命名、組織與順序檢查重構結果
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言