首页
/ Storybook Agent 评测:docs-list + stories-preview 驱动的"文档先行"故事预览工作流

Storybook Agent 评测:docs-list + stories-preview 驱动的"文档先行"故事预览工作流

2026-09-06 12:18:33作者:咎岭娴Homer

本文围绕 Storybook 仓库中 agent-eval 评测套件里的一个具体评测用例 915-preview-story-by-id-docs-first 展开:它要求 AI Agent 在预览两个既有 Button 故事之前,先通过 Storybook MCP 拉取组件文档并做总结,且全程不得修改任何文件。读完本文,你将理解这个"文档先行"(docs-first)提示词的三条硬约束、它的验收断言如何逐条校验 Agent 的工具调用序列、底层 docs-list/stories-preview 两个 MCP 工具的实现依据,以及如何在本地单跑这个评测。

评测用例定位:agent-eval 中的 9xx MCP-only 系列

该用例位于 PROMPT.md,属于仓库根目录下的 agent-eval/ 工作区——一个"Agent 评测套件"(Agent Evaluation Suite)。README 对它有这样的定位:

Runs coding agents (Claude Code and Codex) against fixture projects in sandboxes and asserts that they follow the Storybook workflows this repo ships — writing stories, previewing or reviewing them, and running story tests through the MCP server or the plugin skills.

即:在沙箱中让编码 Agent 对 fixture 项目干活,并断言它们遵守本仓库内置的 Storybook 工作流。README 进一步说明了 9xx 系列与 8xx 系列的区别:

The 9xx evals are a trimmed MCP-only set for shapes the 8xx line does not cover (async mocks, story drift, tool params, preview-by-path/id, vitest CLI).

915-preview-story-by-id-docs-first 正属于其中的 preview-by-path/id(按路径/ID 预览)子主题,是 MCP 专属裁剪集,用来覆盖 8xx 主线没有的形状。它的姊妹用例 915-preview-story-by-id 只要求按 ID 预览;而本用例的提示词额外加了一条前置约束——先拉文档。这个差异正是本用例名称里 -docs-first 后缀的含义,也是它的验收逻辑与姊妹用例的关键分叉点。

提示词全文与三条硬约束

PROMPT.md 全文只有 5 行:

Show a preview of two existing Button stories: Primary and Secondary.

Do not modify any component or story files.

Before showing any previews, first use the Storybook MCP to pull documentation about the Button component and summarise it for me.

拆开看是三条要求:

  1. 按名预览两个既有故事:Primary 与 Secondary。注意提示词只给了故事导出名(export name),没有给 story ID——Agent 必须自己把 Primary 解析成合法的 story ID 才能调用 stories-preview 工具的 storyId 输入形态;
  2. 只读约束:不得修改任何组件或故事文件。这排除了"改故事把 ID 凑对"这类取巧路径,也保证评测可以安全地复用同一个 fixture;
  3. 文档先行(docs-first):在展示任何预览之前,必须先使用 Storybook MCP 拉取 Button 组件的文档并向用户总结。这条约束把"发现 story ID"这一动作显式绑定到文档工具链上,而不是让 Agent 靠读文件系统里的 .stories.tsx 猜。

配套的 package.json 声明了 fixture 模板:

{
  "name": "915-preview-story-by-id-docs-first",
  "type": "module",
  "evals": {
    "template": "reshaped-storybook"
  }
}

Fixture:Button 组件、故事文件与 story ID 的推导

用例自带最小 fixture。组件 Button.tsx 是一个受控的纯展示按钮:

type ButtonProps = {
  label: string;
  disabled?: boolean;
};

export default function Button({ label, disabled = false }: ButtonProps) {
  return (
    <button type="button" disabled={disabled} data-testid="button-component">
      {label}
    </button>
  );
}

故事文件 Button.stories.tsx 使用 CSF3 写法:

const meta = {
  title: 'Example/Button',
  component: Button,
  tags: ['test'],
  args: {
    label: 'Click me',
    disabled: false,
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = { args: { label: 'Primary' } };
export const Secondary: Story = { args: { label: 'Secondary' } };

其中两个字段直接决定了验收断言里期望的 story ID:

  • title: 'Example/Button' → 项目命名空间按 Storybook 惯例 kebab-case 化为 example-button
  • 导出名 Primary / Secondaryprimary / secondary

组合起来即 example-button--primaryexample-button--secondary(命名空间 + -- + 故事名,中间用双连字符分隔)。这个推导不是本文的猜测,而是被 EVAL.ts 中原样写死的期望值证实的。

fixture 模板 reshaped-storybookREADME 描述为"设计系统形状"的完整项目:Reshaped 组件、完整 Storybook(next tag)加本地 addon 构建、MSW 与 vitest 故事测试配置。README 还说明模板负责在 Agent 运行前把 Storybook 跑起来(reshaped-storybookpostinstall 启动),因此 Agent 进入沙箱时 MCP 端点已经可用。

验收断言:EVAL.ts 逐条解读

EVAL.ts 是 vitest 测试文件,断言从 Agent 运行后录下的工作流调用序列中提取事实。核心代码:

function includesStoryIds(call: StorybookWorkflowCall): boolean {
  return call.input.withStoryIds === true;
}

test('discovers story IDs before previewing by ID', () => {
  const previewCalls = getWorkflowCalls('stories-preview');
  expectWorkflowCalls(['docs-list', 'stories-preview']);
  expect(getWorkflowCalls('docs-list').some(includesStoryIds)).toBe(true);
  expect(previewCalls.some(workflowCallUsesStoryId)).toBe(true);
  expect(
    previewCalls.some((call) =>
      workflowCallIncludesStory(call, { storyId: 'example-button--primary' })
    )
  ).toBe(true);
  expect(
    previewCalls.some((call) =>
      workflowCallIncludesStory(call, { storyId: 'example-button--secondary' })
    )
  ).toBe(true);
});

四条断言分别对应提示词的三条约束:

断言 校验目标
expectWorkflowCalls(['docs-list', 'stories-preview']) 两个工具都至少被调用过一次(expectWorkflowCalls 对每个名字断言调用次数 > 0)
docs-list 的某次调用满足 input.withStoryIds === true Agent 拉文档时显式打开了 withStoryIds 参数,而不是只拿组件列表
stories-preview 的某次调用使用了 storyId 输入形态 Agent 走的是按 ID 预览,而不是按路径/导出名的其它形态
预览调用同时覆盖 example-button--primaryexample-button--secondary 两个故事一个不落

与姊妹用例 915-preview-story-by-id/EVAL.ts 对比可以看到 docs-first 带来的增量:基线版本只断言 expectWorkflowCalls(['stories-preview']),不要求 docs-list,也不检查 withStoryIds。也就是说,withStoryIds: true 这一参数是本用例独有、且必须命中的行为——Agent 不能"顺手"列出组件后直接预览,它必须请求带故事 ID 的文档输出。

底层 MCP 工具:docs-list 与 withStoryIds

这些断言之所以能写出来,是因为仓库里的 MCP 工具链本身就支持这套工作流。证据链如下:

1. withStoryIds 参数真实存在且有测试覆盖。 list-all-documentation.test.ts 中有专门用例 should include nested story IDs when withStoryIds is true,以 arguments: { withStoryIds: true } 调用 LIST_TOOL_NAME(即 docs-list 工具),断言返回结果包含嵌套的故事 ID。工具名 docs-listcode/lib/mcp/README.md 的工具列表中也有登记。

2. 参数存在的动机:让 Agent 少跑一次文件系统。 code/lib/mcp/CHANGELOG.md 记录了这次变更:

This change keeps existing path-based story inputs (absoluteStoryPath + exportName) while adding a storyId input shape for preview-stories and run-story-tests. It also adds withStoryIds to list-all-documentation and includes story IDs in get-documentation story sections, so agents can discover and reuse IDs directly without extra filesystem lookup steps.

即:preview-stories(评测侧称为 stories-preview 工作流)与 run-story-tests 新增了 storyId 输入形态;list-all-documentationdocs-list)新增 withStoryIdsget-documentation 的故事段落也会带上 story ID。三者配合,Agent 才能"直接从文档输出里拿到 ID",这正是提示词第 3 条想引导的行为。

3. 错误信息也在引导 Agent 回到这条路径。 当文档工具收到不完整的入参时,get-documentation-for-story.test.ts 期望的错误文案是:

Provide either storyId, or both componentId and storyName. Story ids are listed by the docs-list tool with withStoryIds: true and in docs-show output.

也就是说,MCP 服务端的报错本身就把 Agent 指回"用 docs-list + withStoryIds: true 发现 ID"这条路。工具协议、错误提示与评测断言三者口径一致,构成一个闭环。

评测如何解析 Agent 的调用序列

断言里的 getWorkflowCallsexpectWorkflowCallsworkflowCallUsesStoryId 都来自共享测试工具库 test-utils.ts,它决定了"一次工作流调用"是怎么被还原出来的(见 test-utils.ts):

  • plugin 集成(Claude Code/Codex 的插件技能实验),从 Agent 留下的 shell 命令日志中解析出 Storybook 工作流调用;
  • MCP 集成,解析 MCP 工具调用记录,并合并 Codex 的原始调用;
  • getWorkflowCalls(name) 按工作流名过滤,expectWorkflowCalls(names) 对每个名字断言"至少调用过一次"。

由于本用例位于 9xx 的 MCP-only 系列,实际生效的是后者:评测直接检查 Agent 发出的 docs-list / stories-preview 工具调用的 input 字段。

一个容易忽略的背景是 review 模式。test-utils.ts 中的 expectPreviewStoriesWithFinalLinks 说明了 review-off 场景的收尾方式:视觉类工作必须以 stories-preview 调用结束,且最终回复必须包含预览链接(正则匹配 ?path=/story//iframe.html?id=),同时不得出现 review 页面链接。README 解释了模式归属:插件实验恒为 review-on(storybook ai CLI 通道默认开启 review),而 MCP 实验默认 review-off,只有设置 EVAL_REVIEW=1 才打开 experimentalReview 特性开关。本用例的 EVAL.ts 直接断言 stories-preview 链接形态,与 MCP 默认 review-off 路径吻合。

如何单跑与调试这个评测

以下操作均来自 agent-eval/README.md 的既有说明,在仓库根目录执行:

  1. 预构建本地 MCP 包。沙箱会注入本 checkout 的 @storybook/addon-mcp / @storybook/mcp 构建,先编译再跑,避免过期的 dist 在 preset 加载阶段崩溃(表面症状是 readiness 超时而非构建错误):

    yarn nx run-many -t compile --projects mcp,addon-mcp
    
  2. 配置密钥。按 README 的 Setup,yarn install 后复制 .env.example.env.local,填入 ANTHROPIC_API_KEY(Claude Code 实验与失败分类)或 OPENAI_API_KEY(Codex 实验);沙箱默认 sandbox: 'auto',有 Vercel 凭证时走 Vercel Sandbox,否则回退本地 Docker。

  3. 选对评测线。README 说明默认只跑首个核心评测 801-create-component-no-launch-config;而 9xx 系列"never run on the default next matrix; under EVAL_STORYBOOK_LATEST=1 they become the active line"。因此针对本用例(9xx,MCP-only),按 README 的组合方式应形如:

    EVAL_STORYBOOK_LATEST=1 EVAL_ONLY=915-preview-story-by-id-docs-first yarn workspace agent-eval run eval
    

    其中 EVAL_ONLY 用于"一次只调试一个评测"(README 明确强调逐条调试,不要整线并行)。沙箱在运行时解析 Storybook npm dist-tag 并把精确版本钉进沙箱 package.jsonEVAL_STORYBOOK_LATEST=1 时钉 latest tag 并使用已发布的 MCP 包替代本地构建——这决定了 docs-list/stories-preview 的行为版本。

  4. 查看结果yarn workspace agent-eval run playground 启动本地结果查看器(README 指向 localhost 3000 端口),可浏览运行快照;fixture 模板被有意保留在快照里以便检查。CI 结果也可以用 yarn workspace agent-eval run results:download 拉取到本地 agent-eval/results

小结:"文档先行"验证的是一条完整工具链

915-preview-story-by-id-docs-first 的价值不在于提示词本身,而在于它把一条端到端工作流压缩成了可断言的调用序列:docs-listwithStoryIds: true)发现故事 ID → stories-previewstoryId 输入)精确预览两个故事 → 最终回复给出预览链接,且全程零文件写入。这条工作流的每一环在仓库中都有对应实现与测试:fixture 的故事文件提供了 ID 推导事实(Button.stories.tsx),MCP 包提供了参数与错误文案(list-all-documentation.test.tscode/lib/mcp/CHANGELOG.md),评测 harness 提供了调用序列的还原与断言(test-utils.ts),而 EVAL.ts 则把"先查文档、再按 ID 预览"固化为回归基线。对照基线用例 915-preview-story-by-id,两者之差恰好就是"必须先走文档工具并请求 story ID"这一个行为约束——这也是阅读此类提示词变体评测时最值得抓的线索:变体名即断言差量。

登录后查看全文
热门项目推荐
相关项目推荐