GitHub 为无障碍扫描器构建 alt 文本插件:自动检查通过不等于文本合格
Your alt text passes automated checks. That doesn’t mean it’s any good.
GitHub 为 Accessibility Scanner 构建了 alt 文本插件,用五条确定性规则默认检查缺失、文件名、占位符、泛化词和重复的 alt 文本,另有一条可选规则调用视觉模型结合页面上下文判断质量。
GitHub 团队复盘了自动检查 alt 文本的取舍,对构建自动化质量检查的人有可迁移的方法参考。
网络上最受欢迎的主页中,超过四分之一的图片其替代文本要么缺失、要么含糊不清,要么是从相邻图片复制而来。
这来自 WebAIM 的 2026 年 WebAIM Million 报告,该报告发现,在前一百万个主页中,有 16.2% 的图片缺失替代文本——替代文本是一种 HTML 属性,包含描述图片内容的文字。而在确实带有替代文本的图片中,另有 10.8% 提供了毫无描述性的属性,例如 alt="image"、原始文件名,或从相邻图片复制来的描述。
虽然自动化工具能可靠地标记出缺失的替代文本,但它在修复写得糟糕的替代文本方面就没那么擅长了。大多数替代文本检查器测试的是图片是否存在可访问名称,而不是所提供的替代文本是否对相关图片说了任何有用的话,而这是有意为之的设计选择:一条会产生误报的质量导向规则,是团队会直接关掉的规则。所以 alt="IMG_2847.png" 能通过。五个不同的星形图标上出现同样的 alt="3/5 stars" 也能通过。
我们为 GitHub Accessibility Scanner 构建了一个替代文本插件,以帮助改进你的替代文本。本文涵盖了我们如何在检查器能够证明的内容与只能怀疑的内容之间划定界限、为什么我们最严重的 bug 最终是布局问题而非解析问题,以及当我们让模型参与进来后发生了什么变化。
如果你正在构建自己的自动化检查——无论是为了无障碍还是其他目的——这些权衡应该同样适用。
在看不到图片的情况下证明一个字符串是错的
替代文本是否存在是一个客观事实;该属性要么在,要么不在。质量则往往是一种主观判断。机器无法仅凭标记证明一句话是否在上下文中充分描述了图片。
然而,并非所有质量都是主观的。你可以仅根据替代文本本身执行若干检查,无需查看图片内容:
- 该属性缺失(而非为空)或仅含空白字符。
- 替代文本是文件名,例如
hero.png、IMG_2847.jpg。 - 替代文本是某人打算替换的占位符,例如
TODO、tbd。 - 替代文本是一个泛指媒介而非内容的通用词,例如
image、logo、chart。 - 相同的替代文本在相邻图片上重复出现。
以上每一条都是关于字符串的断言,而这成了我们的分界线。默认运行五条确定性规则,它们无需运行 AI 模型的凭据,也无需网络调用。一条可选启用的规则会调用模型,并传入所提供的图片内容和周围上下文,用于做出替代文本字符串本身无法支撑的判断。
首先,我们必须确定在扫描的网页上要评判哪些图片。我们使用 Playwright 基于角色的定位器,而不是 querySelectorAll('img'),因此任何未包含在浏览器无障碍树中的内容都会被排除,包括任何带有 alt="" 的内容。最后这项排除最为重要。空的替代文本是作者明确表示该图片是装饰性的,标记它恰恰会惩罚你想要鼓励的行为。
那么,它应该有多严格?质量检查器的成败取决于误报,因此我们选择封闭集合而非聪明的启发式方法。含糊替代文本规则会先规范化字符串,然后对照一份精心整理的、本身不携带任何信息的词表进行检查。它只在完全匹配时触发:
alt="image"会被标记。alt="image of the login screen with the SSO button highlighted"不会。
如此字面化的规则会漏掉大量糟糕的替代文本。我们选择漏报而非误报,因为一个开发者愿意启用的可靠检查器,胜过被关掉的检查器。
重复是布局问题,不是 DOM 问题
重复的替代文本带来了一个有趣的问题。想象一行五个星形图标,每个都写着"3/5 stars"。屏幕阅读器用户会听到同样的内容五次,其中四次无法获得任何新信息。
我们的第一个版本按文档顺序遍历图像,并标记任何共享相同规范化替代文本的连续片段。它捕捉到了一些本不该捕捉的东西。例如,页脚的“GitHub”徽标和页眉的“GitHub”徽标在提取列表中可能相邻,但在屏幕上却相距甚远,因此没有人会将它们视为一组。
重要的是图像在屏幕上的位置,而不是它们在标记中的位置。因此,该规则现在会检查页面布局,并且仅当两个边界框之间的间隙相对于边界框本身较小时,才扩展连续片段:
const gap = Math.max(horizontalGap, verticalGap)
const largerDim = Math.max(a.boundingBox.width, a.boundingBox.height,
b.boundingBox.width, b.boundingBox.height)
return gap > GAP_MULTIPLIER * largerDim有两点值得注意:
- 乘数是一个主观判断,并非我们从任何东西推导出的数字。它是那种需要针对真实页面进行调整、而不能盲目相信规范的值。
- 当任一图像没有可测量的边界框时,检查会失败开放,连续片段继续。缺失的发现是不可见的;错误的发现则不然。
让模型像审阅者一样行事,而不是像批评家
确定性规则只需要替代文本字符串。任何更智能的规则都需要知道页面的内容,而图像元素并不跟踪这些信息。alt="a smiling person"是否合适完全取决于它周围的内容:在通用的情绪照片上,它可能没问题。但在指定了具体人物的标题下,它提供的细节就不够了。
在我们可选的alt-text-quality检查中,我们会提取每张图像旁边的页面上下文:最近的标题、页面标题、任何<figcaption>、图像是否位于链接或按钮内,以及最多600个字符的附近文本。
链接信号最重要,因为当图像是链接的唯一内容时,其替代文本会成为链接的可访问名称。此时正确的替代文本应命名目标,而不是描述图片。
一个注意事项:该插件只记录图像位于链接内。我们不检查它是否是链接的唯一内容,而这恰恰是将替代文本变为链接名称的关键。因此目前两种情况在模型看来完全相同。
该上下文、替代文本和图像会通过GitHub Models发送给视觉模型。我们的失败模式很少是模型误读图片,而是模型有自己的看法。即使替代文本完全合适,我们第一版的检查器也会建议不同的替代文本,因为“这能更好吗?”是语言模型总会回答“是”的问题。每张图像都会变成一个发现,于是信号就消失了。
三项改动解决了这个问题:
- 用决策流程代替指令。提示词按四个有序步骤进行,在第一个匹配的步骤停止,并输出该步骤的判定:装饰性、与说明文字冗余、功能性或信息性。
- 明确的防吹毛求疵规则。信任作者的表述。将冗余前缀(“Image of…”)与语义前缀(“Photograph of…”)区分开。当周围文本已经分析了图像时,将简短的替代文本视为正确。
- 强制字段顺序的结构化输出,因此
reasoning会在verdict之前生成,模型必须先构建论证,然后才能选择标签。
这一切并不能让模型永远正确,只是让它足够一致,从而可以据此迭代。该仓库附带了一个离线评分测试框架,由公开的教学材料构建而成:WebAIM、W3C 图像教程和 POET。规则和测试框架共用同一个提示词,因此你在离线时调优的内容,就是 CI 中运行的内容。不过,该测试框架只测试模型的判断力,而非整条流水线。一个用例在那里可以拿到满分,却在真实扫描中根本到不了模型那里。
将图像发送给模型是一个隐私和成本方面的决策
一旦某项检查带着网页数据调用外部模型,它就不再只是一条 lint 规则,而需要仔细的数据流设计。由此可以得出几点:
- 该规则默认关闭。除非你在插件配置中刻意启用它,否则它不会运行,而且它需要一个有权访问 GitHub Models 的令牌。
- URL 会被脱敏。图像 URL 和链接
href往往带有签名的 CDN 令牌或会话标识符,因此任何进入模型上下文或该规则错误日志的内容,其查询参数和片段都会被剥离。出于同样的原因,在我们发送的标记中,src和srcset会被替换为(omitted)。 - 该上下文窗口中的一切都是不可信输入。标题、小标题和正文都来自被扫描的页面,而页面可能包含旨在操纵模型的文字。结构化输出约束的是响应的形状,而不是其背后的推理。
有一点需要提醒,因为这份清单很容易被过度解读:发现结果仍会带着真实页面 URL 和原始 HTML 进入扫描器的常规报告流水线。这是有意为之,因为你无法修复一个你找不到的图像。脱敏缩小的是到达模型和日志的内容,而不是进入你自己 issue 的内容。而且,如果你配置了 Azure AI Vision 凭据,一个可选的 OCR 预处理步骤会将图像字节发送到第二个地方。没有任何环节强制要求 Azure,但数据流审查需要覆盖这两条路径。
成本也遵循同样的形态。在常见情况下,这是每次扫描每张图像一次模型调用,在图像密集的网站上,这会主导整个运行的成本。这足以成为将其安排在定时任务上、而非每次提交都运行的理由。
这仍然做不到的事
- 确定性规则是字面意义上的。它们能捕捉明显未撰写的 alt 文本,却捕捉不到流畅但错误的 alt 文本。它们读取的是
alt属性,而非计算出的可访问名称,因此一个能解决问题的aria-label并不会阻止该发现结果。 - 由模型支持的规则会产生误报。每个发现结果都是提请人工关注的提示,而非裁决。
- 沉默并不等于覆盖。该规则会在浏览器会话之外重新获取图像,因此任何位于身份验证之后的内容都可能加载失败。获取和模型错误会被记录并跳过,这意味着一个页面可能因为什么都没被检查而返回干净结果。
- 建议的 alt 文本只是草稿。一个只看到图像和附近几个词的模型,无法顾及你的受众、你的内部风格,或该图像在整个页面上所承担的作用。
- 有些发现结果会与扫描器内置的检查重复,因为我们的
missing-alt规则覆盖了相同的范围。 - 我们只检查 HTML
<img>标签。SVG、role="img"容器、CSS 背景和 canvas 尚未覆盖。 - 这是新代码,来自真实世界的反馈有限。像这样的规则只有在遇到真实网站上各式各样的标记和内容时才会改进。这个插件还没有经历过这些,所以请相应地看待早期的发现。
- 通过并不等于符合规范。自动化检查只是底线。与使用辅助技术的人一起测试才是目标。
如果你正在构建类似的东西,我们会这样告诉你
把你能够证明的东西与你只能怀疑的东西区分开来,并给它们不同的默认值。能够证明某些东西的检查应该是低成本、可预测且默认开启的。只能怀疑某些东西的检查应该是选择性加入的,并且应该读起来像是建议而非裁决。然后,去问用户实际体验到了什么,而不是 DOM 说了什么。这个插件中每一个仍然存在的缺口都属于第二种形态。我们记录的是图像位于链接内部,而不是它就是那个链接。我们读取的是一个属性,而不是一个计算出的名称。
这段距离才是真正的边界,而更好的模型也无法弥合它。为一个看不见图像的用户判断该图像的功能是什么,仍然需要人的判断。自动化能为你带来的,是确保那个人对正确的图像进行二次检查。
在你的无障碍扫描工作流中试试这个 alt 文本插件。 如果它告诉了你错误的信息,请报告。提交一个issue,附上发现的问题,如果页面是公开的,再附上受影响页面的链接。
文章 Your alt text passes automated checks. That doesn’t mean it’s any good. 首次出现在 The GitHub Blog。
来源:GitHub Blog · Engineering · github.blog