Day 25 · W4 · AI 線 · 難度 ★★★☆☆`
本系列由 AI 協作撰寫。 內容、技術判斷、程式碼由 light-design 數位顧問團隊與 Claude 共同產出,最終由作者驗證後 publish。完整協作模式與把關方式見 Day 01。
Day 20 那份給 agent 的說明書,第一句話是:模型不知道這些規則的內容,必須查。
那句話聽起來很合理。但差多少?我原本以為會差很多,查過的版本應該乾淨得多。結果跑完,兩邊的 lint 結果幾乎一樣,差別藏在另一個地方。
Day 23 比的是同一個站修前跟修後,今天比的是同一個需求查前跟查後。量的不是「有沒有修好」,是「有沒有知道」。
一句話主軸:先查再寫有差,但差別只在它查到的那幾條。查的範圍決定改善的範圍。
需求固定一句話:做一個登入表單,單一 HTML 檔,電子郵件、密碼、記住我、送出,要有即時錯誤提示,繁體中文。需求裡不提無障礙,不提任何碼號。
兩個條件,各寫三份:

唯一的變數是生成前有沒有查。模型、參數、需求敘述全部相同。
寫的人是乾淨的 Claude 子代理,每一份都是新的對話,沒有這個專案的任何記憶。A 只拿到需求;B 多拿到一份表,是工具的 rules search 對 form、input、label 三個詞的輸出,41 列、28 個不同的檢測碼。skill 裡規定 agent 寫表單前該做的就是這三次查詢,所以 B 模擬的是它照規矩走的樣子。
每組寫三份,不是為了平均,是為了看抽樣的晃動有多大。子代理的取樣參數我控制不了,同一個提示跑三次本來就會長得不一樣;如果三份之間的差異跟 A、B 之間的差異一樣大,那 A、B 的差就不算數。
量尺有三把。lint 讀原始碼,只管 50 條;scan 開瀏覽器跑 146 條,不帶模型;最後我自己把六份原始碼攤開對照。先講誠實條款:我們拿自己的工具評自己的 skill,N 是 3,lint 只涵蓋靜態可驗的那一半。這三句要放在數字前面,不是註腳。
我原本以為沒查表的版本會掉一堆基本錯誤。結果六份原始碼攤開,A 的三份全部有:autocomplete、每個欄位對應的 label、aria-describedby 接到錯誤訊息、aria-live 或 role="alert"、aria-invalid、required。錯誤摘要區有標題、能聚焦。
通用的 a11y 常識,模型本來就有。這一半不需要查。A1 的錯誤摘要長這樣,標題裡有字,數字用 JS 換:
<div id="error-summary" class="summary" role="alert" tabindex="-1">
<h2>表單有 <span id="error-count">0</span> 個錯誤,請修正後再送出</h2>
lint 跑出來,A 三份各 1 個 fail,同一條:頁面頂端沒有跳到主要內容的連結。另外 1 到 2 個 info,麵包屑跟錯誤訊息的 class 缺 live region 標記。
B 的三份多了幾樣東西,每一樣都能在那份表上找到對應的那一列:欄位包進 fieldset,加了 legend;每個欄位多接一個格式範例的說明;B1 在頁面頂端加了跳到主要內容的連結。表上有的,它做了。
然後 lint 報了一個 A 沒有的錯:
<h2 id="error-summary-title"></h2>
B1 跟 B3 都有這一行。它是錯誤摘要區的標題,等到有錯誤時才由 JS 填字,平常是空的。lint 看到的是一個沒有內容的標題,螢幕報讀軟體會唸出一個空標題。A 的寫法是把「表單有 N 個錯誤」的文字直接放在標題裡,數字用 JS 換,標題本身從來不空。
照表寫,多了結構,也多了一種新錯。表上寫的是「提供錯誤摘要」,沒寫「摘要沒東西時標題不能空著」。
B1 那條跳到主要內容的連結也是一半:開瀏覽器按 Enter,焦點沒有過去,因為目標元素沒有 tabindex="-1"。它照表加了連結,表沒告訴它連結要能把焦點帶過去。
還有一個反方向的變化:A 三份都同時寫了 required 跟 aria-required="true",B 三份全部只留 required。表上那條講的是用 HTML 5.2 的原生屬性,它照做,把重複的 ARIA 拿掉了。這不是錯,但它顯示查表會改變的不只是加東西,也會減東西。
把 scan 的結果攤開,A 三份的 fail 是 7、6、6,B 三份是 6、6、6。差 0 到 1。

兩邊都缺的五項,沒有一項在 B 拿到的那份表上。
兩邊都缺的是同一批:跳到主要內容的連結、頁面上沒有 nav 或站內導覽、沒有 accesskey 快捷鍵、記住我的勾選框 18 到 22px 不到 24、AAA 對比 6.46 到 6.99 差一點到 7.0。
這五項有一個共同點:全是頁面級的東西,不是表單的東西。B 查的是 form、input、label,那份表裡沒有跳頁連結、沒有 accesskey、沒有 7:1。它沒查到,所以沒做。A 沒查,也沒做。兩邊一樣。
這就是「差別只在它查到的那幾條」的意思。查表有用,用處的邊界就是查詢的邊界。
lint 幾乎看不出差別,還有另一個原因:B 拿到的 28 個碼裡,lint 管得到的只有 7 個。查表改變的大多是 lint 讀原始碼讀不出來的東西,要開瀏覽器或人看才看得到。所以「兩份都拿去 lint」這一關幾乎是平手,真正的差別要到 scan 跟原始碼對照才露出來。
再看那五項,有兩項是台灣碼表特有的判定。accesskey 快捷鍵是標章的慣例要求,WCAG 條文裡沒有這個字,工具裡那條規則也是為台灣標章另外加的;AAA 對比 7:1 在 WCAG 是 AAA 等級,模型記得的是 4.5:1,所以六份的灰色說明文字全落在 6.46 到 6.99,剛好過 AA、剛好不過 AAA。這兩項不查,模型憑常識永遠寫不出來。
另外三項是通用的,跳頁連結、導覽、24px。模型知道它們存在,但寫一個「登入表單」的時候不會主動放,因為需求沒提頁面。
Day 05 的三層模型放在這裡剛好:WCAG 條文模型大致記得,碼表的實作要求它不記得,工具的判定門檻它更不知道。查表補的是後兩層,但只補查到的那一段。
先查再寫不是免費的。六份的 token 用量:A 三份平均 48,600,B 三份平均 62,500,多 29%,多的是那份 3,700 字的表跟寫出來的額外結構。查表本身是本機查,零 token;貴的是把查到的東西放進上下文。寫出來的檔也跟著長:A 三份 9 到 12 KB,B 三份 13 到 17 KB,多的是 fieldset、說明文字跟錯誤摘要那些結構。
所以查什麼要挑。表單就查表單,不要把 146 條全塞進去。這也是 skill 把查詢設計成關鍵字搜尋、而不是一次倒整份碼表的原因。
這個實驗量到的東西有邊界,攤開來:我們評自己的 skill;N 是 3,每組差 1 個 fail 就可能是抽樣;lint 只 50 條;scan 沒開模型,所以要語意判斷的那些條沒跑;六份全用 novalidate,探針不按送出鍵,Day 19 那條在六份全是 caveat。
還有一個小地方:同一個空標題,lint 報的碼號是 HM1130100C,scan 報的是 GN2240600E。Day 09 講過兩套碼號的由來,這次撞到了實例。
差距比我以為的小,這件事照樣寫。它沒有推翻「必須查」那句話,它把那句話收窄了:查是必要的,查什麼決定你得到什麼。要讓差距變大,下一次該查的不是更多表單規則,是跳頁連結、導覽、快捷鍵這些頁面級的詞,讓表把那五項也蓋進去。那是另一次實驗。
明天 Day 26:五個實驗跑完,回頭數自己。每一件 AI 沒做到的事,都是我以為它會自己做的。