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/**內部,代理可以構建一個適配器迷宮或給每個變量起兩個名字。如果合約和邊界成立,我就不必關心。

侷限性

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

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

結論

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

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

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