AI News HubLIVE
站内改写4 分鐘閱讀

如何停止“保姆式”監管AI程式碼

LLM代理讓功能開發變得廉價,但帶來了架構腐化。透過將架構決策與實現分離,並用構建系統強制執行規則,開發者可以擺脫對代理生成程式碼的繁重審查,將精力重新聚焦於系統設計。

來源Hacker News AI作者: mgamma

LLM代理讓功能開發變得幾乎不費吹灰之力。你提出修改需求,代理便編寫程式碼、新增測試並執行。那一刻,未來看起來就像是“氛圍編碼”。

然而,賬單隨後就到。代理從最近的可用路徑匯入,讓前端程式碼直接訪問資料庫。這個選擇讓功能更容易實現,卻讓短期便利重寫了架構。

顯而易見的下一步是將規則寫下來。AGENTS.md檔案、規範和規劃文件都試圖在代理編寫程式碼之前將架構載入到其上下文中。它們有所幫助,但文字仍然是提醒而非約束。代理可能忘記規則,只遵守方便的部分,或在未滿足規則時聲稱成功。

程式碼審查成為了最後防線,這意味著檢查每個差異中是否有此類捷徑。這有所幫助,但將開發變成了繁瑣的監督工作。

解決之道是將架構決策與實現細節分離,然後新增檢查來強制實現尊重這些決策。一旦到位,審查就從生成的管道程式碼轉向長期存在的架構決策,如模組邊界、公共API和依賴關係。

規則需要安身之所

更深層的問題是記憶。人類攜帶社會情境向前推進。代理需要每個執行週期重新載入或強制執行這些情境。它們可能十次違反同一書面規則,仍將第十一次提醒視為新資訊。這就像僱傭了一個每天重置的極快實習生。

文件是有用的上下文,但對於專案無法承受被破壞的規則而言,它是一個糟糕的容器。上下文是稀缺的。指令相互稀釋。如果一個規則必須每次都被遵守,它應該存在於程式碼庫可以強制執行的地方。

成本曲線翻轉

軟體團隊已經在使用檢查。重要的規則離開文字,變成型別、測試、linter、構建檢查或執行時驗證。團隊通常只在違規常見或強制執行足夠重要時才提升規則。即便如此,他們也會使用標準lint預設和配置成本低的檢查。

一個自定義檢查器,能夠理解你的模組佈局、匯入邊界以及你對公共介面的理解,需要前期巨大的工程投入,而回報卻不確定。標準lint預設和作為質量後盾的程式碼審查通常是更好的權衡。

編碼代理在兩方面改變了經濟性。

  • 它們比人類審查速度更快地產生架構漂移。
  • 它們使定製檢查器的成本低到可以在一個下午內完成原型。

漂移的來源現在有助於遏制它。

以下是一個小例子。代理喜歡將簡單引數包裝成儀式。

而不是:load(id: string)

你得到一個微小應用程式形式:load(input: { id: string })

在指令檔案中告訴代理“不要這樣做”暫時有效。讓代理編寫一個檢查,在公共介面中拒絕該模式,我只花了幾分鐘。從那時起,檢查處理提醒,構建失敗直到代理修復它。

該模式很容易擴充套件到真正重要的規則。前端不能匯入資料庫模式?那是一個匯入邊界檢查。不希望驗證邏輯存在於UI中?那是一個原始碼檢查。一旦規則成為檢查,失敗是確定的,錯誤資訊命名規則,代理在我看到差異之前修復自己的違規。

外部合約,內部生成程式碼

在我的專案中,本地規則成為構建系統中的檢查。它們位於包原始碼之外,並按它們所管理的程式碼進行分組,例如UI原語、UI元件和領域包。

以領域包為例。它主要是攜帶專案業務邏輯的普通TypeScript程式碼。邏輯被拆分成小模組,具有狹窄的公共介面。每個模組遵循強制執行的結構。

some-module/ 原始碼檔案按審查職責拆分。

  • contract/ — 權威檔案

* interface.ts — 僅公共API,無實現 * create.ts — 暴露模組建構函式,宣告每個必需的依賴 * cases.ts — 定義行為示例作為測試,透過create.ts構建模組,用假依賴對真實模組進行測試

  • impl/ — 代理編寫的實現程式碼

架構決策存在於權威檔案中。manifest.jsonREADME.mdcontract/**定義了模組的意圖、公共介面、依賴和行為示例。

實現細節保留在impl/**中。它們必須滿足合約並保持在匯入邊界內。

對權威檔案的更改可以改變專案的形狀,並且需要重新生成impl/**。對impl/**的更改應保持本地化。如果超出該邊界,檢查將失敗。

檢查存在於構建系統中,在模組外部,強制執行這些角色。

規範系統檢查

一些示例檢查:

  • interface.ts: 僅公共API,無實現
  • create.ts: 暴露模組建構函式,宣告每個必需的依賴
  • cases.ts: 定義行為示例作為測試,透過create.ts構建模組,用假依賴對真實模組進行測試
  • impl/**: 只能匯入create.tsinterface.ts,不能使用環境能力如Datecrypto

目標是使impl/**成為一個封閉的盒子。如果合約檔案規定良好,生成的程式碼仍然可能出錯,但混亂保持在實現目錄內。

這使得審查可以集中在權威檔案上,那裡是持久決策所在。

依賴關係

依賴是設計決策。生成的實現不應自行授予自己呼叫網路、觸控儲存、讀取時鐘或生成ID的許可權。

在我的專案中,impl/**中的程式碼不能直接呼叫Date.now()fetchlocalStoragecrypto.randomUUID()。如果一個模組需要時間、網路、儲存或ID,該設計決策會透過將依賴宣告為contract/create.ts中的建構函式引數來記錄。

這將審查轉向正確的問題。我不再問代理是否正確使用了localStorage,而是問這個模組是否應該具有永續性。

同樣的模式適用於模組邊界。如果一個模組需要另一個模組,這種依賴也必須顯式。當生成的程式碼跨越該邊界時,失敗應指向規則。

例如,代理可能使order/impl/price.ts深入目錄實現:

// order/impl/price.ts
import type { OrderDeps } from "../contract/create";
import type { PriceQuote, PriceRequest } from "../contract/interface";
import { catalogStore } from "../../catalog/impl/store";

export function priceOrder(_deps: OrderDeps, request: PriceRequest): PriceQuote {
  const item = catalogStore.lookup(request.sku);
  return { amount: item.price * request.quantity };
}

檢查在差異到達審查之前在終端中失敗:

$ repo check

error import-boundary
order/impl/price.ts imports ../../catalog/impl/store
impl/** may only import:
../contract/create
../contract/interface

修復方法是將目錄依賴作為合約的一部分,並從模組外部傳入:

// order/contract/create.ts
import type { CatalogStore } from "../../catalog/contract/interface";
import type { OrderService } from "./interface";

export type OrderDeps = {
  catalog: CatalogStore;
};

export declare function createOrder(deps: OrderDeps): OrderService;

之後,impl/**使用deps.catalog而不是直接匯入目錄實現。

行為

型別和方法簽名定義形狀,但留行為於解釋空間。合約案例透過可執行示例填補這一空白。實際上,這些檢查就是普通的單元測試。

contract/cases.ts編寫時,公共介面已存在於interface.ts中。每個依賴,包括環境能力如時間、儲存和ID,必須在contract/create.ts中宣告。這意味著案例可以在impl/**存在之前編寫。它們首先失敗,然後指導生成。

模擬也變得更簡單。測試不會修補隱藏在實現中的時鐘、儲存或網路呼叫。它透過建構函式傳遞這些能力,與生產程式碼相同。

如果實現發生變化,案例固定住合約的含義。如果含義必須改變,案例首先改變。

審查

審查轉向值得人類關注的決策。結構使高影響更改難以隱藏,因為公共形狀、依賴和行為示例都生活在我期望審查的檔案中。

這改變了我提出的問題。這個模組是否有單一目的?依賴是否被掙得?公共形狀會隨著時間變化而保持良好嗎?示例是否覆蓋了重要行為?

impl/**內部,代理可以構建一個介面卡迷宮或給每個變數起兩個名字。如果合約和邊界成立,我就不必關心。

侷限性

即使有代理幫助,一些規則仍然太難或太昂貴而無法良好編碼。一個變得複雜、脆弱或充滿異常的檢查可能產生比節省更多的維護工作。值得強制執行的規則是執行成本低、易於理解且足夠穩定以每次應用。

檢查需要仔細設計和有用的錯誤資訊。如果錯誤模糊,代理會繞過檢查。這會將強制執行變成打地鼠。好的檢查需要清晰的規則、精確的資訊以及供人類審查的逃生艙口。

結論

程式碼生成使編寫軟體更便宜。審查生成的程式碼正在成為瓶頸。如果工程師的注意力是稀缺資源,它應該花在塑造系統的決策上。

強制層是一個關於重複工作的賭注。只有當代理足夠頻繁地返回相同邊界,並透過節省審查注意力來償還前期成本時,前期成本才有意義。

這種方法消除了審查代理生成程式碼的大量乏味工作。它把我的精力放回工程系統本身:塑造合約、選擇邊界,並決定哪些部分應該允許相互依賴。工作感覺更少像保姆,更多像駕駛。