iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0

模組二|工程底座與可驗收的 AI(Day 5–9)

《一條線救一隻狗》是我用 PixiJS 和 Matter.js 做的一款網頁小遊戲:玩家畫一條線,讓狗安全落地。畫面上的三十幾張 SVG 素材,全部分成好幾批交給 AI 生成。本篇談的就是這件事——怎麼讓分批生出來的圖,看起來像同一個人畫的。

先講清楚我不會寫什麼。這個專案沒有留下風格漂移的中間版本,所以我不會給你「你看,第一版歪成這樣,加了約束之後就對了」的前後對照圖——那些檔案已經流失,我不編。本篇講的是約束本身怎麼設計,證據是規格檔、驗證器與素材檔的現況,不是失敗紀錄。

結論先講:風格一致性不是用形容詞達成的,是靠三層具名約束達成的——顏色有名字、結構有數字、圖層有語意 ID。凡是無法被機器指名的風格要求,都會漂移。


「幫我畫一隻狗」為什麼一定會失敗

PRD.md 有一節叫〈常見生成錯誤與修正〉,十二列,第一列是:

錯誤 原因 修正 Prompt
每張狗狗長得不同 模型只看到文字描述,沒有母版約束 「以 dog-idle.svg 為唯一造型母版,只允許變更列出的節點」

這裡要標一件事:這張表是在生成任何一張素材之前寫的。 它在專案第一個 commit(b4c2c62,08-06 03:07,當時檔名還是 deep-research-report.md,03:12 才更名為 PRD.md)就存在,而第一批 SVG 進 repo 是 04:30 的 dc5bed3。所以它不是我的踩坑記錄,是我開工前抄下來的預期清單。真假由讀者自己判斷——我能保證的只有「這張表存在的時間早於素材」這件可以用 git log 查的事。

至於為什麼「幫我畫一隻狗」注定不穩定,原因 Day 2 說過一次:那句話裡沒有任何可以被判定的東西,所以你沒辦法說第三隻比第二隻「錯」。今天要處理的是它的反面——要怎麼寫,才能讓「錯」變成一個有位址的字。


第一層:色票不是顏色,是名字

這兩盞一樣亮,但因為名字不同,我才能只關掉其中一盞

docs/art-style.md 的色票表現在有 17 個 Token

| Token | Color | Usage |
| --- | --- | --- |
| OL | #3E3440 | Main outlines, bee dark details, UI strokes |
| DOG | #F6B84A | Dog fur |
| MUZZLE | #FFF3DA | Dog muzzle and chest |
| COLLAR | #3A8DDE | Dog collar |
| BEE | #FFD84D | Bee body and stars |
| HIVE | #C98035 | Hive and wood elements |
| SKY | #8ED8F8 | Sky and light wings |
| GRASS | #7AC943 | Grass and bushes |
| SUCCESS | #45C486 | Success UI |
| DANGER | #F45B69 | Failure and hazard UI |
| UI | #30334A | Dark interface elements |
| WHITE | #FFFDF6 | Highlights and light UI |
| SOIL | #7B4A2F | Cavern walls, pillars and floor |
| CAVE | #FFF3DA | Lit cavern interior (same value as MUZZLE) |
| STONE | #8B91A1 | Stone and spikes |

(省略最後兩列 WATERLAVA。這張表會被 scripts/validate-svg.js 直接解析,機制 Day 2 講過,今天只用它的內容。)

CAVE 那一列是我認為整張表最值得講的一列:它的色碼跟 MUZZLE 一模一樣,都是 #FFF3DA,而它們是兩個不同的 Token。

如果命名的單位是顏色,這兩個應該合併——同一個色碼寫兩次是重複。但命名的單位是用途:狗狗的嘴鼻與洞穴的內壁現在剛好一樣亮,不代表以後也要一樣。有兩個名字,你才有機會只改其中一個;只有一個名字,任何調整都會波及到不相干的地方。

這張表也不是一次寫完的。開工時是 15 個 Token;08-06 17:58 的 24a8d1c 把地形從開闊平地改成洞穴,同一個 commit 加了 SOILCAVE 兩個【實測:git log -S'SOIL' -- docs/art-style.md】。新增顏色的動作被強制發生在色票檔裡,而不是發生在某一張 SVG 裡——這就是把顏色收攏成 Token 表換到的東西。


第二層:結構規範,全部是數字

第二層管的是「畫布長怎樣」,特徵是每一條都能寫成斷言

項目 規定
根節點 必須有 xmlns、指定的 viewBoxpreserveAspectRatio="xMidYMid meet"
viewBox 家族 角色/道具 0 0 256 256、蜜蜂/圖示 0 0 128 128、平台/危險物 0 0 512 128、按鈕 0 0 320 96
主輪廓寬度 128 單位 5–7、256 單位 8–12、320×96 按鈕 6–10、512×128 平台 8–14
安全邊距 所有可見圖形距 viewBox 邊界至少 6%
文字 按鈕 SVG 只放背景與陰影,文字由 PixiJS Text 疊上去

CLAUDE.md:130 還多寫了一條「同角色不同狀態必須同尺寸同構圖」。這條特別重要,因為它防的是一個很難用眼睛抓到的 bug:角色切換狀態時會跳一下docs/asset-inventory.mddog-idle 的顯示尺寸是 112 × 112、dog-happy 是 120 × 120——尺寸可以不同,但 viewBox 都是 0 0 256 256,而 PRD 給的修正提示詞寫得更死:「保持頭部中心、腳底與陰影座標完全一致」。

最後一列是我後來覺得最划算的一條。按鈕 SVG 裡不放文字,代表按鈕素材可以重複用在不同語言、不同文案上,也代表 AI 不用去處理字型——而字型正是它最容易偷懶引用外部 URL 的地方(昨天那條 Never 列的十項之一)。


第三層:語意 group ID —— 為了讓「一致」變成可檢查的

九個格子少一個就過不去,托盤外面多黏的那幾塊反而沒關係

這一層最反直覺。

docs/asset-inventory.md 每一列的倒數第二欄是 Groups,列出這個檔案必須包含哪些 <g id="...">。狗狗那四列長這樣:

檔案 必須有的 group
dog-idle.svg shadowtailbodyheadearsfacecollartagoutline
dog-scared.svg 同上 + sweat
dog-happy.svg 同上 + joy-markstongue
dog-hit.svg 同上 + dizzy-stars

直覺的理由是「分組比較好手動改」。那是附加價值。真正的理由是:共同的九個 group 一旦被寫成清單,「這四張圖是同一隻狗的四個狀態」就從一句形容詞,變成一條可以執行的檢查。

驗證器可以逐檔問「這九個 id 在不在」,答案是布林值。如果 AI 為了畫出開心的表情,把 facetongue 合成一個 face-happy,它畫得再好看都會被擋——因為它破壞的不是美感,是這批素材之間的對應關係

反過來說,狀態專用的 group(sweattonguedizzy-stars)是允許新增的。規則的形狀是「共同的不准少,專用的可以多」,這讓變體有空間,同時鎖住骨架。


母版制:六張圖決定其他十三張

scripts/validate-svg.js:9-16 有一個具名常數:

MASTER_ASSET_FILES = dog-idle.svgbee.svghive.svgplatform-grass.svgbutton-primary.svgstar-filled.svg——六個母版。第一階段總共交付 19 個 SVG,其中 6 個是母版。

PRD 的〈分批生成策略〉把生成拆成八個批次,母版批次那一列的「人工關卡」欄寫著四個字:必須人工審查後才繼續

批次 Codex 任務 人工關卡
規範批次 建立合約、色票、驗證器、素材清單 確認色票與角色設定
母版批次 六個母版 SVG 必須人工審查後才繼續
狀態批次 狗狗與蜜蜂的狀態變體 比對中心點、尺寸與輪廓
場景批次 平台、石頭、木箱、雲、樹 檢查縮放與平鋪
UI 批次 其餘按鈕、圖示、計時器 檢查 44px 小尺寸辨識度

母版制的邏輯是:把「風格」這個沒辦法用文字描述完整的東西,換成一個檔案路徑。 之後每一輪的提示詞不再說「卡通風格、線條圓潤、色彩明亮」,而是說「以 dog-idle.svg 為唯一造型母版,只允許變更列出的節點」。模型不需要理解你的審美,只需要對齊一個它讀得到的檔案。

代價是母版本身沒有母版可以對齊,所以它必須是人工把關的那一關。這是我在整條產線上刻意保留的人工閘門之一,Day 9 會講它為什麼放在這裡、而不是放在更後面。


一張工單長什麼樣

三層約束加母版,合起來就是一張可以貼給 AI 的工單。PRD 的〈Codex Prompt 範本〉是這樣寫的(節錄「設計契約」與「執行步驟」兩段):

設計契約:

- 所有檔案使用 `viewBox="0 0 256 256"`
- 保持 dog-idle.svg 的角色比例、頭部位置、輪廓粗細與色票
- 角色視覺中心保持在同一位置
- 只改變表情、耳朵、尾巴與少量姿勢
- 每個 SVG 包含以下 group ID:
  shadow、tail、body、head、ears、face、collar、tag、outline
- 可增加狀態專用 group,但不可刪除共同 group
- 不使用 script、foreignObject、image、Base64、filter、mask 或文字
- 不使用 AGENTS.md 未允許的色碼
- 圖形距離 viewBox 邊界至少 16 個 user units
- 不要修改 dog-idle.svg

執行步驟:

1. 檢查既有 SVG 與風格文件。
2. 先說明你觀察到的共同造型規則。
3. 建立三個完整 SVG。
4. 將素材加入 src/config/assets.js。
5. 將素材加入 AssetGalleryScene。
6. 執行 npm run assets:validate。
(7–10 略:lint、單元測試、build,任一失敗就修到過)

整張工單分成六段:先讀哪些檔、這次要建立什麼、設計契約、執行步驟、驗收要求、回報格式。值得指出的是第 2 步——要求它先講出它觀察到的共同造型規則,再開始畫。這一步不產出任何檔案,但它讓你在最便宜的時間點看到它理解錯了沒有。

還有一個細節:「圖形距離 viewBox 邊界至少 16 個 user units」。合約寫的是「至少 6% 安全邊距」,而 256 的 6% 是 15.36。同一條規則,合約留百分比、工單給絕對值。 合約要同時管四種 viewBox,所以只能寫比例;工單這一批只有 0 0 256 256 一種,那就把算術先做完再交出去——少一個需要模型自己換算的步驟,就少一個它算錯的機會。


來源版與執行版:一個目前還是空架子的分離

最後這一節是誠實補充,講一個我設計了、但沒有真的執行的東西。

規格要求素材分兩份存放:assets-source/ 放可編輯的來源版,public/assets/ 放經過驗證與最佳化的執行版。理由寫在 PRD 的錯誤表最後一列:「最佳化後不能編輯|最佳化工具刪除 ID|原始版與執行版分離,設定保留 ID」——SVGO 這類工具預設會把它認為沒用的 id 屬性拿掉,而那些 id 正是上一節整套檢查的依據。

目錄確實有兩份,各 19 個 SVG。但我把它們逐檔做位元組比對:

for f in $(cd assets-source && find . -name '*.svg' | sed 's|^\./||'); do
  cmp -s "assets-source/$f" "public/assets/$f" || echo "DIFF $f"
done

【實測】19 個檔案全部相同,零個有差異,repo 裡也沒有任何 SVGO 設定檔。PRD 的第八個批次「最佳化批次」從來沒有跑過。

所以現況是:分離的目錄結構在那裡,分離的理由還沒發生。 這不是壞事——它是一個成本很低的預留位——但如果我不說,你從目錄結構會推論出一條不存在的產線。

有一件事倒是真的在守著:tests/unit/assets-config.test.js:70:80 兩個測試,逐檔比對兩邊的 viewBox 與 group ID 集合必須完全相同。這個測試在 CI 裡,所以哪一天最佳化真的跑起來、而它把 id 刪掉了,建置會紅。閘門先架好了,等的是那條產線。


帶走什麼

風格一致性不是形容詞,是三個可以被指名的東西:顏色的名字、結構的數字、圖層的 ID。凡是這三層都沒接住的風格要求,都會在第三批素材上開始漂移。

三件今天就能做的事:

  1. 先建色票 Token 表,再產第一張圖。 名字要照用途取,不要照顏色取;兩個 Token 共用同一個色碼是正常的,合併它們才是問題。
  2. 要求語意分組,理由是可檢查而不是好編輯。 把「同一角色的不同狀態」寫成一份共同 ID 清單,一致性就從審美問題變成布林值。
  3. 母版先產、人工審、然後只准對齊母版。 之後的提示詞用檔案路徑取代形容詞——模型不需要懂你的品味,只需要讀得到那個檔。

明天 Day 8 進到這個模組的核心:前面兩天講的都是「合約」,而合約有一個致命弱點——它沒有辦法讓不合格的東西進不了 repo。真正的閘門是那支 516 行、20 條規則的 scripts/validate-svg.js。明天要一條一條看它在檢查什麼、每條規則各自在防哪一種偷懶,以及昨天賣的那個關子:Never 那十項裡,為什麼有一項天生就檢查不了。


本篇數字的快照時間:2026-08-07 12:35(+0800),對應 commit 5aa3705。專案仍在開發中,量體數字會變動;引用的每一項都可以用本文提到的檔案路徑與指令自行對照。
可玩網址https://save-the-dog-web.vercel.app/原始碼https://github.com/HarryFan/save-the-dog-web

參考資料

如果你卡在語法

深入原理


上一篇
Day 6|把「不准做的事」寫成合約:AGENTS.md 與 CLAUDE.md
下一篇
Day 8|驗證器才是真正的閘門:一支會讓 CI 失敗的腳本,比一百句提示詞有效
系列文
一條線救一隻狗:我用 PixiJS、Matter.js 和一條有閘門的 AI 產線做完一款網頁小遊戲8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言