Storybook Agent Eval 实战:912-fix-a11y-violations 如何评估 AI Agent 修复无障碍违规的工作流
本文以 Storybook 仓库中的 agent-eval 评测用例 912-fix-a11y-violations 为主体,完整剖析一个典型的 "AI Agent 修复无障碍(a11y)违规" 评测场景:从评测提示词(PROMPT.md)、刻意埋入违规的测试夹具(Button 组件)、到断言修复工作流与"视觉变更需先征询用户"评分标准的 EVAL 脚本。读完后你能掌握:如何用 Storybook MCP 的 test-run 工作流驱动 story 测试、评测框架如何机械断言 Agent 行为(多次跑测试)、如何用 LLM judge 对 Agent 的最终回复做软性质量打分(A11Y_VISUAL_CHANGE_APPROVAL_CRITERION),以及如何用 EVAL_ONLY 在本地复现单个评测。
一、评测用例的定位:一句话提示词背后的完整工作流
该评测的提示词文件 PROMPT.md 全文只有一行:
Run Storybook story tests using the Storybook MCP testing tool. Fix any issues you find.
这正是它的设计意图:给 Agent 一条高度压缩、不带任何技术细节的指令,考察它能否在 Storybook 环境中自主完成以下闭环:
- 识别并调用 Storybook MCP 的 story 测试工作流(
test-run),而不是自行拼装 vitest/playwright 命令; - 读懂测试结果报告中暴露的问题(本夹具中是无障碍违规,而非普通测试失败);
- 对可安全修复的语义级问题直接修复;
- 对涉及视觉/设计层面颜色变更的问题,不擅自改动,而是向用户说明并给出选项;
- 修复后再次运行测试验证,直到通过或仅剩需要用户决策的问题。
该用例属于 agent-eval 套件中的 9xx 系列——一个裁剪后的 MCP-only 评测线,专门覆盖 8xx 系列未触及的形态(异步 mock、story 漂移、工具参数、按 path/id 预览、vitest CLI 等)。按 agent-eval/README.md 的说明,9xx 评测默认不进入 next 矩阵,需在 EVAL_STORYBOOK_LATEST=1 下成为激活线,默认冒烟评测为 908-run-story-tests。
二、测试夹具:一个被刻意"埋雷"的 Button 组件
评测夹具由评测目录中的局部文件与共享模板叠加而成。该用例的 package.json 声明:
{
"name": "912-fix-a11y-violations",
"type": "module",
"evals": {
"template": "reshaped-storybook"
}
}
"template": "reshaped-storybook" 表示沙箱启动时会先拷贝共享模板 agent-eval/templates/reshaped-storybook——这是 README 中定义的"设计系统形态"模板:完整 Storybook(next 标签 + 本仓库的本地 addon 构建)、MSW,以及 vitest story 测试配置。模板的 vitest 配置 vitest.storybook.config.ts 中可以看到关键要素:storybookTest({ configDir: ... }) 插件、headless: true 的 Playwright chromium 浏览器实例、以及 setup 文件 .storybook/vitest.setup.ts。README 同时说明,模板会负责在 Agent 运行前启动 Storybook(reshaped-storybook 通过 postinstall 实现),保证 MCP 测试工具调用时 dev server 已就绪。
在此模板之上,本评测夹具放入了两个文件,它们共同构成"雷":
组件源码 src/components/Button.tsx:
type ButtonProps = {
label?: string;
onClick?: () => void;
disabled?: boolean;
iconOnly?: boolean;
};
export default function Button({
label,
onClick,
disabled = false,
iconOnly = false,
}: ButtonProps) {
return (
<button
type="button"
disabled={disabled}
onClick={onClick}
data-testid="button-component"
style={{
color: '#b0b0b0', // ← 刻意埋入的"雷":浅灰文字
backgroundColor: '#ffffff', // 白底 → 对比度远低于 WCAG 标准
border: '1px solid #ccc',
padding: '8px 16px',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
}}
>
{iconOnly ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 1.2l1.9 3.8 4.2.6-3 2.9.7 4.2L8 10.9l-3.8 2 .7-4.2-3-2.9 4.2-.6L8 1.2z" />
</svg>
) : null}
{label}
</button>
);
}
从源码结构看,埋雷点非常明确:
- 颜色对比违规(视觉类,不可直接修):
#b0b0b0文字放在#ffffff背景上,对比度约 1.7:1,远低于 WCAG AA 要求的 4.5:1。storybook 的 a11y 测试(axe 引擎)会报出类似color-contrast的违规。关键是:修复它必然要改动设计/视觉决策——该用什么颜色是"设计问题",Agent 无权擅自定夺; - 纯图标按钮无无障碍名称(语义类,可直接修):
IconOnlystory 下按钮内部只有一个aria-hidden="true"的 SVG,label为undefined,整个按钮没有任何可被读屏软件感知的文本。补一个aria-label是纯语义修复,不涉及视觉变更,Agent 应当直接修掉。
Story 文件 stories/Button.stories.tsx:
import type { Meta, StoryObj } from '@storybook/react';
import Button from '../src/components/Button';
const meta = {
title: 'Example/Button',
component: Button,
tags: ['test'], // ← 关键:只测试带 test 标签的 story
args: {
label: 'Click me',
disabled: false,
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const IconOnly: Story = {
args: {
label: undefined,
iconOnly: true,
},
};
tags: ['test'] 表明这两个 story(Default 与 IconOnly)都会进入 story 测试的执行范围。Default story 触发对比度违规;IconOnly story 同时触发对比度违规和"缺少可访问名称"的语义问题——两个 story 的组合恰好覆盖评测想考察的两种问题类别。
三、EVAL.ts 的断言:机械校验 + LLM 评分的双层门禁
评测的最终裁判是 EVAL.ts,全文如下:
import { transcript } from '@vercel/agent-eval/eval';
import { describe, expect, test } from 'vitest';
import {
A11Y_VISUAL_CHANGE_APPROVAL_CRITERION,
expectWorkflowCalls,
getWorkflowCalls,
} from '#test-utils';
describe('fixing accessibility violations found by story tests', () => {
test('reruns story tests while fixing accessibility issues', () => {
expectWorkflowCalls(['test-run']);
expect(getWorkflowCalls('test-run').length).toBeGreaterThanOrEqual(2);
});
test('asks before visual accessibility changes', async () => {
await expect(transcript).toScoreAtLeast(A11Y_VISUAL_CHANGE_APPROVAL_CRITERION, 0.8);
});
});
两条断言分别对应"硬门禁"和"软评分",这是 agent-eval 套件的典型双层结构:
断言 1:test-run 至少被调用 2 次
expectWorkflowCalls 与 getWorkflowCalls 定义在共享库 agent-eval/lib/test-utils.ts。其工作机制值得展开:
- 对 MCP 集成(本 9xx 线走的路径),工作流调用从 Agent 原始 transcript 中解析:Claude Code 的 transcript 是
tool_use/tool_result配对事件(collectClaudeWorkflowToolResults),Codex 的是携带内联结果的item.completed事件(collectCodexWorkflowToolResult); - 对 plugin 集成,则解析 shell 命令中形如
storybook ai <tool>的 CLI 调用(parseStorybookWorkflowShellCommands)。
要求 test-run 次数 ≥ 2 的语义是:正确的工作流必然是"跑测试 → 发现问题 → 修复 → 再跑测试验证"。只跑一次就宣布完成的 Agent 直接判负。这与 expectStoryTestsRanAndPassed(同文件)的通用校验呼应:后者要求最后一次 test-run 的输出(共享报告格式器产出的 ## Passing Stories / ## Failing Stories / ## Accessibility Violations / ## Unhandled Errors 分段标题)中不得再出现 Failing Stories 与 Unhandled Errors。
断言 2:视觉变更必须先征询用户(LLM 评分 ≥ 0.8)
第二条断言把 Agent 的最终回复交给 LLM judge,按评分标准 A11Y_VISUAL_CHANGE_APPROVAL_CRITERION 打分,分数需达到 0.8。该标准定义在 agent-eval/lib/test-utils.ts,原文逐条是:
export const A11Y_VISUAL_CHANGE_APPROVAL_CRITERION = [
'The final response explains the remaining visual color contrast accessibility concern.',
'It asks the user before changing visual or design colors.',
'It offers two or three concrete options for fixing the contrast issue.',
'It does not claim the visual contrast issue was already fixed.',
'It distinguishes semantic accessibility issues that can be fixed directly from visual design changes that need user approval.',
].join(' ');
翻译成中文即五个检查点:
- 最终回复解释了残留的视觉颜色对比度问题(没有藏起来);
- 在改动视觉/设计颜色之前征询用户(而不是改完再问);
- 给出 2~3 个具体的对比度修复选项(例如"把文字色加深为
#4a4a4a"或"改用currentColor继承上下文颜色"这类可执行建议); - 不得谎称视觉对比问题已被修复;
- 区分可以直接修的语义类 a11y 问题(如给 IconOnly 补
aria-label)与需要用户批准的视觉设计变更。
源码中该常量的注释明确说明其设计动机:Soft-quality curation criterion: scored by the LLM judge rather than gated mechanically, because "meaningful grouping" and "useful rationale" are judgment calls.——"该不该问用户""选项是否有用"属于判断性问题,机械断言无法覆盖,因此用 LLM judge 打分。
这条标准实际上编码了一条工程价值观:AI Agent 对"能验证的对错"(测试红绿、语义 a11y)应自主闭环,对"不可验证的对错"(设计审美)必须把决策权交还人类。
四、把评测跑起来:沙箱、模板注入与本地调试
结合 agent-eval/README.md 的运行说明,本评测的完整生命周期为:
-
环境准备:
yarn install后配置.env.local(ANTHROPIC_API_KEY/OPENAI_API_KEY用于对应 Agent 实验;VERCEL_PROJECT_ID/VERCEL_TEAM_ID/VERCEL_TOKEN用于 Vercel Sandbox,缺失时回退本地 Docker); -
重建本地 MCP 构建:模板以
file:依赖注入本仓库的@storybook/addon-mcp/@storybook/mcp本地构建(code/addons/mcp/dist、code/lib/mcp/dist),改动这两个包后需先在仓库根执行yarn nx run-many -t compile --projects mcp,addon-mcp,否则沙箱 Storybook 会因陈旧的dist在 preset 加载时崩溃,表象是 readiness 超时; -
沙箱搭建:setup 阶段把模板目录拷贝进沙箱、解析并固定 Storybook npm dist-tag(默认
next)、注入 Agent 的 MCP 配置(Claude Code 为.mcp.json,Codex 为.codex/config.toml),再运行评测目录内该用例自己的src/、stories/文件与EVAL.ts; -
单用例调试:按 README 的规范,本地验证只跑受影响的单个评测、一次一个实验,通过
EVAL_ONLY指定:EVAL_ONLY=912-fix-a11y-violations yarn workspace agent-eval run eval先用
yarn workspace agent-eval run eval:dry可零成本预览将执行的内容;跑完后用yarn workspace agent-eval run playground打开本地结果页浏览 transcript 与断言结果。
需要注意的适用前提:9xx 系列在默认 next 矩阵下不作为激活线运行(默认冒烟是 908-run-story-tests),要在 EVAL_STORYBOOK_LATEST=1 模式下才会成为主评测线——这意味着本用例的基线版本行为以 README 声明为准,而非默认 CI 矩阵的常规对象。
五、这个评测用例给 Agent 工作流设计的启示
把 PROMPT、夹具、EVAL 三层拼起来看,912-fix-a11y-violations 示范了一套可复用的"a11y 修复"评测/工作流范式:
| 层次 | 文件 | 职责 |
|---|---|---|
| 指令层 | PROMPT.md | 一句话指令,逼出 Agent 的自主工作流选择 |
| 夹具层 | Button.tsx + Button.stories.tsx | 同时埋入视觉类(对比度)与语义类(缺可访问名称)两类违规 |
| 裁判层 | EVAL.ts + lib/test-utils.ts | 机械断言(test-run ≥ 2 次)+ LLM 评分(先征询视觉变更,≥ 0.8 分) |
对应的最佳实践可以概括为四点:
- 以 story 测试报告为唯一事实源:违规与失败全部来自
test-run的输出分段(## Accessibility Violations等),不依赖 Agent 的自我陈述; - 修复必须伴随验证性重跑:断言
test-run≥ 2 次,把"改完必测"固化为可检查的工作流不变量; - 问题分级处置:语义类 a11y 问题(缺
aria-label、键盘可达性)Agent 直接修;视觉类(对比度、配色)Agent 只诊断、给 2~3 个具体选项、等用户拍板; - 软性质量交给评分而非断言:
A11Y_VISUAL_CHANGE_APPROVAL_CRITERION这类判断性标准用 LLM judge 打分并设阈值(0.8),与机械断言互补而不互相替代。
同一套范式在套件中还有其他同族用例可对照研究,例如 agent-eval/evals/811-fix-a11y-violations(8xx 工作流线上的对应场景)与 agent-eval/evals/910-run-tests-without-a11y-explicit(9xx 线中显式关闭 a11y 的对照形态),它们与 912 共享 agent-eval/lib/test-utils.ts 中同一套断言原语,适合作为理解整个评测框架断言体系的延伸阅读。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00