alt="IMG_2847.png" 一樣能通過多數自動化無障礙檢查——因為多數檢查只確認 alt 屬性存不存在,不確認裡面寫的東西對不對得上圖。GitHub 工程團隊拆開這個落差,並引用 WebAIM 的統計指出:熱門網站上超過四分之一的圖片,alt 不是缺的就是糊的,或者根本是抄隔壁圖片來的。
alt 文字通過了自動檢查,然後呢
團隊把 a11y 掃描接進 CI,紅燈變綠燈,大家鬆一口氣——這是多數團隊對「無障礙已經處理好了」的全部驗證。GitHub 無障礙團隊的前軟體工程實習生 Taarik Ashenafi 與 Keenan Zhou 在 8 月 24 日的文章裡把這個假設攤開來看:自動化工具確實能可靠地標出「alt 屬性不見了」,但用他們自己的說法,工具「對修好寫得很差的 alt 文字就沒那麼在行」——寫了等於沒寫的那一種,工具修不動。他們談的是自己替 GitHub Accessibility Scanner 做的一個 alt text plugin——一套用來擋住明顯錯誤的規則組:規則本身沒有 bug,可是頁面上還是有一堆看起來「有寫」卻毫無資訊量的 alt 文字,順順利利地跟著規則一起通過。這兩件事聽起來像同一個問題的兩種說法,實際上是完全不同的兩種驗證難度,而多數團隊的掃描工具只做了前面那一種。這篇文章想拆的不是「規則寫得夠不夠多」,而是「規則本來就只能驗證哪一種東西」——這是不同層次的問題,而多數團隊接上掃描工具的時候,根本沒想過自己面對的是哪一層。
四分之一張圖,alt 是缺的、糊的,或抄鄰居的
先看規模。WebAIM 的 2026 WebAIM Million 報告抽樣了熱門首頁的圖片 alt 屬性,得出的數字是:16.2% 的圖片完全沒有 alt 屬性;在剩下有寫 alt 的圖片裡,又有 10.8% 寫的東西「描述不足」——檔名、泛稱詞,或者直接抄旁邊那張圖的描述。GitHub 引這兩個數字時的總結說法是「超過四分之一」。這份報告抽樣的對象是排名最前面的一百萬個首頁,不是隨便找的長尾網站,數字卻還是這麼高——合理的推測是,這批網站裡不少早就跑著某種自動化 a11y 檢查,只是那些檢查驗證的是「有沒有」而不是「對不對」,才會讓超過四分之一的問題滑過去。
| 圖片的 alt 狀況 | 佔比 | 說明 |
|---|---|---|
| 完全沒有 alt 屬性 | 16.2% | 屬性整個缺失,自動檢查的「存在性」規則直接抓到 |
| alt 有寫,但描述不足 | 10.8% | 檔名、泛稱詞,或抄鄰居圖片——這一塊最容易被判定為通過 |
| 合計(本文自行相加) | 27.0% | 16.2%+10.8%,兩個比例的分母並不相同;GitHub 原文的說法是「超過四分之一」 |
16.2% 那一塊,自動檢查本來就能處理——屬性不存在,規則一比對就知道。真正有意思的是 10.8% 那一塊:alt 屬性「有」,內容卻等於白寫。如果一套掃描工具只驗證「有沒有」,這 10.8% 的圖片會被標成通過。這正是文章要拆開來講的謎題:一個綠燈全過的頁面,為什麼還會有這麼高比例的爛 alt 混在裡面。而且這個問題不會隨團隊規模變大自動消失——CI 裡的掃描步驟只要沒有紅燈,工程師就會假設無障礙這一項已經處理完,很少有人會回頭去讀掃描工具實際在檢查什麼。
五條規則抓得住「明顯錯」,抓不住「看起來對」
GitHub 自己的無障礙檢查工具叫 GitHub Accessibility Scanner,alt text 的驗證邏輯是一個獨立的 plugin,公開放在 github/accessibility-scanner-alt-text-plugin,任何團隊都能直接拿來接進自己的掃描流程,不是內部專用工具。它預設跑五條規則,全部是字串層級的比對,不需要看圖,也不呼叫任何模型:屬性不存在或只有空白;alt 是檔名,例如 hero.png、IMG_2847.jpg;alt 是忘記換掉的 placeholder,例如 TODO、tbd;alt 是一個只講媒材種類的泛稱詞,例如 image、logo、chart;同一段 alt 在版面上相鄰的圖片之間重複出現。這五條的共同點是:判斷依據全部在字串本身或圖片在版面上的位置裡,完全不用理解圖片畫的是什麼,跑起來也快,不需要額外的 API 呼叫或 token 設定。
把這五條規則套到具體案例上,就能看出它們各自的守備範圍在哪裡,也能看出邊界在哪裡——每一條規則對應的是一種「明顯寫壞」的模式,判斷邏輯全部是字串比對,不牽涉理解圖片內容本身。
點一種壞掉的 alt 文字,看五條規則裡誰抓得到 · 6 種情境 × 5 條規則
alt 屬性整個不見,或只有空白——五條規則裡的第一條直接抓到,這種完全不用看圖也能判。
前四條規則守的都是「字串本身有沒有問題」——空白、檔名、placeholder、泛稱詞,每一種都能靠 pattern 判斷,不需要理解語意。真正逼近語意的只有最後一列:alt 文法通順、屬性也確實填了東西,但內容跟畫面上的圖片對不上。五條規則在這一列全部沉默。GitHub 的說法很直接:「The deterministic rules are literal. They catch alt text that's obviously unwritten, not alt text that's fluent and wrong.」規則能證明一個字串是錯的,卻沒有辦法證明一個字串是對的——這是字串比對的天花板,不是實作沒寫好。換句話說,五條規則加起來也只是把「明顯沒寫」的案例過濾掉,通過檢查這件事,從來就不等於「內容是對的」;它只等於「沒有踩到我們已知的幾種寫壞方式」。GitHub 描述那 10.8%「描述不足」時舉的例子——alt="image"、原始檔名、抄自鄰居的描述——正好跟這五條規則裡的三條對得上;原文只是舉例,沒有給出各種寫法各佔多少的細分,所以合理的推測是,這 10.8% 裡有相當一部分落在這五條規則的守備範圍內,只要掃描工具真的把規則接進 CI,理論上抓得到。真正抓不到的,是文法通順、屬性也填了,內容卻跟畫面對不上的那一種——最後一列。這種壞法之所以危險,正是因為它不會出現在任何一份「常見錯誤」清單裡:寫的人沒有偷懶,字串本身也挑不出語法毛病,唯一的問題是它形容的不是眼前這張圖。對審 code 的人來說,這種 alt 甚至比明顯缺失更難發現——缺失的地方一眼就能看出少了什麼,內容錯置的地方看起來完全正常。要在 code review 裡當場抓出這種問題,審查者得同時看著程式碼裡的 alt 字串跟畫面上那張圖,逐字核對兩者對不對得上,這件事沒有工具能代勞,只能靠人。
相鄰,是版面上的相鄰,不是檔案裡的相鄰
第五條規則——相鄰圖片重複 alt——原本聽起來是五條裡最容易寫的一條:掃過 DOM,找出前後兩張圖 alt 是否相同就好。這條規則要抓的情境很具體:一整排圖示各自代表不同東西,卻共用同一句描述,讓聽螢幕報讀軟體的人反覆聽到同一句話,卻分不清哪一句對應哪一張圖。但 GitHub 團隊在文章裡承認,這條規則一開始真的是照文件順序判斷「相鄰」,結果抓錯了東西:「a footer 'GitHub' logo and a header 'GitHub' logo might sit next to each other in the extracted list but nowhere near each other on screen, so nobody experiences them as a group.」原文用的是「might」:頁首和頁尾的 logo,在被抽取出來的圖片清單裡可能前後腳出現,但畫面上一個在最上、一個在最下,使用者不會把它們當成同一組圖片來聽。這是一個示範用的例子,不是一份出現頻率的統計。更麻煩的是,這種誤判不只是浪費一次檢查——它會讓開發者看到規則亂噴警告,進而懷疑整條規則的可信度,跟前面「a quality-oriented rule with false positives is a rule teams switch off」講的是同一件事:字串比對的規則一旦開始亂抓,團隊處理的方式往往不是修規則,而是關掉規則。
改用 版面距離兩張圖 bounding box 之間的間隙,是相對於圖片本身大小去衡量的——間隙相對於較大的那一邊夠小才算同一組,不是一個固定的絕對像素值,也不是誰在 HTML 裡寫在誰前面。 判斷「相鄰」之後,GitHub 團隊的原則是「What matters is where images land on screen, not where they sit in the markup.」
這一步修正很小,卻說明一件更大的事:就算是「相鄰重複」這種聽起來很機械的規則,選錯判斷依據一樣會系統性地誤判——不是誤判成抓太少,而是誤判成抓錯位置。五條規則本身沒有錯,錯的是它們讀取世界的方式。這也預告了下一個問題:如果連字串比對都得靠版面資訊才能校準,那真正需要理解「這段文字跟這張圖對不對得上」的判斷,字串比對還能碰嗎?答案是不能——這正是 GitHub 團隊把語意判斷整個劃到另一條規則的原因,而且刻意把這條規則跟前面五條分開處理,不混在同一套預設開啟的邏輯裡。
拖動圖 B,看版面距離怎麼跟著變 · 可拖動範圍 140–480px
版面距離:近——這樣的一對圖片,規則會判斷成同一組,alt 相同才會被標記重複。
把「這樣寫好不好」改成一套照順序想的程序
GitHub 團隊確實試過用視覺模型直接判斷 alt 好不好,第一版做法很直覺:把圖片丟給模型,問它「這個 alt 寫得好嗎」。結果他們自己描述的失敗經驗是:「Given perfectly good alt text, our first version of the checker would suggest different alt text, because 'could this be better?' is a question a language model always answers yes to.」模型永遠覺得還能改,於是一條原本該挑出爛 alt 的規則,變成不管三七二十一都在挑剔——就算 alt 已經寫得很好,模型也會擠出一個「可以更好」的建議,逼工程師每次都要停下來判斷這個建議到底該不該採納。他們的結論很直白:「a quality-oriented rule with false positives is a rule teams switch off」——一條會製造誤報的品質規則,團隊遲早會把它關掉。這個失敗經驗其實比表面上更重要:它說明「讓模型看一眼給個意見」這種做法,本身就不是一個可以直接拿來當自動化規則的形狀,模型需要的是一個有邊界的問題,不是一個開放式的評分請求。
修法不是換一個更聰明的模型,而是換一套問法。GitHub 團隊把「這樣寫好不好」拆成一套四步依序檢查、命中就停的決策程序:decorative(裝飾用)、redundant(跟 caption 重複)、functional(可操作元件)、informative(承載實際資訊)。原文寫得很清楚:「The prompt walks four ordered steps, stops at the first that matches, and emits that step's verdict.」送進模型的 context 也是固定的一組欄位:「the nearest heading, the page title, any figcaption, whether the image sits inside a link or button, and up to 600 characters of nearby prose」——不是丟一張裸圖進去猜,是連圖片所在的頁面脈絡一起送。四步依序檢查的順序也不是隨便排的:decorative 排第一,等於先問「這張圖需不需要 alt」,比直接跳去判斷內容寫得好不好更基本;informative 排最後,代表只有前三種身分都不成立,模型才需要老老實實描述圖片實際的內容——順序本身就是一種篩選,避免模型對一張裝飾圖硬生出一段不存在的描述,也避免它對一張已經被 caption 講過的圖畫蛇添足。
拖動滑桿,看模型實際能讀到多少鄰近文字 · 上限 600 字
切換 4 個步驟,看視覺模型怎麼依序判斷 · 4 步驟,命中第一條就停
光有決策程序還不夠,GitHub 團隊還加了一條明確的反吹毛求疵規則:「Treat a short alt as correct when the surrounding prose already analyzes the image.」如果周邊文字已經在講這張圖,alt 短一點也算數,模型不該再挑剔。原文把修正歸納成三件事,第一件是前面已經講過的四步決策程序:「A decision procedure instead of an instruction.」第二件是這一整組反吹毛求疵的規則:「Explicit anti-nitpick rules.」——「相信作者原本的用意」其實是掛在這一條底下的細則之一,不是獨立的第三件事。真正的第三件是輸出格式本身:「Structured output with a forced field order, so reasoning is generated before verdict and the model has to build an argument before it picks a label.」欄位順序被寫死,模型必須先產出 reasoning 這個欄位,才輪得到 verdict,等於強迫它先把理由講完,才准挑一個標籤貼上去。這一條跟前兩條的方向其實一樣:前兩條限制模型被問了什麼,第三條限制模型用什麼順序回答。三件事合起來,是把「這裡是不是還能改進」的開放提問,收斂成「這個 alt 在四種身分裡屬於哪一種、屬於這種身分該有的樣子有沒有出現」。模型不再被允許用「我覺得可以更好」當作理由開口,它只能在程序許可的地方、用程序規定的順序開口。
這條規則預設是關閉的——「The rule is off by default. It won't run unless you deliberately enable it in your plugin configuration, and it needs a token with access to GitHub Models.」也就是說,一個團隊要嘛完全不跑語意判斷,要嘛得先主動去設定一個能存取 GitHub Models 的 token 才會啟動,不會有人在不知情的狀況下被動用到這條規則,這跟前面五條規則裝上去就直接跑是兩種完全不同的預設姿態。啟用之後,圖片網址在送進模型前也會先做隱私處理:「query and fragment are stripped from anything entering the model context or the rule's error logs」,避免 CDN 簽章或 session 識別碼外流。但這條規則同樣不是萬能:它需要在瀏覽器工作階段之外重新抓一次圖片,「anything behind authentication can fail to load」;目前也只檢查 HTML 的 img 標籤,「SVG, role="img" containers, CSS backgrounds, and canvas aren't covered yet」;而且它本身一樣會誤判——「The model-backed rule produces false positives. Every finding is a prompt for human attention, not a verdict.」它給的是一個值得人去看一眼的提示,不是最終判決。把這幾條限制擺在一起看,會發現視覺模型規則並沒有把「驗證語意」這件事徹底解決——它只是把一個原本完全沒人碰的角落,變成一個有人願意去碰、但仍然需要人覆核的角落。這跟前面五條 deterministic 規則的定位剛好相反:五條規則零誤判、但天花板很低,只要字串沒踩到 pattern 就會放行;視覺模型規則能碰到語意、但本身不保證對,每一次命中都得當成一個提示去核對,不能直接當成裁決拿去用。兩者疊起來,勉強算是一套完整一點的檢查鏈,可是還是補不滿 GitHub 自己列出的那些洞——被登入牆擋住的圖、SVG、CSS 背景圖、canvas,都在目前的涵蓋範圍之外。
回到最初的問題:為什麼一個全綠的自動化檢查結果,還是漏掉了四分之一的爛 alt?答案分兩層。第一層很直接——五條 deterministic 規則本來就只證明字串「明顯錯」,證明不了字串「對不對得上圖」,這是字串比對的結構性天花板,不是規則沒寫全。第二層更隱蔽——就連字串比對本身也需要正確的判斷依據,相鄰重複規則從文件順序改成版面距離,抓的才是使用者實際會經歷的重複,不是 markup 巧合造成的假重複。真正能碰語意的機制只有 opt-in 的視覺模型規則,而且它自己也會犯錯,需要人在旁邊看著——這也是為什麼它預設關閉,不像前面五條規則那樣直接跑在每一次掃描裡。把兩層放在一起看,會發現「自動化檢查漏掉壞 alt」從來不是單一原因造成的意外,而是每一層驗證機制各自的能力邊界疊加出來的結果:字串規則管不到語意,早期版本的模型規則又管不住自己的挑剔慾望,兩層都補上之後,剩下的缺口才輪到人去看。GitHub 團隊自己的結論是:「Passing isn't conformance. Automated checks are a floor. Testing with people who use assistive tech is the goal.」
下次:看到一個 a11y 掃描全綠的頁面,先別急著信——去翻一下規則清單,問清楚每一條在讀的是「字串本身合不合語法」,還是「字串跟畫面對不對得上」。前者再多條規則疊起來,也補不了後者的洞;能補的只有人,或者一條你願意手動打開、而且知道會有誤報的模型規則。判斷依據是字串還是版面、還是圖片本身——這一句話,比「有沒有裝掃描工具」更值得先問清楚。