用MCP协议为AI写页面构建轻量设计系统约束
1. 一个月AI写页面的真实体验
1.1 从兴奋到崩溃:AI生成页面的蜜月期与阵痛期
最开始用 Cursor 配合 Claude 写页面的时候,那种感觉确实很上头。一个 Prompt 丢进去,几十秒就吐出来一个带样式的卡片组件,改改颜色、调调间距,基本能看。头一周我几乎把所有重复性的列表页、详情页都交给了 AI,效率提升肉眼可见,原本要写大半天的静态页面,现在一两个小时就能出个雏形。
但到了第二周,问题开始集中爆发。最典型的是同一个按钮,在首页长这样,在详情页又长成另一个样,圆角从 8px 变成 6px,主色调从品牌蓝偏成了紫蓝。原因很简单,每次对话都是独立的上下文,AI 并不知道你上一个页面里按钮是怎么定义的,它只会根据当前 Prompt 里的描述重新“猜”一个。你如果不在 Prompt 里把样式细节写死,它就自由发挥。
第三周我开始尝试用 Prompt 去约束它,比如“所有按钮统一使用 8px 圆角、主色 #2563EB、hover 时加深 10%”。这招短期有效,但 Prompt 越写越长,token 消耗飙升,而且一旦对话轮次多了,模型对早期约束的“记忆”会衰减,到后面又开始跑偏。我甚至遇到过invalid prompt: your prompt was flagged as potentially violating our usage p这种报错,排查半天发现是 Prompt 里某些描述触发了平台的过滤机制,跟技术本身没关系,纯粹是措辞问题。
第四周我彻底意识到,靠 Prompt 去维持一个项目的视觉一致性,本质上是在用自然语言做设计系统该做的事,这条路走不通。AI 擅长的是“从零生成一个能跑的东西”,不擅长“在已有约束下保持一致性”。这不是模型能力问题,是工作方式的问题。
1.2 问题的根子:AI 缺的不是能力,是约束
冷静下来复盘,我发现 AI 写页面翻车的场景高度集中在这几类:组件样式漂移、命名混乱(同一个东西叫 Card、Panel、Box 的都有)、重复造轮子(明明已经有 Button 了,它又给你写一个 PrimaryButton)、以及响应式断点各写各的。这些问题的共同点是——它们都不是“写代码”的问题,而是“遵守约定”的问题。
人类开发者为什么不会这样?因为我们脑子里有一套隐性的组件库:知道项目里有个 Button,知道颜色变量在哪定义,知道间距用的是 4 的倍数。AI 没有这套隐性知识,你给它多少上下文,它就用多少。所以真正要解决的不是“让 AI 写得更准”,而是“给 AI 一套它必须遵守的约束”。
这就引出了我后来用的方案:一个“不像组件库的组件库”。说它不像组件库,是因为它没有传统组件库那种厚重的 API 文档、没有几十个 props、没有复杂的主题系统。它更像是一份“给 AI 看的约定清单”,用最少的代码把设计 token、基础组件、命名规范固化下来,然后通过 MCP 协议喂给 AI,让它在生成任何页面之前先“读”这份约定。
2. 为什么传统组件库救不了AI写页面
2.1 传统组件库的“重”与AI工作流的冲突
我一开始也想过直接用现成的组件库,比如 Element Plus、Ant Design 这类。但实际用下来发现,传统组件库是为“人类开发者查阅文档后手动调用”设计的,不是为“AI 自动生成”设计的。它们的文档动辄几千页,props 几十个,AI 要在生成代码时准确调用,你得把相关文档片段塞进 Prompt,token 成本极高,而且经常塞不全。
更麻烦的是,传统组件库的样式是封装在内部的,AI 生成页面时如果直接调用组件,样式是统一的,但一旦它想“微调”一下,就会用内联样式或者覆盖类名,反而破坏了统一性。我见过 AI 生成的代码里同时存在<el-button>和手写的<button class="my-btn">,两套按钮并存,视觉上还不一致。
还有一个隐性成本:传统组件库的版本升级、主题定制都需要额外配置,AI 对这些配置的理解经常出错。你让它改个主题色,它可能去改 node_modules 里的文件,下次安装依赖就丢了。
2.2 “轻约定”思路:把设计系统压缩成AI能读懂的形态
我后来的思路是反过来的:不追求组件库的完整性和通用性,只追求“AI 能稳定遵守”。具体做法是把设计系统拆成三层,每层都用最简形态表达:
第一层是Design Token,用 CSS 变量定义颜色、间距、圆角、字号、阴影。比如--color-primary: #2563EB;、--space-4: 16px;。这些变量放在一个tokens.css里,AI 生成任何样式都必须引用变量,不许写死数值。
第二层是基础组件,只保留最高频的十来个:Button、Input、Card、Modal、Table、Tag、Tooltip、Dropdown、Tabs、Form。每个组件只暴露最必要的 props,样式全部基于 token。关键是每个组件都配一段简短的“使用说明”,用自然语言写清楚什么时候用、怎么用、有哪些变体。
第三层是命名与结构约定,比如文件放哪、组件怎么命名、样式怎么写、状态怎么管理。这部分用一份CONVENTIONS.md表达,配合 MCP 让 AI 在生成前先读。
这三层加起来代码量很小,一个下午就能搭完,但它解决的是 AI 生成时“没有约束”的根本问题。
3. 这个“不像组件库的组件库”到底长什么样
3.1 目录结构与核心文件
整个东西的结构非常扁平,我直接贴出来:
design-system/ ├── tokens.css # 设计变量 ├── components/ │ ├── Button.tsx │ ├── Input.tsx │ ├── Card.tsx │ ├── Modal.tsx │ ├── Table.tsx │ └── ... ├── CONVENTIONS.md # 给AI看的约定 ├── COMPONENTS.md # 组件使用说明 └── mcp-server.js # MCP服务入口tokens.css是根基,所有颜色、间距、圆角、字号、阴影都在这里定义。我用的命名规则是--{类别}-{语义}-{变体},比如--color-primary、--color-primary-hover、--space-sm、--radius-md。这样 AI 在生成样式时,只要看到color: var(--color-primary)就知道该用什么。
components/下的每个组件都遵循同一个模板:导入 token、定义 variants、导出组件。以 Button 为例,它只接受variant(primary/secondary/ghost/danger)、size(sm/md/lg)、disabled、loading这几个 props,样式全部用 token 拼出来,不写任何魔法数字。
CONVENTIONS.md是给 AI 看的核心文件,里面写清楚:新页面必须放在src/pages/下、组件必须从design-system/components导入、样式必须用 token、禁止内联样式、禁止覆盖组件内部类名、命名用 PascalCase 等。这份文件不长,但每一条都是踩坑后总结出来的。
3.2 MCP 协议怎么把约定“喂”给 AI
MCP(Model Context Protocol)在这里的作用,是让 AI 在生成代码前能主动“读取”这份约定。我写了一个简单的 MCP server,暴露两个能力:一个是get_conventions,返回CONVENTIONS.md的内容;一个是get_component_doc,传入组件名返回对应的使用说明。
配置好之后,Cursor 在生成页面时会先调用get_conventions,把约定加载进上下文,然后根据需要用get_component_doc查具体组件的用法。这样就不需要我在每次 Prompt 里重复粘贴约定,token 消耗大幅下降,而且约束的稳定性高很多。
这里有个实操细节:MCP server 返回的内容要尽量精简,只返回当前任务相关的部分。比如生成一个表单页,就只返回 Form、Input、Button 的文档,不要把整个组件库都塞进去。我一开始图省事返回全部,结果上下文被撑爆,AI 反而抓不住重点。
3.3 为什么它“不像组件库”反而是优势
传统组件库追求的是“覆盖所有场景”,所以 API 设计得很全,文档写得很厚。但这个“全”对 AI 来说是负担。我这个东西只覆盖 80% 的高频场景,剩下 20% 的特殊需求,AI 可以基于 token 和基础组件自己组合,反而更灵活。
另一个优势是“可读性”。传统组件库的源码经过打包、压缩,AI 读起来很费劲。我这个东西就是普通的 TSX 和 CSS,AI 能直接读懂实现,生成代码时更容易保持一致。我甚至发现,AI 在读过 Button 的实现后,生成 Card 时会主动复用同样的 token 和结构,这就是“可读性”带来的隐性收益。
4. 落地实操:从零搭一套给AI用的设计系统
4.1 第一步:定义 Token,把设计决策固化下来
Token 是整个系统的地基,定义得好不好直接决定后面顺不顺。我的做法是先梳理项目里最高频的视觉决策,然后逐个定义。颜色方面,我定义了 primary、success、warning、danger、neutral 五个语义色,每个色配一个 hover 和一个 active 变体。间距用 4 的倍数,从 4px 到 64px 共 8 档。圆角分 sm(4px)、md(8px)、lg(12px)、full(9999px) 四档。字号从 12px 到 32px 共 7 档。
定义的时候有个原则:宁可少而精,不要多而杂。我一开始定义了 20 多个颜色变量,结果 AI 反而不知道该用哪个。后来砍到 15 个以内,每个都有明确的语义,AI 的选择准确率明显提升。
:root { --color-primary: #2563EB; --color-primary-hover: #1D4ED8; --color-primary-active: #1E40AF; --color-success: #16A34A; --color-warning: #D97706; --color-danger: #DC2626; --color-neutral-50: #F9FAFB; --color-neutral-100: #F3F4F6; --color-neutral-500: #6B7280; --color-neutral-900: #111827; --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-6: 24px; --space-8: 32px; --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-full: 9999px; --font-sm: 12px; --font-base: 14px; --font-lg: 16px; --font-xl: 20px; --font-2xl: 24px; }提示:Token 命名一定要带语义,不要用
--blue-500这种纯色名。AI 看到--color-primary知道这是主色,看到--blue-500还得猜它是不是主色,多一层推理就多一层出错概率。
4.2 第二步:写基础组件,每个都配“AI说明书”
组件不用多,十个左右就够。关键是每个组件都要配一份简短的说明,告诉 AI 什么时候用、怎么用。这份说明我写在COMPONENTS.md里,格式统一,每条不超过 5 行。
以 Button 为例,说明是这样的:
## Button 用途:触发操作,如提交、取消、删除。 变体:primary(主操作,一页最多一个)、secondary(次要操作)、ghost(弱操作)、danger(危险操作)。 尺寸:sm、md(默认)、lg。 禁止:不要用 Button 做导航,导航用 Link。 示例:<Button variant="primary" onClick={handleSubmit}>提交</Button>这种写法比传统的 props 表格更适合 AI 阅读,因为它用自然语言表达了“什么时候用”这个决策逻辑,而不只是“有哪些参数”。
组件实现上,我坚持一个原则:样式全部来自 token,不写任何硬编码数值。这样即使 AI 生成的页面里混用了组件和自定义样式,只要自定义样式也引用 token,整体视觉就是一致的。
// Button.tsx import './Button.css'; type ButtonProps = { variant?: 'primary' | 'secondary' | 'ghost' | 'danger'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; loading?: boolean; children: React.ReactNode; onClick?: () => void; }; export function Button({ variant = 'primary', size = 'md', disabled, loading, children, onClick, }: ButtonProps) { return ( <button className={`btn btn-${variant} btn-${size}`} disabled={disabled || loading} onClick={onClick} > {loading ? '加载中...' : children} </button> ); }/* Button.css */ .btn { border: none; border-radius: var(--radius-md); font-size: var(--font-base); cursor: pointer; transition: background-color 0.15s ease; } .btn-primary { background: var(--color-primary); color: white; } .btn-primary:hover { background: var(--color-primary-hover); } .btn-primary:active { background: var(--color-primary-active); } .btn-sm { padding: var(--space-1) var(--space-3); font-size: var(--font-sm); } .btn-md { padding: var(--space-2) var(--space-4); } .btn-lg { padding: var(--space-3) var(--space-6); font-size: var(--font-lg); }4.3 第三步:写约定文档,把“潜规则”变成“明规则”
CONVENTIONS.md是整个系统里最容易被忽视但最重要的文件。它把项目里那些“大家都知道但没人写下来”的规则明确化,让 AI 有据可依。我这份文档包含以下几块:
文件组织:页面放src/pages/,组件放src/components/,工具函数放src/utils/。每个页面一个文件夹,包含index.tsx和styles.css。
命名规范:组件用 PascalCase,函数用 camelCase,常量用 UPPER_SNAKE_CASE,CSS 类名用 kebab-case。文件名和组件名保持一致。
样式规则:所有颜色、间距、圆角、字号必须用 token,禁止硬编码。禁止内联样式,禁止用!important,禁止覆盖组件内部类名。
组件使用:优先使用design-system/components下的组件,没有合适的再自己写。自己写的组件也要遵循同样的 token 和命名规则。
状态管理:局部状态用useState,跨组件状态用 Context,服务端状态用 React Query。禁止在组件里直接发请求。
这份文档我控制在 200 行以内,太长了 AI 读不完,太短了约束不够。写完之后的感受是:以前这些规则都在我脑子里,现在写出来了,不仅 AI 能遵守,新来的同事也能快速上手。
4.4 第四步:接 MCP,让 AI 主动读约定
MCP server 的实现不复杂,核心就是暴露几个方法让 AI 调用。我用 Node.js 写了一个最简单的版本:
// mcp-server.js import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import fs from 'fs'; const server = new Server( { name: 'design-system', version: '1.0.0' }, { capabilities: { tools: {} } } ); server.setRequestHandler('tools/list', async () => ({ tools: [ { name: 'get_conventions', description: '获取项目的编码约定和设计系统规则', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_component_doc', description: '获取指定组件的使用说明', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'], }, }, ], })); server.setRequestHandler('tools/call', async (request) => { const { name, arguments: args } = request.params; if (name === 'get_conventions') { return { content: [{ type: 'text', text: fs.readFileSync('CONVENTIONS.md', 'utf-8') }], }; } if (name === 'get_component_doc') { const doc = fs.readFileSync('COMPONENTS.md', 'utf-8'); const section = doc.split('##').find((s) => s.startsWith(` ${args.name}`)); return { content: [{ type: 'text', text: section || '未找到该组件' }] }; } }); const transport = new StdioServerTransport(); await server.connect(transport);配置到 Cursor 的 MCP 设置里之后,AI 在生成页面前会先调用get_conventions,需要具体组件时再调用get_component_doc。实测下来,生成代码的合规率从之前的六成左右提升到了九成以上。
注意:MCP server 返回的内容要控制长度。我一开始把整个
COMPONENTS.md都返回,结果上下文被占满,AI 反而忽略了约定。后来改成按需返回单个组件,效果好很多。
5. 实测效果与踩坑记录
5.1 一致性提升:从“每次都不一样”到“基本统一”
接入这套系统之后,最直观的变化是页面之间的一致性。以前同一个按钮在不同页面有五六种样式,现在基本统一。我统计过一组数据:在接入前,AI 生成的 20 个页面里,按钮样式出现了 14 种不同的圆角和 9 种不同的主色;接入后,同样 20 个页面,按钮样式只有 2 种(primary 和 secondary),圆角和主色完全一致。
这个提升不是靠 Prompt 写得更细,而是靠约束前置。AI 在生成之前就知道有哪些 token、有哪些组件、该怎么用,自然就不会乱来。
另一个变化是代码审查的时间大幅缩短。以前 AI 生成的代码我要逐行检查样式,现在主要看逻辑,样式部分基本可以信任。这省下来的时间,够我多写两个页面。
5.2 踩过的坑:Token 命名、组件粒度和 MCP 配置
坑一:Token 命名太抽象。我一开始用了--color-1、--color-2这种命名,想着灵活。结果 AI 完全不知道该用哪个,生成的页面颜色五花八门。后来改成语义命名,问题立刻解决。教训是:给 AI 用的东西,语义清晰比灵活更重要。
坑二:组件粒度太细。我一度把 Button 拆成 IconButton、TextButton、LinkButton 好几个,结果 AI 经常选错。后来合并成一个 Button,用 variant 区分,选择准确率反而高了。组件不是越多越好,够用就行。
坑三:MCP 配置路径问题。MCP server 的路径要用绝对路径,相对路径在某些环境下会找不到。我在这上面卡了半小时,最后发现是路径写错了。另外 server 启动失败时 Cursor 不会给明显提示,要去日志里翻,这点比较坑。
坑四:约定文档更新后 AI 不知道。MCP server 每次调用都是实时读文件,所以更新CONVENTIONS.md后 AI 下次生成就会用新版本。但如果 server 是常驻进程且做了缓存,就要重启。我建议不要做缓存,每次读文件,文件不大,开销可以忽略。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| AI 生成的样式不一致 | 未接入 token 或约定未加载 | 检查生成代码是否引用 token | 确认 MCP 已配置且get_conventions被调用 |
| AI 不用现有组件,自己写 | 组件文档未返回或描述不清 | 查看 MCP 日志确认get_component_doc是否被调用 | 完善COMPONENTS.md,补充使用场景说明 |
| MCP server 启动失败 | 路径错误或依赖缺失 | 查看 Cursor MCP 日志 | 用绝对路径,确认依赖已安装 |
生成代码报invalid prompt | Prompt 措辞触发过滤 | 检查 Prompt 中的敏感词 | 换一种表述,避免歧义措辞 |
| 上下文被撑爆 | MCP 返回内容过长 | 查看 token 消耗 | 按需返回,不要一次性返回全部文档 |
| 组件样式被覆盖 | AI 用了内联样式或!important | 搜索生成代码中的style=和!important | 在约定中明确禁止,并在审查时拦截 |
6. 后续扩展与个人体会
6.1 可以继续做的几件事
这套系统跑通之后,我发现还有几个方向可以继续优化。一是把常用的页面模板也固化下来,比如列表页、表单页、详情页,让 AI 基于模板生成,进一步减少自由发挥的空间。二是把测试也纳入约定,让 AI 生成页面的同时生成对应的测试用例,保证质量。三是把设计稿和 token 打通,从 Figma 直接导出 token,避免手动维护两套。
另外,MCP 的能力不止于读文档,还可以扩展成“校验器”。比如 AI 生成代码后,调用一个validate_code方法,检查是否用了未定义的 token、是否覆盖了组件样式,有问题直接反馈给 AI 让它重写。这个思路我觉得挺有潜力,相当于给 AI 加了一道自动审查。
6.2 我个人的一点体会
用 AI 写页面这一个月,最大的收获不是“AI 能写代码”,而是“AI 需要什么样的约束才能写好代码”。以前我总觉得 Prompt 写得好就行,后来发现 Prompt 只是入口,真正的功夫在入口之前——你得先把项目的约定、规范、设计系统整理清楚,AI 才能稳定输出。
这个“不像组件库的组件库”,本质上就是把人类开发者的隐性知识显性化,然后用 AI 能理解的方式喂给它。它不追求大而全,只追求 AI 能稳定遵守。事实证明,这个思路是对的。现在我的工作流是:AI 负责生成,约定负责约束,我负责审查和调整。三者配合,效率比纯手写高不少,质量也比纯 AI 生成稳得多。
如果你也在用 AI 写页面,并且遇到了类似的一致性问题,我建议你先别急着优化 Prompt,先花半天时间把项目的 token 和约定整理出来。这一步做完,后面的事情会顺很多。