范到發(fā)布:用SDD駕馭AI協(xié)作開發(fā)npm工具包的實踐)
老實說我一開始對讓 AI 寫代碼這件事已經(jīng)有點審美疲勞了。不是不好用是太容易翻車需求說一半AI 自由發(fā)揮一半改了三輪才發(fā)現(xiàn)最初的方向就偏了。直到我試著用 SDDSpec-Driven Development規(guī)范驅(qū)動開發(fā)配合 AI 做一個小工具——一個用于文案排版的 npm 包整個協(xié)作方式才突然順了起來。寫規(guī)范、拆任務(wù)、生成代碼、跑測試、發(fā)布包每一步都有明確的驗收標準AI 不再猜需求我也不用反復(fù)打補丁。這篇文章就把這套流程、踩過的坑、以及最終如何把包發(fā)到 npm 的完整過程記錄下來希望能給同樣在摸索 AI 協(xié)作開發(fā)的人一點參考。1. 把 AI 協(xié)作從聊天寫代碼升級成按規(guī)格施工1.1 對話式編程為什么經(jīng)常失控我以前用 AI 寫代碼的路徑基本是打開對話窗口描述一個功能等它吐出一大段實現(xiàn)然后復(fù)制到項目里跑。一開始確實驚艷但規(guī)模稍微上來就露餡了。比如我想做一個文本排版函數(shù)需求是把中英文之間加上空格AI 第一版只用一行正則替換。跑起來才發(fā)現(xiàn)問題URL 里的AI被打上了空格代碼塊內(nèi)部的縮進被清掉全角半角標點混在一起時輸出完全不可控。問題不在 AI而在需求本身。自然語言是有歧義的中英文之間到底指什么是指中文字符和 ASCII 字母之間還是單詞與單詞之間加空格要加在哪個位置遇到鏈接、代碼塊、引號嵌套時怎么處理如果這些邊界條件沒有定義AI 就只能根據(jù)概率給你猜一個答案。而這個猜的過程恰恰是不可控的根源。1.2 SDD 的六步閉環(huán)后來我在整理自己項目經(jīng)驗時把以前做需求分析的習慣提煉成了一套流程恰好在社區(qū)里也看到不少人把它叫作 SDD。核心不是新概念就是把開發(fā)順序從先寫代碼再補測試變成先寫規(guī)范再寫實現(xiàn)。我個人實踐中固化下來的六步是定義目標用一段話描述要解決的問題不給任何實現(xiàn)方案。編寫規(guī)范把目標拆成可驗證的行為規(guī)則每個規(guī)則都附輸入、輸出示例和反例。拆解任務(wù)按依賴關(guān)系把規(guī)范拆成小任務(wù)每個任務(wù)有明確的驗收標準。AI 實現(xiàn)把單個任務(wù)交給 AI帶上規(guī)范和測試用例讓它只做這一件事。人工審查跑測試、讀代碼、對照規(guī)范逐條確認而不是直接信任輸出。收斂迭代把 review 中發(fā)現(xiàn)的新邊界情況變成新的規(guī)范條目回到第 2 步。這個流程放在 AI 協(xié)作里尤其順手因為 AI 最擅長的是給定明確規(guī)則后生成實現(xiàn)而不是自己理解復(fù)雜模糊的業(yè)務(wù)場景。把業(yè)務(wù)判斷拿回人手里把模式識別交給 AI分工就清晰了。1.3 為什么拿排版工具來練手選排版工具當實驗品是因為它的輸入輸出足夠明確非常適合驗證 SDD 流程。我給你隨便丟一段中文廣告文案里面有中英文混排、全角半角標點混用、多余空格排版規(guī)則是要把它們處理成統(tǒng)一風格。這件事的規(guī)則看起來主觀但只要把規(guī)則逐條定義清楚就完全可以自動化。它不像生成一個電商系統(tǒng)那樣宏大模糊也不像寫一個排序算法那樣沒有業(yè)務(wù)味道恰好處于 AI 能搞定和人類要拍板的中間地帶。于是就有了 typo-clean 這個包一個純 TypeScript 實現(xiàn)的文本排版函數(shù)庫附帶一個簡單的 CLI專門處理中文文案里的空格、標點、引號這類常見排版問題。2. 排版包的規(guī)范拆解哪些規(guī)則可自動化哪些必須放棄2.1 需求到規(guī)范的第一步先定邊界寫規(guī)范的第一步不是寫規(guī)則而是定義這工具不做什么。我給 typo-clean 定的邊界很明確它只處理純文本和 Markdown 文本不碰 PDF、不排版圖片、不做段落重排。這個邊界看著像廢話但非常關(guān)鍵。因為一旦邊界不清晰AI 在實現(xiàn)時就會自作主張往里面塞功能比如給你輸出一個 HTML 解析器或者把換行全改成段落標簽。邊界定完后才開始定義核心規(guī)則。我給整個工具定了 8 條初始規(guī)則全部圍繞中文文案排版規(guī)則編號規(guī)則名稱示例說明R1中西文之間加空格AI賦能 → AI 賦能在 CJK 字符和 ASCII 字母/數(shù)字之間插入空格R2統(tǒng)一標點形式你好,world → 你好world中文語境使用全角逗號、句號R3引號整理AI → AI中文語境使用彎引號但代碼塊內(nèi)不處理R4壓縮重復(fù)標點太好了!!! → 太好了連續(xù)重復(fù)標點只保留一個R5清理行尾空白你好 \n → 你好\n去除每行末尾多余空格R6數(shù)字與單位不拆5G網(wǎng)絡(luò) → 5G 網(wǎng)絡(luò)5G內(nèi)部不加空格但后面緊跟中文時加空格R7保護代碼塊\nconst a1\n 原樣保留代碼塊內(nèi)部不應(yīng)用任何規(guī)則R8保護 URL訪問https://example.com → 訪問 https://example.comURL 作為整體內(nèi)部不加空格這里最反直覺的一條是 R8。常規(guī)正則替換很容易把 URL 內(nèi)部的連字符、點號、斜杠都當成邊界處理結(jié)果把鏈接搞得支離破碎。所以規(guī)范里必須明確URL 是不可分割的整體處理時先提取保護處理完再放回去。2.2 每條規(guī)則都得有正例和反例規(guī)范文檔里只有規(guī)則描述是不夠的AI 和人都會對自然語言產(chǎn)生不同理解。我給每條規(guī)則都配了至少一個正例和一個反例。正例說明什么情況要處理反例說明什么情況不要處理。R1 的反例就很有意思英文單詞內(nèi)部不能加空格Transformer 不能被處理成 Trans former再有就是 Markdown 標題語法后不能加##AI 應(yīng)該變成 ## AIATX 標題標記和標題文本之間需要空格但這種空格是 Markdown 結(jié)構(gòu)性空白不是排版規(guī)則該管的。我干脆在規(guī)范里寫死R1 只處理 CJK 字符和 ASCII 字母/數(shù)字相鄰的邊界不處理字符串內(nèi)部、不處理標記符號。這樣一個一個摳出來規(guī)范文檔寫到第 35 條時已經(jīng)非常長了但每條規(guī)則長度只有一兩行示例。事實證明這部分投入非常值A(chǔ)I 后續(xù)生成的代碼幾乎不需要返工。2.3 從規(guī)范直接生成測試用例規(guī)范里的正例和反例直接就是測試用例。我把 spec.md 放在倉庫根目錄用 Markdown 表格維護規(guī)則然后用一個腳本把這些示例對解析成 Vitest 測試。也就是說改規(guī)范表格里的任何一行示例測試代碼就會自動變化。這樣做的好處是規(guī)范永遠和測試同步AI 拿到的驗收標準就是測試本身不存在文檔說一套測試測另一套的情況。3. 六步 SDD 實操記錄從規(guī)范文件到第一版提交3.1 目標定義和任務(wù)拆解項目啟動時我只寫了一段話放在 README 頂部目標是提供一個 npm 庫能讓開發(fā)者輸入一段中文/中英混排的文案輸出經(jīng)過統(tǒng)一排版規(guī)則的文本。用戶主要通過函數(shù)調(diào)用或簡單 CLI 使用不需要圖形界面。排版規(guī)則以 spec.md 為準。這段話后來基本沒有改過它給整個項目定住了風向。AGENTS.md 里有一段更長的描述但核心就是這幾句AI 每次讀上下文時先看到這個不會跑偏。任務(wù)拆解則是按照依賴順序核心格式化引擎輸入字符串輸出處理后的字符串內(nèi)部按規(guī)則順序執(zhí)行。保護機制先把 URL、代碼塊、行內(nèi)代碼提取出來處理完再還原。規(guī)則實現(xiàn)每 2-3 條規(guī)則一組一個任務(wù)一個分支。CLI 入口用 Node.js 解析命令行參數(shù)讀取文件或標準輸入。打包發(fā)布生成類型聲明配置 package.json 的入口文件驗證發(fā)布結(jié)果。這個順序很重要。一開始執(zhí)行保護機制后面的規(guī)則就會在安全環(huán)境里工作極大降低正則誤傷概率。3.2 AI 實現(xiàn)階段一條任務(wù)一個合并請求到 AI 實現(xiàn)階段我不再讓它一口氣生成整個項目。每一個子任務(wù)拆出來時都已經(jīng)帶著對應(yīng)的測試文件。比如 R1 的實現(xiàn)任務(wù)我先提交了一個只有測試的文件測試里寫著AI賦能 → AI 賦能然后把 spec.md 片段一起提供給 AI讓它只去實現(xiàn) typoCleanR1 這個函數(shù)。AI 第一次給出的實現(xiàn)是function typoCleanR1(text: string): string { return text.replace(/([\u4e00-\u9fff])([A-Za-z0-9])/g, $1 $2) .replace(/([A-Za-z0-9])([\u4e00-\u9fff])/g, $1 $2); }這個版本看起來沒問題但只過了最基本的測試。真正的問題出現(xiàn)在組合場景當字符串里同時包含 URL 和中文時比如訪問https://example.com/AI助手它會先經(jīng)過 R1把AI助手處理成AI 助手但 URL 里的com/AI也會被處理成com/ AI并不會因為com/后是A前面是m/是 ASCII 和斜杠相鄰R1 不會觸發(fā)??蓡栴}出在別的地方如果 URL 里包含了形如zh-cn的段R1 的正則并不會觸碰連字符但后面 R8 保護邏輯還沒生效時R1 就已經(jīng)在里面改了不該改的東西。這個問題的根源是規(guī)則之間的順序耦合。我最后決定在架構(gòu)層把所有保護邏輯提前先提取 URL 和代碼塊保留位置占位符再執(zhí)行 R1 到 R6最后還原。這個改動既不是 AI 主動提出來的也不是我拍腦袋拍出來的而是在 review 階段對照規(guī)范時發(fā)現(xiàn) R1 和 R8 存在沖突后做的設(shè)計決策。3.3 人工審查時我具體看什么很多人讓 AI 寫完代碼就直接合并最多跑一遍測試。但我的經(jīng)驗是測試通過只說明規(guī)則被滿足不代表實現(xiàn)方式是對的。我會重點看三個地方是否修改了規(guī)范之外的行為比如 AI 可能順手把換行符統(tǒng)一成 LF這在 Windows 環(huán)境下屬于越權(quán)必須禁止。正則是否有潛在的災(zāi)難性回溯排版工具可能跑在長文本上一個復(fù)雜度很高的正則就能拖垮頁面。規(guī)則之間有沒有隱式依賴比如先替換標點還是先加空格順序不同結(jié)果可能完全不同。這些審查點我在第一次使用 AI 時踩過后來全部寫進了審查清單每次提交代碼時按清單過一遍效率很高。4. 最容易翻車的三處實現(xiàn)細節(jié)鏈接保護、成對符號和邊界空格4.1 預(yù)處理領(lǐng)先于一切規(guī)則typo-clean 的最終架構(gòu)和大多數(shù)人想象的一堆正則順序執(zhí)行不同。我采用了兩階段處理export function cleanTypo(text: string): string { const tokens: Token[] []; const content protectTokens(text, tokens); const cleaned applyRules(content); return restoreTokens(cleaned, tokens); }protectTokens 負責把所有代碼塊、行內(nèi)代碼、URL、以及不需要處理的占位符提取出來替換成不可見字符加索引的形式。applyRules 在安全文本上操作restoreTokens 最后把原始內(nèi)容塞回去。這個設(shè)計的直接收益是URL 內(nèi)部的字符永遠不會被誤處理。URL 里出現(xiàn)的下劃線、斜杠、點號在保護階段就離開了規(guī)則作用域。這個方案其實不復(fù)雜代碼量也很小但是結(jié)構(gòu)化地解決了 AI 原生實現(xiàn)里最容易出現(xiàn)的問題。后來我測試了 100 個真實 URL 和 20 個代碼塊零誤傷。4.2 成對符號的狀態(tài)處理另一個翻車點是引號。中文排版規(guī)范里彎引號要成對出現(xiàn)左引號和右引號不能搞混。簡單的正則替換會把所有直引號變成同一個方向的彎引號輸出像這是一個 測試 文本里的引號方向完全錯亂。AI 在這個問題上的第一版實現(xiàn)是text text.replace(//g, \u201C); // 全部變成左引號這版跑測試直接紅了因為規(guī)范里寫了右引號對應(yīng)的用例。實現(xiàn)正確邏輯需要跟蹤狀態(tài)遇到一個引號時判斷它是左還是右取決于上一個引號的狀態(tài)、上下文是否為中文引用的開頭。我最后用了一個雙向狀態(tài)標志來解決代碼大致是let open false; let result ; for (const ch of text) { if (ch ) { result open ? \u201D : \u201C; open !open; } else { result ch; } }這段代碼是 AI 生成的但我在審查時發(fā)現(xiàn)一個缺陷如果文本是從中間截斷的或者引號數(shù)量本身是奇數(shù)會出現(xiàn)方向錯位。于是我在規(guī)范里加了一條當引號數(shù)量不配對時保留原始字符不強行轉(zhuǎn)換。這屬于典型的規(guī)范越細AI 輸出越穩(wěn)的例子。4.3 邊界空格不是越細越好加空格的規(guī)則里數(shù)字與單位不拆是個很微妙的邊界。規(guī)范定義數(shù)字緊隨英文字母時數(shù)字和字母之間不加空格但在數(shù)字單位英文字母整體和中文相鄰時整體作為單詞加空格。5G網(wǎng)絡(luò) → 5G 網(wǎng)絡(luò)但5G內(nèi)部不加空格。AI 在這里給出的正則方案是text.replace(/([0-9])([A-Za-z])/g, $1 $2)直接把5G拆成了5 G這是我一開始最擔心的誤傷。后來在規(guī)范里明確數(shù)字與字母之間是否存在空格依據(jù)字典表判斷并為常見的單位G、GB、KB、cm、mm、Hz 等建立了一張白名單。這個表不長但解決了 AI 無法通過上下文準確判斷的行業(yè)術(shù)語問題。這里收獲很大排版規(guī)范有些部分需要精確到字符級別但也有些部分需要放到領(lǐng)域知識層面不是 AI 從文本模式中就能學出來的。5. 發(fā)布 npm 包的過程比寫代碼更折騰PowerShell、鏡像源和 files 字段5.1 Windows 環(huán)境下的 npm.ps1 執(zhí)行策略問題寫完代碼只是萬里長征第一步發(fā)布 npm 包時我才發(fā)現(xiàn)本機環(huán)境一堆問題。最常見的那個報錯就是npm : 無法加載文件 C:\Program Files\nodejs\npm.ps1因為在此系統(tǒng)上禁止運行腳本這是 PowerShell 的執(zhí)行策略限制了 .ps1 腳本運行而 npm 的 Windows 安裝包默認是通過 npm.ps1 調(diào)用的。解決辦法不是關(guān)掉整個執(zhí)行策略而是只針對當前用戶放開Set-ExecutionPolicy -Scope CurrentUser RemoteSigned這里要注意別順手寫成Set-ExecutionPolicy Unrestricted那會把機器置于不可控狀態(tài)。如果你在公司電腦上最好先和本地安全策略確認一下避免影響其他腳本的權(quán)限審計。5.2 鏡像源過期導致證書報錯發(fā)布前我想先跑一輪 npm install 驗證依賴結(jié)果屏幕上出現(xiàn)了一堆npm ERR! code CERT_HAS_EXPIRED。仔細看錯誤信息里面訪問的是https://registry.npm.taobao.org/...。這其實是一個歷史遺留問題早前我為了提速把 registry 切到了淘寶鏡像但老地址的證書已經(jīng)過期。新版鏡像源已經(jīng)遷移到了https://registry.npmmirror.com。排查方式很簡單先看當前 registrynpm config get registry然后統(tǒng)一改成新版鏡像源或官方源npm config set registry https://registry.npmmirror.com如果你用的是公司內(nèi)部 Nexus 倉庫同理需要確認證書鏈是否完整。這里給出的經(jīng)驗和發(fā)不了包的關(guān)系很多人要等到發(fā)布時才能體會到。5.3 發(fā)布前先用 npm pack 檢查包內(nèi)容真正執(zhí)行npm publish之前我建議先跑一遍npm pack --dry-run這個命令會列出最終打到 npm 包里的所有文件。我在這一步發(fā)現(xiàn)了兩個問題一是src/目錄也被打包進去了而我并不想讓消費者看到 TypeScript 源碼二是spec.md沒有被包含但我想讓使用者能查看規(guī)則說明。解決方案是在 package.json 里顯式配置 files 字段files: [ dist, spec.md, README.md ]files 字段是白名單機制只要寫清楚就不會把任何多余文件帶進去。同時確認main指向 dist/index.jstypes指向 dist/index.d.ts。這一步不做好最容易出現(xiàn)的情況就是包發(fā)上去了別人import進來卻是 undefined。5.4 發(fā)布后必須做的冒煙測試發(fā)布成功不代表萬事大吉。我會在另一個干凈目錄里重新安裝一遍npm i -g typo-clean echo AI賦能 | typo-clean輸出正確后再用 Node 跑一次函數(shù)調(diào)用node -e const { cleanTypo } require(typo-clean); console.log(cleanTypo(AI賦能))兩次都通過才說明這個包在真實環(huán)境里可用。這個流程看起來繁瑣但能篩掉一大半類型聲明挺好跑起來報錯的尷尬情況。實測下來發(fā)布一個純 TypeScript 寫的工具包最容易出問題的往往不是業(yè)務(wù)邏輯而是exports字段沒配好導致 ESM 和 CJS 環(huán)境下的加載行為不一致。6. 嘗到甜頭之后我現(xiàn)在怎么安排 AI 與人工的分工6.1 AI 負責模式識別人負責邊界定義這個項目做完后我最大的感觸是AI 的能力邊界不在代碼生成而在業(yè)務(wù)邊界的判斷。AI 可以寫出非常漂亮的實現(xiàn)但它不知道數(shù)字單位白名單該包含哪些詞不知道 URL 在中文排版里應(yīng)該整體保留更不知道彎引號不配對時就保留原樣這種面向異常的策略。這些邊界定義必須由人來完成而且越早寫進規(guī)范后面返工越少。SDD 的價值就在于它逼著你在編碼之前把這些邊界想清楚。沒有這套流程時我可能寫完正則才開始想邊界結(jié)果被各種誤傷追著跑。有了規(guī)范之后AI 的實現(xiàn)質(zhì)量明顯提升review 的時間反而大幅縮短。6.2 我還在堅持的幾條習慣項目結(jié)束后我把這套流程沉淀進了自己的項目模板現(xiàn)在已經(jīng)成了固定習慣每個項目的根目錄都有一份 spec.md所有業(yè)務(wù)規(guī)則的唯一權(quán)威來源。任何 AI 產(chǎn)生的代碼變更必須對應(yīng)一個測試用例不能只有代碼沒有測試。規(guī)則的分裂或修改先改 spec再改代碼順序不能反。每次 AI 生成了意料之外的好方案我會回填進規(guī)范作為參考實現(xiàn)知識庫越攢越多。這個習慣給我?guī)淼闹苯幼兓茿I 生成代碼的采納率從原來的不到一半提高到了八成以上。剩下的兩成不是代碼質(zhì)量問題而是業(yè)務(wù)邏輯本來就沒定義清楚。說到底AI 并不是替你寫代碼而是替你執(zhí)行你已經(jīng)寫好的規(guī)范。規(guī)范寫得越清晰AI 協(xié)作的體驗就越接近靠譜同事而不是猜謎選手。做 typo-clean 這個排版包只是個小項目但驗證下來的這套協(xié)作范式我覺得值得每個人都試一次。