一個功能有三個入口,每個入口都自行決定重試、錯誤分類與去重。Agent 可以很快補上第四個入口,卻也可能帶來第四種處理方式。這時候,把共同決策收進模組,有明確的維護價值。

2026-10-03,Matt Pocock 在與 Lauren Tan(@poteto)交談後發表這則貼文,主張 AI 時代可以更積極設計抽象,並以嚴格 lint 限制使用方式。他也認為抽象能減少 token,做錯的抽象比較容易撤回。貼文沒有附對照實驗或成本數據;後兩點應視為待驗證的判斷。

本文延續原篇的深模組、接縫與領域語言主軸,補上這次觀點的驗收方法。我的立場是:**新增抽象的理由,是讓呼叫端少做一項容易做錯的決策。**類別、介面與目錄變多,並不自動達成這件事。

深模組要收走決策,薄包裝可能只多一次跳檔

John Ousterhout 在《A Philosophy of Software Design》討論以簡單介面封裝複雜實作的模組設計;他的作者網站也說明第二版加強了通用模組與深度的討論。這是軟體設計方法,並非針對 LLM 成本的量測。

套到 Agent 工作流,可以問一個很具體的問題:完成一次訂單操作,需要知道多少內部規則?

假設呼叫端必須自行選擇支付 SDK 方法、填入冪等鍵、解讀供應商錯誤,再決定是否重試。把相同參數轉交給 PaymentService,仍要求每個呼叫端理解這些細節,封裝就沒有完成。反過來,若模組接受已確認的訂單 ID,在內部統一處理重複請求與結果分類,呼叫端的責任就縮小了。

以下是設計草圖,沒有實作支付流程,也不是可直接上線的 SDK:

type CheckoutResult =
  | { kind: "confirmed"; orderId: string }
  | { kind: "rejected"; reason: "invalid-order" | "declined" }
  | { kind: "pending"; operationId: string };

interface Checkout {
  submit(orderId: string): Promise<CheckoutResult>;
}

這裡刻意保留 pending:外部請求逾時,不足以證明扣款失敗。模組需要有查詢與對帳策略,呼叫端則根據結果決定顯示什麼。這是本文的工程建議,不是 Matt 貼文中提供的支付設計。

這個介面值不值得存在,要看它承擔了哪些不變量。若每個呼叫端仍要重建冪等規則、辨識供應商例外,或偷偷讀取內部狀態,介面看似短小,實際決策面仍然很大。

嚴格 lint 必須同時指出合法入口

Matt 的建議有兩個部分:設計抽象,以及限制錯誤用法。只做前半,Agent 仍能跳過公開入口,直接引用內部 SDK。只做後半,錯誤訊息沒有替代路徑,工作可能停在反覆試錯。

下面用獨立的 JavaScript ESM 範例展示規則形狀。假設功能程式在 src/features/,支付實作在 src/payments/,公開入口是 @app/payments。這些名稱是本文示例,並非 CarlStack 的目錄或已安裝設定。

// eslint.config.mjs:示例設定,需依專案調整。
export default [
  {
    files: ["src/features/**/*.js"],
    linterOptions: { noInlineConfig: true },
    rules: {
      "no-restricted-imports": [
        "error",
        {
          paths: [
            {
              name: "stripe",
              message: "請經由 @app/payments 的公開入口操作支付。",
            },
          ],
          patterns: [
            {
              group: ["@app/payments/internal", "@app/payments/internal/**"],
              message: "內部實作不提供給 feature 使用。",
            },
          ],
        },
      ],
    },
  },
];

ESLint 的 no-restricted-imports 支援指定模組名稱、模式與提示訊息;flat config 的 files 控制套用範圍,noInlineConfig 可禁止檔案內註解改動 lint 設定。此例只管 feature,支付模組本身仍可使用 SDK。

這條規則有明確上限:它管靜態 import 字串,沒有涵蓋動態 import()、CommonJS require(),也不會替你解析所有相對路徑與 alias。../../payments/internal/client.js 就沒有落在上述模式內。這份設定不能宣稱封死所有入口,更不是安全沙盒。

如果專案需要完整依賴邊界,應補上能解析實際模組路徑的檢查,或增加針對那些語法的規則。每加一道檢查,都要確認合法公開入口仍能使用;不能只看禁止清單愈來愈長。

規則也要有正反例,業務契約另行驗收

先建立一份小型 fixture,讓規則的保證範圍可被重跑。不要把違規樣本混進 production 的 feature 目錄;測試可以對樣本文字指定虛擬檔名。

  • 合法靜態引用 @app/payments:應通過。
  • feature 直接靜態引用 stripe:應得到 no-restricted-imports 錯誤。
  • feature 引用 @app/payments/internal/client:應失敗。
  • 支付實作引用 stripe:不應被這組 feature 規則擋下。
  • feature 使用相對路徑、動態 import 或 require():上述單一規則不保證攔截;在導入完整邊界檢查前,將它們列為已知缺口。

這些是本文建議的驗收案例,不代表已在讀者的 repository 執行。專案採 TypeScript 時,還要配置相應 parser,不能把這份只匹配 .js 的設定直接當作 TypeScript 的完整防線。

**停止規則:禁止案例仍能通過,就不能宣稱該條邊界已受保護。**CI 也必須實際掃到目標檔案,並把錯誤當作合併阻擋;設定存在於 repository 不等於 gate 已啟用。

lint 綠燈只說明程式沒有命中已編碼的違規。它不會證明付款金額正確、冪等鍵沒有跨訂單碰撞,或逾時後能完成對帳。這些需要公開契約的行為測試。可以針對同一訂單重送、供應商逾時、明確拒付與後續確認結果,分別檢查狀態,而非只斷言某個 SDK 方法被呼叫。

型別則負責另一段工作。TypeScript 的 discriminated union 與 never 檢查能讓新增結果分支後,未補齊的處理路徑浮現。型別、lint 與行為測試各自提供有限的證據,不能互相代替。

接縫與領域語言,讓抽象有穩定的含義

原篇強調的 Seam 與 DDD,仍是設計公開契約時的兩個檢查角度。

接縫用來思考可替換的位置。例如同一支付契約由正式 adapter 與測試 fake 實作,呼叫端應該不必知道底層是哪一個。兩個實作能幫助暴露契約是否過度綁定供應商,但不應把「一定要有兩個 adapter」誤用為所有介面存在的必要條件。更完整的測試討論見〈Seam 與 In-Memory Fake〉。

領域語言則讓介面名稱對上業務動作。未付款的取消、已付款的退款、授權後的撤銷,可能有不同前提與狀態轉移;把它們全部命名為 cancel(),再以多個布林參數調整行為,會把決策推回呼叫端。

一份精簡的領域詞彙文件可以記錄「哪個狀態允許哪個操作」,再由型別與測試落實。文件本身不能強制 Agent 遵守;它的作用是讓設計者、使用者與測試作者說同一件事。語意工作可接著讀〈CONTEXT.md 與領域建模〉。

Token 與重構成本,要算完整任務

抽象可能讓 Agent 少讀內部檔案,也可能增加查找與理解成本。呼叫端只需讀公開契約時,前者較有機會成立;任務要求修改模組內部不變量時,Agent 仍然要讀實作與測試。不能用檔案變少、程式變短,直接推導 token 一定降低。

若團隊想驗證 Matt 的判斷,可以選擇可比較的任務,在相同模型、工具權限與驗收條件下,記錄總輸入輸出 token、失敗重試、人工審查時間與返工。不同架構的初次建立成本也要算進去;單次漂亮的結果,只能當下一次測試的線索。

「做錯再拆掉」也有邊界。尚未發布、沒有外部使用者的區域性 helper,通常比公開 API 容易替換。若抽象已寫入資料格式、跨服務協定或第三方整合,移除時還要處理相容性與遷移。Agent 能加速改碼,無法讓這些承諾消失。

下一次遇到重複實作,挑一項已經造成分歧的決策,寫成公開契約,再加一個合法案例與一個故意繞路的反例。兩者都按預期工作後,再評估是否值得擴大範圍。若需要安排整體 Agent 驗證與交付流程,可接著讀〈Agent 要能並行,先把工作變成可驗收結果〉。

若要往前檢查「需求本身是否定義正確」,可接著讀〈AI 越會寫程式,我們越需要學會抽象〉,從付款語意、可驗收契約與獨立商業案例檢查委派工作的前提。

來源與更新範圍

本次保留原篇 URL 與系列位置,補上可檢查的 lint 範圍與成本判準;原篇沒有實證支持的普遍效能與必然性敘述,改以條件與驗收案例表達。