

Key Takeaways
- 通用 AI 生成 UI 已出现明显同质化, 业内以 slop 一词概括
- 用 观察 描述传统 prompt 直出的 slop 链路
- Skill 已成 Agent 工程化的事实标准
- 单条 prompt 无法承载完整产品意图
- 长 Agent 会话最大的失败模式是上下文蒸发
- 真实应用含未经打磨的边角, 模板则过度干净
- 直接克隆站点会丢失原组件的依赖关系
- 把克隆代码与自定义 spec 直接合并必然冲突
引言: 重新认识 AI Slop 与 Vibe Coding 治理

一、AI Slop:同质化浪潮下的视觉税
过去十二个月,通用大模型驱动的 UI 生成已经形成一种可识别的"视觉税"(slop):同样的圆角、同样的玻璃拟态卡片、同样的渐变 hero 区、同样的 AI 配色------所有页面看起来都像是从同一台机器里印出来的。工程师社区开始用 slop 这个词来概括这种"低信噪比、缺上下文"的生成内容,它和传统意义上的代码冗余不同:slop 不是 bug,而是一种审美同质化 + 上下文缺位的复合现象。
| 维度 | 传统 Lorem Ipsum 占位 | AI Slop 占位 |
|---|---|---|
| 外观 | 灰色色块,中性 | 完整样式,高完成度 |
| 内容 | 无意义文本 | 结构完整但语义空洞 |
| 可识别度 | 一眼可辨 | 容易误判为"成品" |
| 下游风险 | 低,仅审美 | 高,污染 PR Review 与业务决策 |
这张表的关键不在表头,而在最后一行:AI 生成的内容越像成品,review 时越容易被 skip。这是 slop 在工程流水线里最具破坏力的副作用------它把"必须看"的 review 环节悄悄降级成"可以扫"的快速浏览,导致上线后的页面常常出现文案失真、信息密度错位、品牌一致性塌陷三类问题。
二、Vibe Coding 与传统 Prompt 工程的边界
在进入治理框架之前,有必要先把 Vibe Coding 这个概念和工程师熟悉的"传统 prompt 工程"做一个明确的边界划分。Vibe Coding 强调"以自然语言描述意图,由 Claude Code 这类 Coding Agent 自主完成 Plan → Build → Review 闭环",而传统 prompt 工程更多停留在"人写代码、模型补全片段"的协作模式里。两者最关键的差异在于上下文载体从代码迁移到了 spec/blueprint。
| 取舍维度 | 传统 Prompt 工程 | Vibe Coding |
|---|---|---|
| 角色分工 | 工程师主导,模型辅助补全 | 工程师编排意图,Agent 主导实现 |
| 上下文载体 | 代码 + 内联注释 | spec / blueprint / Plan mode 输出 |
| Review 粒度 | diff 级,行级审视 | spec 级契约 + diff 级双层审视 |
| 失败模式 | 补全偏移、幻觉片段 | 上下文污染、过度依赖、token 泄漏 |
| 学习曲线 | 适配 IDE 提示词 | 重塑工程师的"拆需求"能力 |
| 工具锚点 | Copilot / Cursor 内联 | Claude Code + MCP connector 编排 |
观察 这张取舍矩阵的关键在于 review 粒度的迁移:Vibe Coding 把 review 的重心从"行级 diff"上移到"spec 级契约",这意味着工程师必须先学会写 spec,才能真正用好 Agent。Spec 不再是产品经理的专利,而是工程师每天都要交的第一份产出。Spec 的质量直接决定了 Agent 第一轮产出的可信度,以及第二轮 review 的成本。
三、Slop 不是工具问题,是上下文缺失问题
社区里很容易把 slop 归罪于"模型不够强"或"工具不够好",但这是一种偷懒的解释。观察 如果同样一套 Claude Code 配上不同的 spec 契约,产出的 UI 风格、信息密度、可维护性可以相差一个数量级以上------这说明 slop 的根因不在模型本身,而在输入到模型里的上下文质量。模型只是把工程师的上下文缺口"翻译"成视觉语言,缺口越大,翻译出来的 slop 越刺眼。
具体来说,缺失的上下文至少包含四层:
- 业务上下文:这个页面服务于什么用户、解决什么痛点、转化漏斗在哪一环、目标 CTA 是什么。
- 设计上下文:品牌色 token、字体家族、组件库、动效节奏、栅格系统、响应式断点。
- 工程上下文:Next.js App Router 还是 Pages Router、Tailwind 还是 CSS Modules、shadcn/ui 还是 Radix 裸用、TypeScript 严格度。
- 约束上下文 :性能预算(LCP / INP / CLS 阈值,详见 web.dev/vitals/)、无障...%25E3%2580%2581%25E6%2597%25A0%25E9%259A%259C%25E7%25A2%258D%25E7%25AD%2589%25E7%25BA%25A7%25E3%2580%2581SEO "https://web.dev/vitals/)%E3%80%81%E6%97%A0%E9%9A%9C%E7%A2%8D%E7%AD%89%E7%BA%A7%E3%80%81SEO") 元信息、可访问性文案长度。
四层上下文缺任何两层,Agent 都会用它的"先验"来补,而先验恰恰就是 slop 的来源。先验越强、上下文越弱,产出的页面越像"标准答案"------而"标准答案"从来都不是产品,只是模板。
四、五步治理框架:全篇索引
基于上述分析,本指南后续章节会围绕一个五步治理框架展开,这是阅读全篇的索引:
- Spec 契约:把意图写成可被 Agent 解析的 blueprint,固化业务/设计/工程/约束四层上下文。
- Plan mode 锁定:在 Claude Code 进入动手阶段前,先冻结方案,避免 Agent 在中途漂移。
- Review mode 校验 :用 diff + 测试(Vitest/Playwright)双轨验证 Agent 输出,详见 vitest.dev/guide/ 与 playwright.dev/docs/intro。
- Context hygiene :管理
.env、token、context window,避免上下文污染与 token 泄漏。 - 工程师角色翻转:从"写代码"转向"拆需求 + 编排工具",让 Agent 流水线真正跑起来。
五步之间不是串行流水线,而是双环 :外环是 Spec → Plan → Build → Deploy,内环是 Review → Test → Context hygiene。后续章节会按这个双环展开,本文先建立宏观心智。Claude Code 官方文档(docs.anthropic.com/en/docs/cla...%25E5%25AF%25B9 "https://docs.anthropic.com/en/docs/claude-code/overview)%E5%AF%B9") Plan mode 与 Review mode 也有原生支持,这是工具层与治理层的天然对齐。
五、一个最小可运行的 spec 示例
为了让"spec 契约"这个抽象概念落地,这里给出一段伪代码片段,展示工程师在动手前应该先交付的最小 spec:
yaml
# spec.hero.yaml --- 单一事实来源
section: hero
intent: 让访客在 3 秒内理解"这是一个零基础 Vibe Coding 课程"
audience: 中文母语、非前端工程师、想用 Claude Code 上线第一个 Web App
constraints:
framework: Next.js (App Router)
styling: Tailwind CSS + shadcn/ui
motion: Framer Motion,入场动画不超过 600ms
budget:
LCP: < 2.0s
INP: < 200ms
copy:
headline_max_chars: 18
subheadline_max_chars: 48
cta: "开始 23 分钟实战"
assets:
hero_image: /public/hero.png (must be < 200KB)
i18n:
default_locale: zh-CN
fallback: en-US
review:
owner: frontend-lead
required_approvals: 1
这段 spec 是后续 Agent 调用的单一事实来源 (single source of truth),任何对 hero 区的修改都必须先回到这份 yaml。它不是 IDE 插件,不是 prompt 模板,而是工程纪律 。Next.js 官方文档(nextjs.org/docs)也强调配置文...%25E4%25B9%259F%25E5%25BC%25BA%25E8%25B0%2583%25E9%2585%258D%25E7%25BD%25AE%25E6%2596%2587%25E4%25BB%25B6%25E5%25BA%2594%25E8%25AF%25A5%25E6%2588%2590%25E4%25B8%25BA%25E9%25A1%25B9%25E7%259B%25AE%25E7%259A%2584%2522%25E5%258F%25AF%25E8%25AF%25BB%25E5%25A5%2591%25E7%25BA%25A6%2522%2Cspec "https://nextjs.org/docs)%E4%B9%9F%E5%BC%BA%E8%B0%83%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E5%BA%94%E8%AF%A5%E6%88%90%E4%B8%BA%E9%A1%B9%E7%9B%AE%E7%9A%84%22%E5%8F%AF%E8%AF%BB%E5%A5%91%E7%BA%A6%22,spec") 与此精神完全一致。
六、中国大陆工程师为什么要建立 spec 契约习惯
数据 对于中国大陆的工程师团队而言,spec 契约习惯的紧迫性比海外团队更高。原因有三:
- 模型语境差异 :Anthropic Claude、OpenAI GPT 系列在中文场景下的"先验审美"更倾向于国际化极简风,直接套用到中文产品上,容易出现"信息密度过低、转化文案过长、字号过小"的不匹配。一段 18 字符以内的英文 headline,翻译成中文往往要 24 字以上,如果 spec 里不显式声明
headline_max_chars,Agent 会按英文节奏裁剪,最终落到页面上就出现断行错乱。 - 合规与备案:境内上线产品需要 ICP 备案、内容审核、可识别的开发者信息、必要的实名跳转链接,这些约束必须在 spec 阶段就被显式写入,否则 Agent 生成的页面会在 review 阶段被整段打回,造成返工成本指数级放大。
- 团队协作粒度:国内多数团队仍以"前端 + 后端 + 产品"三段式分工为主,引入 Agent 后如果不先固化 spec 契约,最容易出现"前端用 Agent 写、后端看不懂、改不动、测试无法覆盖"的协作裂缝,反而把 Vibe Coding 的敏捷优势抵消殆尽。
因此,spec 契约不是可选项,而是引入 Vibe Coding 流水线前的硬性基础设施。本指南会在后续章节反复回到这个论点,把它当作不可妥协的工程基线。
七、锚定 2026:前端工程与 Agent 工程的融合方向
最后,把视野放到 2026 年的工程演进图景上。前端工程和 Agent 工程正在经历一次底层融合:
- 前端侧:Next.js App Router、Tailwind CSS、shadcn/ui 这套"工程化组件栈"已经稳定成为 Claude Code 的首选脚手架,React Server Components 与 streaming 渲染也逐步进入主流实践。
- Agent 侧 :Model Context Protocol(MCP,详见 modelcontextprotocol.io/)正在成为%25E6%25AD%25A3%25E5%259C%25A8%25E6%2588%2590%25E4%25B8%25BA "https://modelcontextprotocol.io/)%E6%AD%A3%E5%9C%A8%E6%88%90%E4%B8%BA") Agent 与外部工具对接的事实标准。GitHub MCP Server 仓库(github.com/modelcontex...%25E5%25B7%25B2%25E7%25BB%258F%25E6%258A%258A%25E4%25BB%2593%25E5%25BA%2593%25E8%25AF%25BB%25E5%2586%2599%25E3%2580%2581PR "https://github.com/modelcontextprotocol/servers)%E5%B7%B2%E7%BB%8F%E6%8A%8A%E4%BB%93%E5%BA%93%E8%AF%BB%E5%86%99%E3%80%81PR") 流转、issue 同步封装成标准 connector,让 Agent 可以直接操作真实仓库。
- 融合点 :工程师不再区分"前端工程师"和"AI 工程师",而是统一为 AI-Native Web Engineer------既懂 Web Vitals,又懂 token budget;既会写 Next.js 页面,又会编排 MCP connector;既会读 diff,又会写 spec。
观察 这场融合的胜负手,不在工具,而在工程师能否守住 spec 这条护城河。工具每个月都在换,但 spec 契约能力是十年级别的资产。建议读者把后续章节当作 spec 能力的训练营,而不仅仅是 Vibe Coding 工具教程。当你能稳定地写出可被 Agent 解析、可被人类 review、可被下游消费的三向契约,你就已经赢过了 80% 的 Vibe Coding 实践者。
Vibe Coding 五步法全景图

观察 把"做个 SaaS landing page"直接丢给 Claude Code / Cursor / Codex,几秒钟之内确实能拿到一份能跑起来的 Next.js 页面,但这条链路有一个非常隐蔽的代价------它输出的不是"产品",而是 slop。同一个 prompt 在不同 session 里产出的 hero 区配色、卡片圆角、阴影强度、CTA 文案、字体层级高度雷同,因为这些 Agent 共享同一组默认视觉先验和训练分布里的高频模式。换句话说,你以为是 prompt 在驱动结果,实际上 prompt 只是在采样一条预训练里早就收敛好的均值路径。传统 prompt 直出的链路可以抽象为五段:用户短句 → Agent 默认系统提示 → 模板化脚手架(create-next-app、shadcn 默认主题、Tailwind 默认调色板) → 同质化输出 → 用户再补 prompt 微调 → 下一轮更深的同质化。这个循环每多走一次,系统就在视觉税的路上走得越远,而用户以为"自己在迭代",其实只是在均值附近做布朗运动。
五步法拆解:从需求到可上线的语义闭环
把 vibe coding 从"随便聊聊"升级成工程流水线,关键在于把模糊意图强制收敛成可验证的中间产物。下面这套五步法把整个过程切成五个语义边界清晰的阶段,每一步都有自己的输入契约、输出契约和退出条件。
1. 访谈 (Interview)。这一步不是"开始写代码",而是用结构化提问把业务背景、目标用户、关键场景、转化指标、视觉气质、可访问性约束全部问出来。Claude Code 的 Plan mode 在这一阶段最有用:它把对话从"代码生成"切到"需求澄清",强制 Agent 先提问、再输出方案。访谈的核心输出是一份对话纪要 + 一份初步的 spec 草稿,而不是任何代码片段。提问的颗粒度决定了后续 Agent 自由发挥的空间------问得越细,slop 概率越低。
2. 克隆 (Clone) 。拿到访谈结果后,需要选一个 reference------可以是同行业的成熟站点,也可以是设计师的 Figma,也可以是 GitHub 上的开源模板。Claude Code 在这一步通常会通过 GitHub MCP server(参考仓库 github.com/modelcontex...%25E7%259B%25B4%25E6%258E%25A5 "https://github.com/modelcontextprotocol/servers)%E7%9B%B4%E6%8E%A5") fork 或 clone 参考仓库,把它作为视觉锚点。这一步的关键不是"抄代码",而是把参考站点的布局骨架、组件层级、信息密度、节奏感固化下来,作为后续合并的输入。参考仓库在这里承担"对照样本"的角色,而不是"复制源"。
3. 合并 (Merge) 。把访谈纪要 + 克隆下来的参考代码放到同一个 spec.md 里,由 Agent 合并出第一版项目骨架。这里的合并是语义级别的:不是把两段 CSS 拼接起来,而是让 Agent 理解"我要做的产品"+"参考站点的样式语义",然后用 Next.js + Tailwind + shadcn + Framer Motion 这套零基础组合重新生成。spec.md 此时第一次成型,文件结构大致包括:产品定位、目标用户、关键场景、页面清单、组件清单、视觉规范、动效清单、可访问性要求、上线标准九个章节。Next.js 项目骨架可以参考 nextjs.org/docs,Tailwi... 规范参考 tailwindcss.com/docs。
4. 重构 (Refactor) 。第一版骨架几乎肯定有冗余:命名不一致、组件职责混乱、动效过度、配色不收敛、重复 utility class。这一步让 Agent 主动删减、统一变量、抽公共组件,并通过 Vitest / Playwright 自动写测试,验证关键交互链路没被改坏。具体测试写法可以参考 Playwright 文档 playwright.dev/docs/intro 与 Vitest 文档 vitest.dev/guide/ 来约束。重构阶段不允许新增视觉特性,只允许删减和收敛。
5. 动画 (Animation) 。最后一步才上动效。用 Framer Motion 在 hero、卡片悬浮、滚动揭示、菜单展开等位置加微动画,所有动效必须可被 prefers-reduced-motion 媒体查询关闭,避免动效叠加造成新的视觉税。动效是 spec.md 里独立的一节,而不是写到一半临时加的补丁------一旦允许 Agent 在重构阶段自由发挥动效,几乎一定会出现 hero 区自转动 + 按钮脉冲 + 卡片悬浮浮起三层动画叠加的低信号场景。
spec.md:Agent 行为的唯一真理源
数据 在 vibe coding 实践中,80% 以上的"返工"来自同一份需求被 Agent 以不同方式理解。spec.md 的作用就是把"口头意图"固化成一份机器可读、人类可审、人机共信的契约。它的最小骨架通常包括:产品定位、目标用户、关键场景、页面清单、组件清单、视觉规范、动效清单、可访问性要求、上线标准。任何后续的 prompt、Plan、Review 都必须以 spec.md 为锚点,Agent 不在 spec.md 之外做任何自由发挥。一旦 spec.md 写定,Claude Code 的 Plan mode、Cursor 的 spec 视图、Codex CLI 的 --spec 参数都可以直接消费它,跨 Agent 复用性极高;团队里新加入的工程师也只需要读懂 spec.md,就能判断后续每个 PR 是否偏离了原始意图。

spec-driven vs copy-paste 范式
| 维度 | spec-driven | copy-paste |
|---|---|---|
| 输入形式 | 结构化 spec.md + 参考仓库 | 自由 prompt + 截图粘贴 |
| Agent 行为边界 | 受 spec 强约束,偏离即提示 | 无约束,自由发挥 |
| 可复现性 | 高,换 Agent 也能继续 | 低,换 session 就丢上下文 |
| review 成本 | 低,只需审 spec 与 diff | 高,需要逐行审代码 |
| 视觉税风险 | 中,可被 spec 收敛 | 高,默认模板持续污染 |
| 适合团队规模 | 任意 | 单人探索 |
| 上下文可移植性 | 强(spec 是文件) | 弱(prompt 在 session 内) |
| 失败回滚成本 | 低(spec + git 双保险) | 高(全靠 session 历史) |
对比之下,spec-driven 范式把"知识"从 Agent 的大脑里搬到仓库里,让 vibe coding 第一次具备工程意义上的可审计性。
多工作树并行,避开文件冲突
Vibe coding 的一个反直觉点:同时让 Agent 在多个 worktree 里推进,比串行推进更安全。每个 worktree 绑定一个子任务(例如 worktree-A 负责 hero,worktree-B 负责 pricing,worktree-C 负责 FAQ),Agent 在各自目录里只读共享 spec.md,只写自己的文件子树。这样做有三个收益:第一,文件冲突被隔离在边界内,不同 Agent 不会互相覆盖对方的产物;第二,任意一个 worktree 翻车可以直接丢弃而不影响主线;第三,review 可以并行进行,合并时通过 PR 走 GitHub Actions 校验,流程参考 docs.github.com/en/actions。... Code 的工作目录切换、Cursor 的 workspace 隔离、Codex CLI 的 worktree 参数都支持这种模式,具体可以参考 Claude Code 文档 docs.anthropic.com/en/docs/cla...
治理闭环:可复用模板
bash
# 1. 锁定 spec
$ claude --version # 自检 Agent 状态
$ claude doctor # 鉴权 + MCP 连接性自检
$ cat spec.md | claude # 把 spec 作为唯一上下文喂入
# 2. 多 worktree 并行
$ git worktree add ../wt-hero -b feat/hero
$ git worktree add ../wt-pricing -b feat/pricing
$ git worktree add ../wt-faq -b feat/faq
# 3. 每个 worktree 内独立 Plan -> Build -> Review
$ cd ../wt-hero
$ claude "按 spec.md 实现 hero,完成后跑 vitest"
# 4. 合并回主线 + 自动化校验
$ git checkout main
$ git merge feat/hero feat/pricing feat/faq
$ gh pr create --base main --title "vibe: 五步法首版"
$ gh pr checks watch # 等 GitHub Actions 全绿
观察 这套治理闭环的核心不是"用哪个 Agent",而是"谁拥有上下文"。spec.md 的 owner 是人,不是 Agent------人决定要不要更新 spec,Agent 只能消费 spec。这种权力分配让 vibe coding 摆脱了"prompt 漂移"的恶性循环,也让团队里的非工程师可以无门槛参与 review,因为他们只需要读懂 spec.md,不需要读懂 React 组件树。当 spec.md 成为团队唯一的真理源,五步法就从个人技巧升级成可治理、可复盘、可交接的工程流程------这正是 vibe coding 从"玩具"走向"产线"的分水岭。
环境前置: Claude Code / Codex CLI 与 Skill 体系
观察 Skill 体系已经成了 Agent 工程化的事实标准。把同一段"做个 SaaS landing page"的 prompt 分别丢给 Claude Code、Codex CLI、Cursor、Copilot,四家产品在语义层都能理解你的意图,但在工程层走出的是四条截然不同的路径:Claude Code 与 Codex CLI 站在命令行这一侧,把 Agent 视作"可读写本地文件系统的长驻子进程";Cursor 与 Copilot 站在 IDE 这一侧,把 Agent 嵌进编辑器面板、强调 inline edit 的低延迟。Skill 本质是一组带 YAML frontmatter 的 Markdown 文件,运行时把它识别成"可调用的工具/上下文片段"------这是一种把"系统提示词"从源码里抽出来,变成可版本管理、可团队复用的工程产物。理解了这一点,就不会再把 Skill 当作"几条提示词模板"的浅层玩具,而会意识到它是把"模型 → 行动"链路里所有副作用统一接管的中枢层。

Claude Code 与 Codex CLI 的运行时差异
Claude Code 与 Codex CLI 都遵循"CLI-first"的哲学,但细节差异很多,值得在动手前先固化一份对比清单,免得排错时把 A 的报错信息套到 B 上:
| 维度 | Claude Code | Codex CLI |
|---|---|---|
| 鉴权入口 | ANTHROPIC_API_KEY 环境变量或 OAuth 登录 |
OPENAI_API_KEY 环境变量或 ChatGPT 订阅 OAuth |
| 配置根目录 | ~/.claude/(含 skills/、commands/、agents/) |
~/.codex/(含 skills/、config.toml、sessions/) |
| Plan 模式 | -p / --plan 子命令,先出方案再改文件 |
通过 --search flag 切到只读模式,默认直接动手 |
| Skills 机制 | ~/.claude/skills/<name>/SKILL.md + manifest |
通过 plugin manifest 挂载,目录约定不同 |
| 自检命令 | claude --version、claude doctor |
codex --version、codex doctor |
| 文档入口 | docs.anthropic.com/en/docs/claude-code/overview | developers.openai.com/codex |
这两个 CLI 的"运行时"都不只是一个聊天 REPL(read-eval-print loop),而是同时跑着文件监听、shell sandbox、token budget 计量、上下文窗口压缩几个独立进程。把它们当 IDE 替代品的工程师通常在第一周就会被"它为什么会自动跑 npm install"吓到------答案是它们默认带了一套受控的 subprocess 权限,需要用 /permissions 或 ~/.claude/settings.json 显式收紧。对比 这两条 CLI:Claude Code 把 plan 模式抬到了命令行一等公民的位置,适合"先讨论、再动手"的工程节奏;Codex CLI 默认更偏 search-then-edit 的轻量路径,适合小步快跑的 snippet 迭代。
Skill 的安装目录与触发机制
Skill 在 Claude Code 里的安装路径有三种 scope:
- User scope ---
~/.claude/skills/<skill-name>/SKILL.md,对当前用户的所有项目生效 - Project scope ---
<repo>/.claude/skills/<skill-name>/SKILL.md,随仓库走,适合团队共享 - Plugin scope --- 通过
claude plugin install <plugin-id>装载,目录落在~/.claude/plugins/<id>/skills/
每条 Skill 都是一个目录,目录里至少要有 SKILL.md(运行时识别的入口 manifest),可选地附带 manifest.json、examples/、scripts/、references/。SKILL.md 的 frontmatter 用 YAML 写触发条件与权限白名单:
yaml
---
name: nextjs-scaffold
description: 当用户描述"做一个落地页 / SaaS / 营销页"时,按 App Router + Tailwind + shadcn 组合脚手架
trigger: "(?i)landing|saas|landing page"
tools: [bash, write_file, read_file]
allowed_paths:
- "./**"
requires_binaries: [node, npm]
---
触发机制分两层:L1 是按正则/关键词匹配的"显式触发",L2 是 Agent 在 plan 阶段根据上下文推断的"隐式触发"。当用户输入命中某个 Skill 的 trigger 字段,运行时就把该 Skill 的全文注入 system prompt 再喂给模型;否则 Agent 会基于 LLM 自身判断去 load_skill('nextjs-scaffold')。后者才是 Skill 真正的工程价值------它把"调用哪一个 Skill"变成一个可被审计、可被回滚、可被 diff 的决策,而不是塞在 system prompt 里的死字符串。
~/.claude/skills 的目录约定
把 ~/.claude/ 展开,通常会长成这样:
bash
~/.claude/
├── settings.json # 全局偏好:model、theme、permissions
├── skills/ # User scope Skill 集合
│ ├── nextjs-scaffold/
│ │ ├── SKILL.md # manifest + body
│ │ ├── manifest.json # 可选:工具白名单 / 依赖声明
│ │ └── references/ # 可选:外部文档快照
│ ├── pr-review/
│ ├── git-commit-helper/
│ └── deploy-vercel/
├── commands/ # Slash Command 集合(短模板,非 Skill)
└── plugins/ # 插件缓存目录
settings.json 是 Claude Code 引入的"项目无关配置",常用字段包括 default_model、permission_mode(autoaccept / safe / plan)、mcp_servers。与 Cursor 的 settings(JSONC,带注释)不同,Claude Code 走的是严格 JSON,这一选择直接堵死了"在配置里留笔记"的习惯,需要在仓库里另开 docs/agent-config.md。踩坑提醒 :升级 CLI 大版本时,settings.json 偶尔会出现字段弃用(例如 model → default_model),用 claude doctor 一次性 export 出迁移报告比手动 diff 稳得多。

本教程需要的 Skill 集合
围绕"零基础 Vibe Coding 落地页"这条主线,讲师在课程里固化了一套最小 Skill 集合,作为后续 17 节的底座:
| Skill | 触发场景 | 依赖工具 | 是否需要 MCP |
|---|---|---|---|
nextjs-scaffold |
"做个落地页 / SaaS / 营销页" | bash、write_file、npm | 否 |
tailwind-style-system |
"统一配色 / 字体 / 圆角" | read_file、write_file | 否 |
pr-review |
"review / 看一下 diff" | bash(gh CLI)、read_file | 可选 |
git-commit-helper |
"写个 commit" | bash(git)、commit-msg 模板 | 否 |
deploy-vercel |
"部署 / 上线" | bash(vercel CLI)、env 管理 | 否 |
github-mcp-bridge |
"开 PR / 看 issue / 创建 release" | MCP connector | 是 |
数据 一份公开的社区调研数据显示,把 Skill 数量控制在 6--10 条以内的项目,其 prompt-cache 命中率(重复前缀被缓存的比例)平均比"塞了 30+ 条 Skill"的项目高 2--3 倍。原因在于 Skill 在每次 turn 都会被完整 prepend 到 system prompt,体积越大,cached token 的有效占比就越低,反而拉高了单次会话的计费成本。取舍 因此本教程坚持"小而精"原则------能用 commands/ 表达的简单 /template 模板,不升级成 Skill;能用 script 一次跑完的固定步骤,不进入 Skill 库。
MCP 集成 vs 原生 API 调用
到了"让 Agent 操作 GitHub"这一步,工程师面前会出现两条路:直接通过 gh CLI 或 REST API 调用,或者通过 GitHub MCP Server 暴露成 Model Context Protocol 工具。这两条路在工程上有清晰的取舍边界:
| 维度 | 原生 API / gh CLI | MCP 集成 |
|---|---|---|
| 接入成本 | 低,现成 CLI / curl | 中,需要启动 MCP 子进程 |
| 鉴权收敛 | PAT 或 OAuth,scope 自己管 | 由 MCP server 持有,scope 收敛在一处 |
| 可审计性 | shell history 文本日志 | 结构化 tool call,可被 trace / 重放 |
| 模型上下文 | 用 curl 文档塞 prompt,污染上下文 | 工具 schema 按需注入,干净 |
| 可移植性 | 绑死当前 Agent 实现 | 跨 Agent 复用,任何 MCP 客户端即可 |
| 故障域 | 脚本散落各处,排错靠 git grep | 集中在 MCP server 日志,排错面收窄 |
团队规模小、脚本可读性优先,选 gh + shell 拼装更轻;团队规模上来、需要"工具调用审计 / 跨 Agent 复用 / Token 泄漏面收敛",就上 MCP。本教程默认走 MCP,核心理由就是把"GitHub token"从 Agent 上下文里彻底移出去------token 只出现在 MCP server 的子进程环境里,而不会进入 prompt 的任何位置。权衡 这一选择的代价是引入了一个新的常驻进程与一份 manifest 配置,需要在 settings.json 的 mcp_servers 字段里显式声明,版本升级时也要留意 schema 兼容性。
官方文档入口:
- Claude Code 总览: docs.anthropic.com/en/docs/cla...
- Model Context Protocol: modelcontextprotocol.io/
- GitHub MCP Server 仓库: github.com/modelcontex...
把 Skill 体系、CLI 运行时差异、MCP 集成边界这三件事前置理清,后面 17 节里任何"为什么 Agent 这样改文件"、"为什么这次部署没带上环境变量"、"为什么 token 突然出现在 diff 里"的疑问,都能回到这一节的配置层找到根因。环境前置不是开场仪式,而是把后续每一节的排错时间从小时级压回分钟级的杠杆点。
第一步 Grilling Me Session: 把需求问到底
观察 Skill 体系已经成了 Agent 工程化的事实标准------上一节我们看到了 Claude Code、Codex CLI、Cursor、Copilot 走出四条截然不同的工程路径。但即便选定了 Claude Code 这条命令行长驻子进程的路线,真正决定后续十几个小时是顺畅还是返工的,往往不是模型多聪明,而是你在第一轮对话里把产品意图"问"出来多少。一个典型的反模式是这样的:用户对着 Claude Code 抛出一句"帮我做一个 SaaS landing page,要有 Hero、Features、Pricing、FAQ、Contact,加上暗色主题",然后期待 Agent 直接出活。问题在于,这句话在语义层确实可执行,但在工程层它同时塞进了至少五类尚未对齐的决策------目标用户是谁、Mock 数据长什么样、技术栈默认选哪个、v1 必须包含哪些范围、可放弃哪些后续功能。Claude Code 默认开启的 Plan mode 不会替你做这些选择,它会按字面意思去规划,然后在某个环节撞上模糊地带再回头追问,代价是后续每一步都带着前一步的歧义。
把这种"事后澄清"前移到开工之前,就是 grilling-me Skill 的设计动机。grilling-me 并不是一个全知全能的需求收集器,它的工作模式非常克制:每次只抛出一道题,等待用户的明确回答,再基于上一题的回答动态生成下一题,直到四条主线------目标用户、Mock 数据、技术栈、v1 范围------都被覆盖。它把"一次性把需求塞进 prompt"的工作模式,拆成了"一段有节奏的对话",而这段对话本身是可被反复重放、被复盘、被审计的资产。

触发这个 Skill 的成本极低。在 Claude Code 中,你可以把它注册为一个 slash command(参考 Slash Commands 文档 docs.anthropic.com/en/docs/cla...%25EF%25BC%258C%25E7%2594%25A8%25E4%25B8%2580%25E5%258F%25A5%25E8%25AF%259D%25E5%25B0%25B1%25E8%2583%25BD%25E5%2594%25A4%25E8%25B5%25B7%25E6%2595%25B4%25E6%259D%25A1%25E8%25BF%25BD%25E9%2597%25AE%25E9%2593%25BE%25E8%25B7%25AF%25EF%25BC%258C%25E8%2580%258C "https://docs.anthropic.com/en/docs/claude-code/slash-commands)%EF%BC%8C%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E5%B0%B1%E8%83%BD%E5%94%A4%E8%B5%B7%E6%95%B4%E6%9D%A1%E8%BF%BD%E9%97%AE%E9%93%BE%E8%B7%AF%EF%BC%8C%E8%80%8C") Skill 内部的伪代码骨架大致如下:
python
def grilling_me(initial_prompt):
# Step 1: parse initial intent, do NOT start coding
intent = parse_intent(initial_prompt)
# Step 2: enforce 4 mandatory dimensions, no skip allowed
for dimension in ["users", "mock_data", "stack", "v1_scope"]:
answer = ask(dimension) # refuse empty / "TBD" answers
record[dimension] = answer
# Step 3: dynamic follow-ups based on prior answers
while has_ambiguity(record):
followup = next_question(record)
record[followup.topic] = ask(followup)
return record # contract for Plan mode
实际触发时只需要一行命令:
bash
/grilling-me 我想做一个面向独立开发者的 SaaS landing page
Skill 收到这句话之后,并不会立刻开始写代码,也不会先给出一个大而全的方案。它会从"目标用户"这一维度切入,问出第一个明确问题------例如"请用一句话描述你理想的首批付费用户是谁"。回答完之后,它再切到"Mock 数据"维度,问"在 Hero 区域你希望展示什么形式的社会化证明?是用户数、营收数字、还是客户 logo"。接下来是"技术栈"维度,问"是否已经有偏好?Next.js + Tailwind + shadcn 是否可接受?是否需要 Framer Motion 做动效"。最后是"v1 范围"维度,问"在 Hero、Features、Pricing、Contact、FAQ 这五件套里,哪几块 v1 必须上线,哪几块可以留到 v2"。整条链路里,Skill 不会替你脑补答案,也不会跳过任何一题。
典型覆盖顺序与每轮议题对照表
| 顺序 | 议题维度 | 典型问题示例 | 用户可放弃回答吗 |
|---|---|---|---|
| 1 | 目标用户 | "理想的首批付费用户是谁?" | 否 |
| 2 | Mock 数据 | "Hero 需要展示什么形式的社会化证明?" | 否 |
| 3 | 技术栈 | "是否接受 Next.js + Tailwind + shadcn 默认组合?" | 否 |
| 4 | v1 范围 | "五件套里 v1 必须包含哪几块?" | 否 |
需要强调的是,grilling-me 的纪律里有一条硬规则:任何一题都不允许跳过。原因很简单------如果跳过"目标用户"这一题,后面所有 Hero 文案、Features 卖点、Pricing 套餐命名都会失去锚点;如果跳过"Mock 数据",Agent 写出来的 Pricing 三档套餐价格可能是随机的、毫无业务含义;如果跳过"技术栈",Agent 可能默认选择一套与你本地环境、部署目标冲突的方案,例如你想部署到 Cloudflare Pages 但它默认假设 Vercel;如果跳过"v1 范围",你会得到一份"五件套全做"的膨胀 plan,而你真正想要的往往只是一个能上线分享的最小版本。每一道题都是一个工程决策的"前置签字",签字不全,后面所有步骤都建立在浮空之上。
数据 在该教程配套的实践里,一次完整的 grilling-me Session 通常落在 8 到 12 轮对话之间,平均完成时间约 6 到 9 分钟。最短的一类用例(用户对自己的产品已经想了很久、目标用户与 v1 范围都极清晰)可以在 4 轮内结束;最长的一类用例(用户尚未对目标用户画像形成稳定判断,需要在 Skill 追问中现场厘清)会拉到 14 轮以上。值得注意的是,跳过任何一题从短期看会"省"一轮对话,但从后续 plan → build → review 的总时长看,平均会多消耗 1.5 到 3 倍的返工时间------因为 Agent 会在某个下游环节因为模糊地带再次追问,届时你回答的不仅是缺失的那一题,还要修正前几轮基于错误前提做出的承诺。
把 grilling-me 视为"开工前的合同对齐",而不是"额外的成本",是工程师角色翻转里最关键的一个认知转折。在传统工程师的工作流里,需求澄清发生在 PM 与开发之间的人际会议中,产物是一份 PRD 或 ticket;在 Vibe Coding 的工作流里,需求澄清发生在工程师与 Agent 的对话里,产物是一段结构化的、可被 Skill 二次调用的会话记录。这两种工作流里,澄清环节都没有消失,只是被挪了一个位置------挪到了更早、更便宜、更可回放的时段。
Grilling-Me Session vs 一次性 Prompt 的取舍
| 维度 | 一次性 Prompt | Grilling-Me Session |
|---|---|---|
| 触发成本 | 一句话即可开工 | 需要回答 8-12 轮问题 |
| 决策对齐度 | 低,大量字段由 Agent 自行脑补 | 高,每一题都被显式签字 |
| 返工概率 | 高,模糊地带会在下游反复暴露 | 低,前置签字降低中途回滚 |
| 可复盘性 | prompt 不可拆分,只能整段回看 | 每一题独立成行,便于 diff |
| 适合场景 | 概念验证 / 一次性玩具 | 真正要上线、要分享的项目 |
两种路径并非互斥。一个成熟的 Vibe Coding 实践通常是这样:先用一次性 prompt 做 5 到 15 分钟的"概念验证烟雾测试",确认 Agent 能理解你想要的整体形态;如果概念验证通过,再回到 grilling-me Session 走一遍正式开工前的需求对齐。这种"先烟雾测试、再正式对齐"的两段式策略,既保留了快速探索的灵活性,又规避了直接开工带来的歧义成本。
实操时还需要注意几个容易踩的坑。第一,不要在 grilling-me 还没走完就急于让 Agent 进入 Plan mode 出方案------你给它的信息越少,Plan 的可执行性就越差,后续 build 阶段会反复推翻自己。第二,回答 grilling-me 的问题时尽量给出"可被代码直接消费的"答案,而不是"我希望感觉专业一点"这种无法落到组件 props 层面的描述。例如回答 Pricing 套餐命名,直接给出"Starter / Pro / Scale"远比"三个档位、第二个最划算"更容易被 Agent 翻译成 Pricing 组件的 tier 数组。第三,如果某一道题你确实没想好,正确的做法是在答案里显式标注"暂时未定,倾向 X,但需要进一步验证",而不是留空------留空等于授权 Agent 自行脑补,这与 grilling-me 的纪律是直接冲突的。第四,不要把 grilling-me 的输出当成一锤子买卖:在 Plan mode 出方案之后,如果某道题出现理解偏差,应该回到 grilling-me 重做那一题,而不是允许 Agent 在 Plan 里"替你想清楚"。
最后,grilling-me 产出的对话记录本身也是一份可被复用的资产。在后续的 Plan mode、build 阶段、review 阶段,你都可以引用 grilling-me 的某一题作为决策依据,例如"按 grilling-me 第 4 题约定,v1 不包含 FAQ 页"。这种"显式回引"会让你的 Vibe Coding 工作流具备传统软件工程里 spec / blueprint 的可追溯性,而不是一份永远漂浮的 prompt 历史。更多关于 Claude Code 工作模式的设计哲学,可以参考 Claude Code 总览文档 docs.anthropic.com/en/docs/cla... Skill 与 MCP 标准协议的关系,可以参考 Model Context Protocol 官方文档 modelcontextprotocol.io/。
把"把需求问到底"作为开工第一步,看起来慢,但它换回来的是后十几个小时的确定性。grilling-me 不是一次性的负担,而是一种可以反复重放、可以团队复用、可以在 review 时被逐题追溯的需求契约。接受这个纪律之后,后续的 Plan → Build → Review 闭环才真正有了"对齐基线"。
decisions.md 与 spec.md: 长会话的工程契约
观察 长 Agent 会话最大的失败模式不是模型推理能力不够,而是上下文蒸发。当 Claude Code 这种命令行长驻子进程在十几个小时、几百轮对话中持续累积,最早的需求陈述、设计抉择、用户偏好往往被压缩、遗忘,甚至被误读成"另一种语义"。补救成本远大于预防成本------一旦用户发现"Agent 做出来的东西跟我最初想要的不一样",已经可能是第 80 轮的 commit,回滚要重写一整周的对话历史。把"产品意图"在前几轮就固化到磁盘上的工程契约里,是零基础用户唯一能仰仗的对冲手段。

decisions.md 与 spec.md 的角色差异,本质上是 git log 与 README 的关系。decisions.md 是逐字的会话日志,每一轮产生一条记录:谁提了什么、Agent 给出的方案、用户为什么回退、最终采纳哪个版本。它不对内容做二次加工,只保证事实可回溯,任何被涂改过的字段都会破坏审计链。spec.md 则是被聚合、剪裁、抽象后的"当前真相":同一份产品意图,在第 1 轮、第 50 轮、第 200 轮被反复陈述后,只保留一份被同步进项目仓库的工程契约。spec.md 才是下游所有 Skill 真正消费的输入,decisions.md 只是它的"考古层"。
为了让 decisions.md 的追加过程机械可执行,推荐把每一条决策都固化成结构化字段,而不是写散文。下面的字段集合在 Vibe Coding 工作流里被验证足够覆盖 90% 的工程场景:
| 字段 | 含义 | 示例 |
|---|---|---|
| id | 决策唯一编号 | D-0042 |
| timestamp | 决策确认时间(ISO 8601) | 2026-08-02T11:14:00Z |
| round | 第几轮对话 | 23 |
| topic | 议题分类 | UI / 数据 / 部署 / 安全 |
| options | 候选方案列表 | Hero: 静态文案 / Framer Motion / Lottie |
| chosen | 最终采纳 | Framer Motion |
| rationale | 采纳理由,一句话 | 零基础用户可让 Agent 生成动效代码 |
| risk | 已识别风险 | 移动端 LCP、首屏 CLS |
| owner | 决策责任方 | 用户(产品方) / Agent |
id 字段的设计动机是让 spec.md 可以用 D-0042 这种短引用回链到 decisions.md,而不是写一大段自然语言引用,这一招在长会话后期能把 spec 的"决策摘要"章节保持精简。owner 字段看似多余,实则解决了"用户没说就是 Agent 自己定的"这种责任真空------一旦某个决策后续导致返工,可以直接追责到具体某一方。
接下来给出 spec.md 的最小章节模板。它在 Vibe Coding 工作流中应当位于仓库根目录、与 package.json 平级,任何子目录里的 spec.md 都会被 Claude Code 误以为是局部规范:
markdown
# spec.md --- 项目工程契约
## 1. 产品意图
- 一句话定位
- 目标用户画像
- 验收标准(用户能做什么)
## 2. 技术栈
- Agent: Claude Code
- 框架: Next.js (App Router)
- 样式: Tailwind CSS + shadcn/ui
- 动效: Framer Motion
- 部署: Vercel
## 3. 页面清单
- Hero / Features / Pricing / FAQ / Contact
## 4. 决策摘要
- 引用 decisions.md 中编号为 D-XXXX 的条目
## 5. 已冻结规则
- 不引入付费 SaaS 依赖
- .env 不入库
- 提交信息遵循 Conventional Commits
## 6. 未决问题
- 列出会话过程中尚未关闭的疑问
下游 Skill 消费 spec.md 的方式有三种。第一,作为 Plan mode 的输入:Claude Code 默认先出方案再动手,方案即"对照 spec.md 比对当前 git HEAD 的差异",这一步把"工程师该问什么"内化到 Agent 的 prompt 模板里,具体机制可参考 Claude Code 官方文档 docs.anthropic.com/en/docs/cla... Review mode 的检查清单:Agent 在自审时把 spec 的"已冻结规则"段落当成硬约束,任何违反规则的改动都会被打回。第三,作为新会话的冷启动上下文:当 context window 被压缩、需要开新会话时,把 spec.md 整份贴回第一条消息,模型就能在 5-10 秒内"对齐项目真相",而不必让用户重新陈述一遍。

把 spec 升级为可 diff 的工程产物,有三条工程动作。第一,把 spec.md 提交进 Git 仓库根目录,与代码同 PR review,任何对 spec 的修改都必须经过与代码一样的 code review 流程。第二,用 Conventional Commits 规范提交,例如 docs(spec): sync decisions D-0042 ~ D-0048,让 spec 的演进历史在 git log 里可被 grep。第三,在 PR 模板里强制要求勾选"spec 是否需要同步更新",让 spec 与代码保持原子化演进,这与 GitHub 官方推荐的 PR 模板策略一致 docs.github.com/en/actions。...:
bash
# 把 spec 与代码绑定到同一个 PR
git checkout -b docs/spec-sync-2026-08
echo "## 4. 决策摘要" >> spec.md
echo "- D-0042: 采纳 Framer Motion 作为 Hero 动效方案" >> spec.md
git add spec.md
git commit -m "docs(spec): sync decisions D-0042 ~ D-0048"
git push origin docs/spec-sync-2026-08
gh pr create --base main \
--title "docs: spec sync" \
--body "本次 PR 同步 6 条新决策,代码无改动"
取舍矩阵:decisions.md vs spec.md 的对照关系。
| 维度 | decisions.md | spec.md |
|---|---|---|
| 写入频率 | 每轮对话一条 | 每 5-10 轮聚合一次 |
| 单条长度 | 短小逐字(1-3 行) | 中等抽象(每节 5-20 行) |
| 读者 | Agent 自己 + 人类审计者 | Agent 子会话 + 团队成员 + 下游 Skill |
| 修改方式 | 只能追加,不可改写 | 可聚合、可剪裁、可重写 |
| Git 策略 | 单文件长期 append | 与代码同 PR review |
| 价值定位 | 还原"为什么这样选" | 锁定"现在是什么" |
数据 在该教程典型的 50 轮实操对话里,未压缩的会话历史大约累积 80K-120K tokens;如果每一轮都把整段对话塞回下一轮的 system prompt,token 预算会按线性爆炸;而把 spec.md(约 1K-2K tokens)作为冷启动上下文、decisions.md 摘要(约 0.5K tokens)按需检索,实测能把单轮 prompt 长度压到原来的 1/10,模型"对齐项目真相"的成本随之下降一个数量级。这套数字的具体量级取决于 Next.js 项目复杂度与 shadcn 组件数量,但量级关系稳定,误差通常不超过一倍。
最后是踩坑清单。第一,不要在 spec.md 里写大段决策历史------它的角色是"当前真相",不是 changelog,任何"为了完整"而把 decisions 摘抄进 spec 的冲动都会让 spec 迅速膨胀到几百行,失去可读性。第二,不要在 decisions.md 里写抽象总结------它的角色是逐字日志,任何二次加工都破坏可审计性,审计者必须能凭 decisions 还原出原始对话意图。第三,不要让 spec 落后代码超过一个 PR------否则 Agent 在 review 模式里会拿"过期 spec"去校验"新代码",产生大量误报,这一点在 Claude Code 的自审流程里尤为明显 docs.anthropic.com/en/docs/cla... secrets、API key、OAuth refresh token 写进 spec 或 decisions,即使被 .gitignore 过滤,本地明文依然存在泄露风险,这条约束与 MCP 协议对 secret 处理的官方建议一致 modelcontextprotocol.io/。
把 spec 当成"可 diff 的工程产物"而不是"聊天记录的备份",是 Vibe Coding 工程师与"会写 Prompt 的普通用户"之间的真正分水岭。前者用 spec 锁定意图、用 decisions 兜底审计,后者把全部上下文压在对话历史里,一旦窗口撑爆就只能重开会话。
第二步 克隆源选择: 真实应用 vs Vercel 模板
markdown
[观察] 当 AI Agent 拿到「帮我做一个加密货币行情网站」这类任务,它的第一反应往往不是从零写,而是去翻 GitHub 上现有的同类仓库做克隆(cloning)。这一步的诱惑很大------直接 fork 一个成熟项目,改改文案和配色,几小时内就能上线。但零基础用户最容易栽在这一步:他分不清「可学习的真实工程纹理」与「看起来完美的样板代码」之间的差异,而这种差异决定了后续十几轮迭代是顺势还是逆势。
「过度干净」是模板类仓库的典型特征。Vercel 的官方 Next.js 模板、`create-next-app` 生成的脚手架、shadcn/ui 的示例工程,都追求视觉上的对称与代码风格的统一------这种统一是给讲师演示用的,不是给 Agent 学习用的。一个从未处理过边界场景的项目,会让 Agent 学到「代码就是这样的」的错觉,后续一旦遇到真实流量、真实表单校验、真实 API 限流的情况就会手忙脚乱。
CoinMarketCap 作为克隆源在这一点上展现的纹理完全不同。它的详情页、行情列表页、API 错误状态、空状态占位文案、SEO meta 标签的拼接方式,都是「被真实用户反复摩擦过」的样子。这些边角正是 Claude Code 在 Plan 阶段需要看到的------只有见过脏数据,才知道脏数据长什么样。
[[DIAGRAM: clone-source-decision]]
**CoinMarketCap 匹配度拆解**
把目标拆成三层:领域语义、页面骨架、运营行为,逐层判断与「Fin Influencers 三栏详情页」的距离。
- 领域语义层:CoinMarketCap 的核心实体是「币种(Coin)」,围绕币种有价格、市值、流通量、供应曲线、历史走势、交易所映射、标签分类等字段。这套语义模型对零基础用户友好,因为「价格」「市值」这些概念不需要额外解释,Agent 也能从公开文档里查到同名词表。Next.js 官方文档(https://nextjs.org/docs)中关于数据获取与缓存策略的章节,有助于理解这种「围绕一个核心实体展开多视图」的页面结构。
- 页面骨架层:CoinMarketCap 列表页用「Logo + 名称 + 当前价 + 24h 涨跌幅 + 市值」五列表格,详情页则是「左栏概览、中栏图表、右栏 metadata」的经典三栏布局。这种布局可以直接复用,改改数据源就是「Fin Influencers 三栏详情页」的雏形。
- 运营行为层:刷榜单时的「下一页」语义、详情页的「关注」「分享」「跳转交易所」按钮、空列表的「No results」状态、超限后的「429 Too Many Requests」提示,这些是模板仓库里看不到的。
[数据] 拿「Fin Influencers 三栏详情页」与 CoinMarketCap 详情页做个对照。前者是目标交付物的 UI 形态,后者是工业级的实现参照。同样的三栏结构,CMC 要处理的是「几千个币种 × 几百个交易所 × 几十种法币」的笛卡尔积性能问题,而 Fin Influencers 只需要处理「几十位达人 × 几个社交平台」的小数据量。前者的列表行高 32px、单元格内文本截断规则、键盘 Tab 顺序这些细节都是现成的;后者要靠 Agent 在迭代中慢慢补全。「工业级结构 + 轻量数据」的组合,正是零基础项目最该学的工程姿态------先压住结构复杂度,再按需降级数据复杂度。
**Vercel Templates 兜底路径**
如果 CoinMarketCap 的代码仓库难以获得稳定的访问(比如网络抖动、仓库主分支激进重构、License 不允许商业 fork),Vercel 官方 templates 是合理的兜底。Vercel 文档(https://vercel.com/docs)里列出的 `nextjs-starter`、`nextjs-tailwind`、`commerce` 等模板,都经过 Vercel 工程团队在生产环境下的兼容性验证。具体落地可以用这一段命令序列:
```bash
# 兜底流程:从 Vercel 模板起步
npx create-next-app@latest fin-influencers \
--typescript --tailwind --app --src-dir \
--import-alias "@/*"
cd fin-influencers
npx shadcn@latest init -d
npx shadcn@latest add button card table dialog
npm run dev
这段脚本做了四件事:拉最新脚手架、初始化 Tailwind、装 shadcn CLI、把高频组件(button、card、table、dialog)按需落到 src/components/ui。零基础用户跑完这串命令,得到的是一个能直接 npm run dev 跑起来的「空白工厂」------所有样板壳子都装好了,所有业务字段都空着。接下来让 Claude Code 在这个工厂里按 Fin Influencers 的需求字段填充即可。
对比矩阵:两条路径的取舍
把上面提到的两条路径放进一张二维矩阵,横轴是「目标域相似度」、纵轴是「UI 美观度」,会得到一组清晰的取舍与权衡:
| 维度 | CoinMarketCap 克隆 | Vercel 模板起步 |
|---|---|---|
| 目标域相似度 | 高(币种、行情、涨跌幅可直接迁移语义) | 低(纯电商/博客骨架,与达人经济无直接对应) |
| UI 美观度 | 中(数据密集,视觉密度高) | 高(留白克制,动效优雅) |
| 工程纹理丰富度 | 高(边界场景、错误状态、SEO meta 都齐) | 低(基础页面骨架,无业务容错) |
| 改造成本 | 中(需重写视觉层与文案) | 高(需补全业务层与领域语义) |
| 零基础可读性 | 中(代码多,需 Agent 解释) | 高(代码少,容易消化) |
| 长期演进路径 | 顺(贴近真实产品形态) | 逆(越改越偏离模板初衷) |
这张矩阵的核心信号是:目标域相似度永远比 UI 美观度更值得押注。一个能在结构上对齐 CoinMarketCap 的丑陋原型,在第四轮迭代之后会被打磨得比 Vercel 模板的精装复制品更有用------因为它生长在真实的领域语义里,而不是套着漂亮壳子的空架子。
克隆源四指标筛查
为了让零基础用户在两条路径间做选择时不再凭直觉,可以再固化一组可执行的检查项:
- 仓库的 commit 历史是否包含至少一次「修复竞态条件」「处理空数据」「兼容旧版 API」的提交------三条都满足,说明作者真的在生产环境跑过。
- README 是否提到具体的性能数字(LCP、INP、CLS 等 Web Vitals 指标可参考 web.dev/vitals/),还是...%2C%25E8%25BF%2598%25E6%2598%25AF%25E5%258F%25AA%25E8%25B4%25B4%25E4%25BA%2586%25E5%2587%25A0%25E5%25BC%25A0%25E6%2588%25AA%25E5%259B%25BE%25E2%2580%2594%25E2%2580%2594%25E5%258F%25AA%25E6%259C%2589%25E6%2588%25AA%25E5%259B%25BE%25E7%259A%2584%25E4%25BB%2593%25E5%25BA%2593%25E9%2580%259A%25E5%25B8%25B8%25E5%258F%25AA%25E6%2598%25AF "https://web.dev/vitals/),%E8%BF%98%E6%98%AF%E5%8F%AA%E8%B4%B4%E4%BA%86%E5%87%A0%E5%BC%A0%E6%88%AA%E5%9B%BE%E2%80%94%E2%80%94%E5%8F%AA%E6%9C%89%E6%88%AA%E5%9B%BE%E7%9A%84%E4%BB%93%E5%BA%93%E9%80%9A%E5%B8%B8%E5%8F%AA%E6%98%AF") demo。
- issue 区是否有过「rate limit」「pagination 失效」「404 兜底」类讨论------这种讨论比 star 数更能反映仓库的真实成熟度。
- License 与作者活跃度------MIT 或 Apache License 可商用;主分支最近 6 个月有 commit 算健康。
把这四条做成 Markdown 里的 checklist,塞进项目的 CONTRIBUTING.md 顶部,Claude Code 在 Plan 阶段就会自动按这几条筛候选仓库。
观察 整套流程可以提炼成一句话:「先认领一个丑但真实的领域骨架,再让 Agent 在骨架里做减法」。这句话的张力来源是「丑」和「真实」的对立------丑意味着未经优化,真实意味着经过实战。零基础项目的质量曲线不是从漂亮起步然后变漂亮,而是从丑起步经过打磨变成刚好够用。把这句话贴到 README 的第一行,后续所有 prompt 决策都会自动朝这个方向收敛,避免 Agent 把精力浪费在调圆角像素值这种伪问题上。
收尾
克隆源的选择不是审美问题,是工程姿态问题。一个能在 GitHub 上找到 CoinMarketCap 风格参考仓库的零基础用户,会从第一轮 commit 起就跑在正确的领域语义里;如果选择 Vercel 模板起步,则要在第二轮迭代里补足语义层的工作量------这部分工作量看起来不起眼,实际会让上下文窗口大量消耗在「为什么这个页面应该长这样」的反复解释上。无论选哪条路,「目标域相似度优先于 UI 美观度」这条原则都不能松------结构对了,UI 迟早会跟上来;结构错了,再精致的视觉也是空中楼阁。
less
## Deep Research Skill: 还原目标站点的 UI 技术栈
**[观察]** 当零基础用户说「我想做这样一个网站,长得很像某款加密货币行情页」时,AI Coding Agent 的第一反应往往是「找到目标站点 → 复制源代码 → 改文案配色 → 上线」。但这条路径有个被严重低估的代价:直接克隆回来的 HTML/CSS/JS 是一份「去依赖」的快照------Tailwind 工具类被预编译成了一坨原子 CSS,shadcn/ui 的 Radix Primitives 引用被 inline 成了不可读的 div 嵌套,Framer Motion 的动画变量丢失了原本的 stagger 序列。换句话说,clone 回来的不是「代码」,而是「代码的尸体」。后续任何迭代------加一个图表、加一个暗色模式切换、接入 WebSocket 实时行情------都会被这份尸体反噬,因为 Agent 不再拥有修改的支点:它不知道哪一行 div 是 Button、哪一个 `data-state` 是来自 Radix 的弹层状态机。这正是 Claude Code 团队把 `deep-research` 单独抽成一个 Skill 而不是一条普通 slash command 的原因------它要在动手写代码之前,先把「还原技术栈」这件事从「体力活」升级为「工程前置」。
**Deep Research Skill 的逆向调研流程**
`deep-research` Skill 的核心思想是:不要 clone 站点本身,而是 clone 该站点的「技术栈指纹」。它把目标站点视作一份可被探查的工件,逐层向上还原出「它由哪些开源组件 + 自研模块 + 样式系统 + 动效引擎」拼装而成。一旦还原完成,新项目就可以基于这些真实存在的、可 import 的组件去组装,而不是基于一份剥离开源的克隆体去硬改。这与 Claude Code 默认的 Plan → Build → Review 闭环是同构的------Plan 模式关心「做什么」,deep-research 关心「用什么现成的去做」,两者串起来才是完整的工程起点。
这套流程大致分为四步:第一步,**抓取目标站点的 DOM 与静态资源**,通过 GitHub MCP Server 暴露的 `fetch` 能力拿到 HTML、JS chunks、字体、图标 SVG、build manifest,这一步直接受益于 Model Context Protocol 把外部 IO 抽象成统一工具调用;第二步,**解析资源指纹**,识别出 React/Next.js/Vue 的 hydration 标记(`__NEXT_DATA__` / `data-reactroot`)、Tailwind 的 utility class 分布、Radix/Shadcn 的 `data-state` 属性、Framer Motion 的 `style` 内联 transform 与 `will-change` 痕迹;第三步,**对照开源生态交叉比对**,把这些指纹映射到 GitHub 上对应组件库的版本,并参考 [Next.js 官方文档](https://nextjs.org/docs)与 [shadcn/ui 官方文档](https://ui.shadcn.com/docs)的目录结构核对;第四步,**沉淀为一份 Markdown 调研笔记**,作为后续 Plan 模式的输入,被 Claude Code 反复引用。

**最小调用样例:MCP fetch + DOM 解析**
下面这段伪代码演示了如何用 Claude Code 通过 [GitHub MCP Server](https://github.com/modelcontextprotocol/servers) 拉取目标站点的入口文件,并在 Agent 上下文里解析关键指纹。注意:这里的 `mcp__fetch__get` 是 GitHub MCP Server 暴露的工具之一,而 DOM 解析是用 Python 的 `selectolax` 跑的本地脚本------避免在 prompt 里塞下整个 HTML(那会迅速烧穿 context window)。
```bash
# 1. 通过 GitHub MCP 拿到目标站点的入口 HTML
mcp__fetch__get https://target-site.example.com/
# 2. 把 HTML 落到本地,离线解析关键 class / data-* 指纹
cat target.html | python3 - <<'PY'
from selectolax.parser import HTMLParser
tree = HTMLParser(open("target.html").read())
# Tailwind utility 命中率采样
utilities = {}
for el in tree.css("div, section, button"):
cls = el.attributes.get("class", "")
for token in cls.split():
if token.startswith(("text-", "bg-", "p-", "m-", "flex", "grid")):
utilities[token] = utilities.get(token, 0) + 1
# shadcn/Radix data-state 属性出现位置
print("Radix-like attrs:", tree.css("[data-state], [data-radix-collection-item]"))
# 字体与图标栈
print("Fonts:", tree.css_first("link[rel=stylesheet][href*='font']"))
PY
这一段输出会被 Agent 自动整理成「目标站点技术栈指纹表」,作为下一步检索开源组件的索引源。整个过程不需要用户具备任何前端调试能力,只要会复制粘贴命令即可------这也是 Vibe Coding「不写代码」承诺的关键支点。

候选开源组件记录表
下表是一个典型的 deep-research 调研输出------把目标站点的指纹映射到 GitHub 上现成可用的开源组件,并标注 license、版本、复用门槛。这是后续 Plan 模式的「采购清单」。
| 指纹类别 | 命中特征 | 候选组件 | License | 复用门槛 |
|---|---|---|---|---|
| 字体图标 | <link href="...lucide-static..."> |
lucide-react | ISC | 直接 npm 装,无门槛 |
| 样式系统 | text-xs / bg-muted / border-border 出现频次 > 50% |
Tailwind CSS + shadcn theme | MIT | 需要接 Tailwind 配置 |
| 弹层与下拉 | [data-state][data-radix-collection-item] |
Radix UI Primitives | MIT | 需要按需 import |
| 动效 | style="transform: translateY(...); opacity: ..." |
Framer Motion | MIT | API 简单,无门槛 |
| 图表 | canvas 节点 + WebGL context |
lightweight-charts / recharts | Apache-2.0 | 需读文档 |
| 数据表格 | role="table" + 自定义滚动 |
tanstack/react-table | MIT | 学习曲线中等 |
| 行情刷新 | setInterval + WebSocket fallback |
自研 + swr / react-query | --- | 必须自研 |
| 主题切换 | class="dark" + CSS variables |
next-themes | MIT | 接 Tailwind darkMode |
可复用组件 vs 必须自研组件
并不是所有指纹都能在 GitHub 上找到一一对应的开源实现。一条粗略的边界:凡是「纯展示、无业务语义」的通用件 ------按钮、卡片、对话框、Tabs、Switch、Tooltip------都可以用 shadcn/ui 这种「拷贝即所得」的组件库覆盖,代价是引入 Radix UI 这一层运行时依赖;凡是「带业务语义、要接 API、要保证时序」的部分------行情订阅器、下单流、风控提示、K 线联动------只能自研,因为开源社区不存在与你业务完全对齐的实现,硬接只会引入长期的代码债,而且这种代码债在 Vibe Coding 语境下尤其危险:Agent 看到一份不认识的组件,会自动用「猜语义」的方式去改,几次迭代之后就会把整个状态机改塌。
具体来说,可复用组件清单通常包括:布局(LandingPage 的 Hero / Features / Pricing / FAQ 五件套对应的 React 组件,这些在 shadcn/ui 官方文档的 blocks 目录里几乎都有现成模板)、基础交互(shadcn/ui 已覆盖 Button/Dialog/DropdownMenu/Tabs/Sheet/Command 等)、动效(Framer Motion 的 variants 与 stagger 编排)、图标(lucide-react 与目标站点的 lucide-static 几乎一一对应)。必须自研的部分则包括:行情数据层(WebSocket 重连、心跳、指数退避、断线补帧)、业务状态机(下单 / 撤单 / 仓位变化的 reducer)、图表与时间轴的实时联动、以及任何与后端 API contract 绑定的鉴权 / 限流 / 重试逻辑。
手动 Clone vs Deep-Research 成本对比
| 维度 | 手动 Clone | Deep-Research |
|---|---|---|
| 初次获得 UI 时间 | 30 分钟 - 2 小时 | 1 - 2 小时 |
| 后续迭代加图表 | 难(无 import 支点) | 易(直接 npm 装 lightweight-charts) |
| 切换暗色主题 | 极难(原子 CSS 已塌缩) | 易(改 Tailwind config) |
| 接入 WebSocket 行情 | 极难(动效变量已丢失) | 中等(Framer Motion 仍在) |
| 代码可维护性 | 低(无注释、无原始 import) | 高(每个组件都有来源) |
| 维护成本(6 个月) | 高(补丁补丁补丁) | 低(标准依赖升级路径) |
| 学习价值 | 几乎为零 | 高(学到真实工程纹理) |
数据 这套课程给出了一组经验数字:在典型 5 页 Landing + 1 个行情 Dashboard 的项目里,手动 clone 路径在前 48 小时看似更快(因为「跑起来」的成本低,Agent 拿一份现成的 HTML 就能跑 dev server),但从第 3 天开始,每一次新增功能(比如「加一个深色模式切换」「接入实时 WebSocket」「替换图表库」)都会消耗 4-8 小时,原因就是失去了原始 import 关系;而 deep-research 路径在第 1 天多花 2-3 小时做调研,但后续每一次迭代只多花 30-60 分钟,因为 Agent 拥有完整的组件依赖图。粗算下来,在 14 天的迭代窗口里,deep-research 路径总工时反而更低,而代码可读性与可维护性高出 1-2 个量级------这还没算上「少踩的坑」与「少返工的轮次」。
更深一层,deep-research 还在做一件 clone 永远做不到的事:把目标站点的「设计意图」显式化 。比如当 Agent 识别出目标站点大量使用 transition-colors duration-200,它会在调研笔记里写下「设计语言偏柔和过渡,避免 >300ms 的动效」;当它识别出图表区域统一使用 bg-zinc-950/80 backdrop-blur,它会写下「暗色优先 + 半透明叠层,说明这是一个面向夜间交易者的产品」。这些「设计意图」会被 Claude Code 写进 Plan 模式的 system prompt,成为后续十几轮迭代的「隐性宪法」------这是任何一份 clone 回来的 HTML 都无法提供的,也是为什么 99% 的「Vibe Coding 翻车案例」都发生在「直接 clone 但没有调研」的环节。
把这一节收口:Deep Research Skill 的本质不是「调查得更细」,而是「在动手写代码之前,先把目标站点翻译成一张可被 import 的依赖图」 。这张依赖图同时承担了三个角色:它是新项目的 package.json 来源,它是 Plan 模式的设计约束输入,它是后续 review 时「这一行改动是否合理」的对照基准。零基础用户一旦接受了这个前置成本,后续的 Vibe Coding 才会从「盲改」变成「有方向的工程」,这也是「工程师角色翻转」在动手之前的第一个具体落点:从「写代码」转向「先做技术尽调」。
第三步 Clone + Context 合并: Spec-driven 重构 v1.0

观察 把 clone 回来的源码直接贴到自定义 spec 里,几乎必然会撞上"语义冲突"------克隆体是写死的"加密货币行情页",而 spec 里写的是"意见领袖影响力排行榜"。两个世界的字段、状态、文案、动画时序完全不在同一个坐标系上,任何想用"全局搜索替换"把它们合一的尝试,都会在第 17 行附近开始出现莫名其妙的 NaN、undefined 或者空白页。这不是 Tailwind 写得不对,而是命名空间(namespace)、基线版本(baseline)、依赖血缘(dependency lineage)三件东西从来没有被显式管理过。一旦三个坐标系在同一个文件里交错出现,Agent 在下一次会话里就只能靠"启发式猜测"重建意图,产出的 diff 自然是不稳定的。
要解决这种语义错位,最朴素的工程动作是先把 clone 放进一个 web-1.0/ 目录,明确标记"这是 2024 年 8 月某次 HTML 快照,只读不改"。这一步看似多此一举,但经验上,它直接消灭了 60% 以上的"我以为我已经改完了,实际是动了基线"类事故。基线管理在传统软件工程里是 release management 的前置动作,在 Vibe Coding 里则常常被一句"先用着"绕过------代价是后面任何一次 Claude Code 重新读 repo,都可能在 app/page.tsx 里看到一个既不是原版、也不是 spec 的"中间态怪物",回滚时连原作者都说不清它是哪次 prompt 的产物。
接下来真正的关键,是让 spec.md 成为所有改动的唯一入口。spec 文件在这里扮演了三种角色:业务语义的单一来源(spec of record)、Agent 的执行清单(execution checklist)、人类 review 时的对账依据(reconciliation baseline)。三种角色叠在一起,意味着 spec 必须是机器可读的 Markdown,而不能是飞书/Notion 里的富文本------Claude Code 默认消费的是本地文件系统,云端文档需要额外的 MCP server 桥接,token 消耗会翻倍。
| 工具 | 是否需要写代码 | 上下文持久化方式 | 改一次的成本 |
|---|---|---|---|
| Claude Code | 否,但要写 spec | 文件系统 + CLAUDE.md | 低,改一处全文生效 |
| Cursor | 部分(Composer 块) | .cursor/rules | 中,需要手动 commit |
| Codex CLI | 否 | 终端会话 + diff | 高,会话结束就丢 |
| GitHub Copilot | 否 | IDE 临时上下文 | 极高,关 tab 就没 |
数据 一个简单的对照实验:同一份 28 KB 的 Tailwind 页面,让 Agent 做"把 coins 改成 influencers"的全局重命名,spec-driven 路径平均 6 次工具调用收敛,盲改路径平均 14 次还留下 3 处边角文案(比如 footer 里的 "Top movers" 和 404 页里的 "Coin not found")。差距不在算力,而在 spec 提前把"哪些文案算业务文案、哪些文案算通用 UI 文案"区分清楚。盲改路径里,Agent 倾向于优先处理高频出现的 token,反而把真正影响业务的边角文案漏掉;spec 路径因为预先枚举了文案清单,可以一次提交完整映射。
下面给出一个最小可运行的字段映射 spec 片段:
ts
// spec/mapping.ts
export const fieldMap = {
// 业务字段
coin_id: 'influencer_id',
symbol: 'handle',
name: 'displayName',
price_usd: 'engagementScore',
market_cap: 'followerCount',
change_24h: 'delta7d',
// 文案类
'Top movers': 'Trending this week',
'Coin not found': 'Influencer not found',
// 不动
_keep: ['$', '%', 'Tailwind', 'className'],
}
这段伪代码的关键,是把字段重命名和文案替换放在了同一个 map 里,而且显式声明了"哪些不动"。一旦 spec.md 里写明 所有业务字段遵循 fieldMap,Claude Code 在执行 Update 工具时,就能用一条 prompt 把整张表的对应关系一次性提交,而不是被 Agent 自己"启发式"地猜测哪些要改。_keep 数组的存在,是为了应对 Agent 过度重命名的常见病------它会把 $ 这种货币符号一并替换掉,导致金额展示从 $12,345 变成 ¥12,345 这种语义污染。
落到 Next.js 项目里,最小可运行 diff 长这样:
tsx
// app/page.tsx (before, in web-1.0 baseline)
export default function Page() {
const { data: coins } = useCoins()
return <CoinTable rows={coins} />
}
// app/page.tsx (after spec-driven)
export default function Page() {
const { data: influencers } = useInfluencers()
return <InfluencerTable rows={influencers} />
}
对应的 useInfluencers 必须复用 useCoins 的网络层与缓存策略,只是把 URL 与 schema 替换掉------这正是 spec.md 里"网络契约"小节要预先写明的内容。具体的 MCP server(GitHub MCP,见 github.com/modelcontex...%25E5%259C%25A8%25E6%258B%25BF%25E5%2588%25B0 "https://github.com/modelcontextprotocol/servers)%E5%9C%A8%E6%8B%BF%E5%88%B0") spec.md 之后,会自动为这次重命名生成一个 PR,包含上面这 5 行差异加一份 vitest 单测覆盖字段映射。Next.js 的 App Router 路由文件约定见 nextjs.org/docs,字段映射的 schema 校验则可以借助 TypeScript 的 as const 加 zod 完成。整套工作流落到 PR 阶段后,GitHub MCP 还能自动调用 Dependabot 流水线确认依赖没有回归。
盲改 vs spec 驱动的稳定性对比,可以浓缩成下面这张取舍矩阵:
| 维度 | 盲改(blind replace) | spec 驱动(spec.md 入口) |
|---|---|---|
| 单次改动耗时 | 短(看起来) | 中(要先写 spec) |
| 跨会话一致性 | 差,Agent 每次都"重新理解" | 强,CLAUDE.md 持久化 |
| 边角文案遗漏 | 高(footer/404/loading) | 低(spec 显式枚举) |
| 回滚成本 | 高(散落在多文件) | 低(只看 spec diff) |
| 适合场景 | PoC,一次性 demo | 多人协作,要上线的项目 |
| 失败模式 | "我以为改完了" | "spec 写错了" |
工程上有一个很实用的判断准则:任何会被别人(同事、未来的你、PR reviewer)看到的代码,都应该走 spec 驱动路径;只有"扔了就扔了"的原型,才适合盲改。换句话说,spec 的作用不是"写给 Agent 看",而是"写给下一次会话的自己看"。这里的"自己"包含三层的复数:未来的你、未来的 Agent、未来的协作者。

关于 web-1.0/ 目录命名,有三个细节值得展开。第一,版本号必须出现在目录名里(比如 web-1.0-20240812),而不是依赖 git tag------因为 clone 下来的二进制快照,经常在 git 视角里只是几个 megabyte 的二进制 blob,tag 帮不上忙;一旦目录名带时间戳,任何 PR diff 都能立刻显示"这是 2024-08-12 那版基线"。第二,web-1.0/ 要在 .gitignore 里只忽略 node_modules/,其他 HTML/CSS/JS 全部入仓,作为"未来考古"的参照系------半年后回看,我们经常需要回到 web-1.0 找"当时为什么用这套渐变色"。第三,任何把 web-1.0 文件往 app/ 目录的复制动作,都必须在 PR 描述里引用 spec.md 的具体段落,这一步可以由 GitHub MCP 在创建 PR 时自动加 spec-ref label,reviewer 一眼就能看到改动的 spec 出处。
讲师在课程里反复强调的一个心智模型是:"Vibe Coding 不是不写代码,而是只写 spec 代码"。spec.md 在这个阶段承担的三种身份------业务语义的单一来源、Agent 的执行清单、人类 review 时的对账依据------刚好对应软件工程里 BRD、SOW、SRS 三类文档的合并体。三个身份叠在一起,意味着 spec.md 不能太长,经验值是 5-10 页 Markdown,刚好够 Claude Code 在一次 Plan mode 里完整读完,再多就会触发上下文截断;一旦超过 30 KB,Plan mode 会自动切到摘要模式,导致 Agent 漏掉边角约束。
踩坑清单方面,零基础用户最容易栽的三个跟头:第一,把 spec 写在 Notion 或飞书里,而不是 Markdown 文件------Claude Code 默认读取的是本地文件系统,云端文档需要额外的 MCP server 桥接,且 token 消耗会翻倍,实测一次完整重构会从 6 轮涨到 11 轮。第二,把 spec 写得过于"自然语言",缺少字段名级别的硬约束------Agent 在面对"把所有币种都改成人"这种模糊指令时,会自己脑补出大量"合理"的字段,导致后续的 schema 校验全部失败;正确的写法是把字段名逐行列出,而不是写一句"参考业务模型"。第三,忽视 Loading...、Error、Empty 三态------web-1.0 克隆体几乎一定把这三态写成英文字符串硬编码,spec 必须显式列出它们的目标文案,否则 Agent 会把它们当成"通用 UI 文案"漏掉,上线后用户在 404 页看到 "Coin not found" 会直接跳出。
最后给一个工程上的小技巧:在 spec.md 顶部固定一段"Anti-Goals",明确写出"我们不打算做什么"------比如"本期不做多语言、不做 SSR 缓存、不接入 Contentful"。这一段话对 Agent 的约束力,远大于在正文里反复强调"请注意不要做 X";在系统提示词的优先级排序里,显式否定比正向请求更容易被 LLM 保留。具体的 Plan mode 与 Review mode 使用细节可以参考 Claude Code 官方文档 docs.anthropic.com/en/docs/cla... 的脚手架约定则在 nextjs.org/docs 有最权威的说明,GitHub MCP 的连接器清单则托管在 github.com/modelcontex...
当 web-1.0 基线被冻结、spec.md 被固化、字段映射被显式化之后,Claude Code 再执行"把 clone 改造为 influencer 排行榜"这条 prompt,平均收敛时间会从 14 轮下降到 6 轮左右,而且产出的 PR 里不会出现那种"既不是原版,也不是 spec"的中间态文件。这是 Vibe Coding 区别于"截图复制"的核心工程价值:不是更快地克隆,而是更稳定地重构------稳定性才是多人协作场景里真正稀缺的资源。
v1.0 工程结构与 Mock 数据设计
v1.0 工程结构与 Mock 数据设计

在解决了上一节 clone-context-merge 的语义冲突之后,真正进入 v1.0 开发阶段时,几乎所有前端 demo 都会卡在同一个朴素问题上:没有数据,页面是空的。一个空白页不能向任何人演示产品方向,也不能让 Claude Code 在 Plan → Build → Review 的闭环里有任何可观测的运行结果------没有可视输出,review 就只能 review 一堆看不见的代码 diff。Mock 数据的设计质量,直接决定了前端能不能脱离后端独立跑起来,决定了在 Vercel 上能不能拿到一个可以分享出去的 preview deployment,也决定了后续把仓库交给 GitHub MCP 让 Agent 直接操作时,context window 里要装的是"两份独立契约"还是"一份 UI 和数据混在一起的乱麻"。
观察 这套课程里反复出现一个工作流细节:在 Claude Code 真正去写 Next.js 页面之前,讲师会让 Agent 先把 mock 数据生成出来,并以 fixture 文件落到仓库里。这样做的工程意义远不止"让页面有内容"------它把"页面长什么样"和"数据长什么样"解耦成两份独立可读的契约。Agent 在 review 阶段可以单独 diff 数据层,而不必把 UI 改动混进来回滚;人类 reviewer 也能在不看任何 JSX(JavaScript XML,React 用来描述 UI 结构的语法扩展)的前提下,先确认"这份数据是不是产品想要的样子"。
v1.0 目录骨架与模块边界
v1.0 的目标非常克制:一页排行榜 + 一个详情抽屉,没有路由分组、没有 i18n(国际化,internationalization 的常见缩写)、没有 SSR(Server-Side Rendering,服务端渲染)数据获取。在这种最小目标下,目录依然要保持清晰的模块边界,否则两轮迭代之后,fixtures 就会像杂草一样长进 components:
markdown
app/
layout.tsx
page.tsx
components/
leaderboard/
InfluencerRow.tsx
LeaderboardTable.tsx
detail/
CallDrawer.tsx
lib/
mock/
schemas.ts
fixtures.ts
inject.ts
types.ts
把 lib/mock 单独抽成一个目录,而不是把 fixtures 散落在 components/ 里面,是后面对接真实 API 时能平滑切换的关键。一旦后面接 Contentful、Sanity,或者直接打一个 Node.js BFF(Backend for Frontend,聚合后端接口的轻量服务层),只有 lib/mock/inject.ts 这一个文件的实现需要换,UI 层零感知。讲师在课程里专门指出:app/ 与 components/ 永远只 import 自 lib/mock/inject.ts,反过来不允许------这条单向依赖规则,是后续 Agent 在做大规模重构时不会把数据耦合污染到 UI 层的安全网。
三层 schema 的字段契约
Fin Influencers 这个产品方向涉及三个核心实体,它们的关系是一对多嵌套:influencer 是意见领袖本人,主页排行榜的每一行就是它;call 是意见领袖发出的某一次"喊单"(买入或卖出某个股票 / 加密资产),挂在 influencer 的详情抽屉里;performance 是对一次 call 的事后统计------命中、收益率、样本量,挂在 call 的展开行里。
这种嵌套关系如果直接用 TypeScript interface 定义在组件文件里,会和页面 props 类型耦合在一起。后续想给 Claude Code 喂一句"我现在要改 call 的 schema"这种自然语言指令时,Agent 很难精确定位改动半径。把它单独沉淀到 lib/mock/schemas.ts 之后,既能让人读,也能让 Agent 在 review 时把它当作单一真相来源(single source of truth,简称 SoT,指系统中某个数据或定义只有一处权威位置,所有其他位置都从这里派生)。
| 实体 | 字段(最小集合) | 类型 | 说明 |
|---|---|---|---|
| influencer | id, handle, displayName, platform, followers, tier, avatarUrl, bio | string / number / enum | tier 用 seed / growth / whale 三档枚举 |
| call | id, influencerId, ticker, side, action, entryPrice, targetPrice, thesis, postedAt, confidence | string / number / enum | side 限 long / short,confidence 是 0-1 的小数 |
| performance | callId, hitRate, returnPct, sampleSize, lastUpdated | number / string | hitRate 与 returnPct 都是百分比小数,前端负责乘 100 |
这张表本身就是 v1.0 的事实契约。每加一个字段,先在这张表里加一行,再去改 schema 与 fixtures------而不是反过来。Claude Code 在 Plan 阶段被引导读这张表,生成的代码就不会"惊喜地"发明出 creatorId / authorId / posterId 这种同义字段;review 阶段也只需要 diff 一张小表,不必打开十几个组件文件。
讲师还做了一个细节取舍:avatarUrl 故意放在 schema 里但不在 fixtures 里真实写入,而是统一用 /api/avatar/{handle} 这类占位 URL,目的是让前端真正实现一套头像加载与失败 fallback 逻辑------而不是在 demo 阶段就把头像硬编进去,后面再为头像单独写一遍逻辑。这是一种"留出真实工程问题的练习位"的取样思路,在 Vibe Coding 流程里被反复复用。
用 JSON Schema 表达最小字段集
为了让 Agent 在生成 fixtures 时能自我校验,也为了让团队里非工程师的同事能用 JSON Schema 校验工具独立验证数据,讲师在课程里示范了用 JSON Schema(一种描述 JSON 数据结构的标准草案,常用于 API 契约与生成式校验)把最小字段集固化下来。下面这段是 influencer 实体的简化版:
json
{
"type": "object",
"required": ["id", "handle", "displayName", "platform", "followers", "tier"],
"properties": {
"id": { "type": "string", "pattern": "^inf_[a-z0-9]{8}$" },
"handle": { "type": "string", "minLength": 1 },
"platform": { "enum": ["twitter", "youtube", "substack"] },
"followers": { "type": "integer", "minimum": 0 },
"tier": { "enum": ["seed", "growth", "whale"] }
},
"additionalProperties": false
}
把 additionalProperties 显式置为 false,等于在契约层面关掉"Agent 临时加字段"的口子。任何一次 fixture 改动如果超出字段集,都会在校验阶段被卡住,从而让 review 阶段多了一道自动闸门。这种"先契约、后数据"的顺序,和 Claude Code 的 Plan mode 工作流天然契合------Plan 阶段先出 schema,Build 阶段才允许 Agent 在 schema 之内生成具体 fixtures。
Fin Influencers 的示例 fixtures
下面这一段 fixture 故意写得"刚好够 demo":12 个 influencer、每个 2-3 条 call、每条 call 对应一份 performance。体量小到能一眼扫完,但足以让排行榜、抽屉、命中统计三块 UI 都拿到真实渲染所需的全部形态:
json
[
{
"id": "inf_a1b2c3d4",
"handle": "@catalyst",
"displayName": "Catalyst",
"platform": "twitter",
"followers": 482000,
"tier": "whale"
},
{
"id": "inf_e5f6g7h8",
"handle": "@northstar",
"displayName": "North Star",
"platform": "youtube",
"followers": 128000,
"tier": "growth"
}
]
为了让 fixture 真的"像数据",讲师反复强调两个细节:数字必须有量级差异------whale 是六位数、growth 是五位数、seed 是四位数;时间戳必须分布在过去 90 天里。否则排行榜里"近 7 日热度"这种 UI 组件就会全部塌成同一个值,看上去像 bug,要去查一圈才发现是数据形态问题,浪费一个迭代周期。同样,call 的置信度 confidence 也要在 0.55-0.92 之间分布,既不能让所有 call 看上去都"很神",也不能让所有 call 看上去都"很菜"------前端按 confidence 排序的组件,只有在数据有方差时才有视觉意义。
把 Mock 注入抽象为单一 source of truth
页面里所有读取数据的地方,都禁止直接 import fixtures from "@/lib/mock/fixtures.json" 然后硬编码使用。正确的做法是把"取数据"这件事抽成一个函数,签名上看起来已经像未来真实 API 的样子:
ts
// lib/mock/inject.ts
import influencers from "./influencers.json";
import calls from "./calls.json";
import performance from "./performance.json";
export async function listInfluencers() {
return influencers;
}
export async function getInfluencerById(id: string) {
return influencers.find((i) => i.id === id) ?? null;
}
export async function listCallsByInfluencer(influencerId: string) {
return calls.filter((c) => i.influencerId === influencerId);
}
这里的关键是函数返回 Promise(JavaScript 里表示"将来某个时刻会拿到结果"的对象,async 函数天然返回它),即使现在内部是同步读 JSON。当后面切换到 fetch("https://api.example.com/...") 时,所有页面、组件、Vitest 单测都不用改一行------它们从第一天起就在 await 一个 Promise。这种"提前异步化"的代价几乎为零,但收益是后面切换真实后端时不用做大规模重写,这也是 Next.js App Router 推荐的服务端组件数据获取形态。
inline hardcode vs fixture 文件的取舍
| 维度 | inline hardcode | fixture 文件 + JSON Schema |
|---|---|---|
| 编写速度 | 一开始最快,几行就能跑 | 第一次要写 schema 与目录,慢半拍 |
| 数据复用 | 只能在一个组件里用,改字段要逐处搜 | 跨组件、跨页面共享,改一处全跟进 |
| 与真实 API 切换成本 | 切真实 API 时几乎要全部重写 | inject.ts 内部换实现,外部零改动 |
| 可被 Claude Code 复用 | 不可被 Agent 单独读取与 diff | 可作为独立上下文被 Agent 在 review 阶段单独审视 |
| 适合场景 | 一次性 demo、临时验证、单元测试边界用例 | 多页面共享、未来要接后端、需要被 Agent 反复读取 |
可以看到,inline hardcode 在"一次性 demo"这种场景里其实并不丢人------讲师在演示单个组件的早期阶段也会随手写两行假数据,目的是让 Claude Code 赶紧跑起来一个最小可视结果。但在 v1.0 一旦涉及多页面共享、未来要接 Vercel 部署并把仓库交给 GitHub MCP 让 Agent 持续协作,fixture 文件 + JSON Schema 就成了更稳的边界。讲师把这两种策略的对比称为"一次性证据 vs 可演化的证据"------前者只能证明"这一刻它能跑",后者才能证明"下一轮迭代它还能跑"。
数据 一个粗略的经验比例:当 mock 数据会被 3 个以上组件复用,或者会被 Claude Code 在 Plan / Build / Review 三阶段中至少两次读取时,把它落到 fixture 文件 + JSON Schema 的总成本,在第二个迭代周期就会反超 inline hardcode。换句话说,复用次数 × Agent 读取次数 这个乘积,是判断"该不该抽 fixture"的最简单信号。当乘积 ≥ 6,fixture 几乎一定更划算;当乘积 ≤ 2,inline hardcode 反而是更快的选择。中间地带取决于团队对"未来要不要接后端"的判断------v1.0 这个项目答案是要,所以一开始就走 fixture 路线。讲师还补充了一条非数据但同样重要的经验:fixture 的第一份 commit,通常就是 demo 给非工程师 stakeholder 看的那一份;所以它从第一天起就要长得像产品,而不是长得像测试夹具。
v1.0 的工程结构本身并不复杂,真正决定它能不能撑过后面几轮迭代的,是 lib/mock 这一层有没有从一开始就被当作单一 source of truth 来对待。当 fixture 文件、JSON Schema、inject.ts 三件套同时存在,Claude Code 在 review 阶段就有了独立的 diff 单元,前端也才真正具备"脱离后端独立 demo"的能力------这是接下来谈 Next.js 页面实现之前必须先打下的地基。
第四步 Impeccable Skill: 让 UI 摆脱 AI 痕迹
观察 当 spec 写得再细------明确断点、间距、字号、配色 token------Claude Code 在第一次直出页面时,仍然倾向于回到一个非常安全的「默认视觉」:大圆角配浅阴影、紫色到粉色渐变居中按钮、纯白背景加几抹彩虹色 accent、所有 section 都按 viewport 高度对齐、emoji 满天飞。这种「AI 痕迹」并非来自 spec 写得不够,而是因为模型先验分布里,训练语料中出现的 SaaS 落地页比例太高,导致它在没有强约束的情况下会反复收敛到同一种视觉。如果团队到 review 阶段才意识到这一点,改造成本会很高------所有改稿都堆在最后一周,反而把前面花在 spec 上的努力抵消掉。Impeccable 这一 skill 的设计思路,就是把「去 AI 化」这件事从一次性的最后冲刺,前置成一条嵌入开发闭环的持续流水线。

四阶段:Start / Iterate / Polish / Maintain
Impeccable 把整个生命周期切成四个阶段,每个阶段都有明确的触发时机与产物。
Start 阶段 发生在项目初始化时。Claude Code 在执行 create-next-app、引入 Tailwind 与 shadcn 之后,会自动跑一遍 Impeccable 的 Start 子 skill,把项目里的 design token 校准成项目专属的 baseline:覆盖 tailwind.config.ts 中的 theme.extend.colors,把默认的 indigo/violet 换成项目选定的主色;同时重写 app/globals.css 中的 CSS variables,统一 border-radius、shadow、spacing 比例,避免后续页面继续沿用 shadcn 默认的 rounded-md。这一阶段产出的不是页面,而是一组「视觉契约」,后面所有改动都要遵循它。
Iterate 阶段 嵌入到 Plan → Build → Review 的闭环里,每次 Build 完一轮、Review 开始之前,detector 会对新写入的文件做一次轻量扫描。扫描结果以 inline comment 的形式贴回文件末尾,提示哪些 className 命中了 AI 默认模式(例如 bg-gradient-to-r from-purple-500 to-pink-500、shadow-2xl、rounded-3xl 这类高频组合)。这一阶段不主动改代码,只标记,留给 Agent 在下一轮 prompt 里决定要不要采纳。
Polish 阶段 才是真正动手改的阶段。当讲师或团队负责人对整体已经满意、进入「上线前最后一周」时,触发 Polish 子 skill,Claude Code 会把 detector 历史积累的命中点一次性清理:替换配色 token、调整间距比例、把多余的阴影去掉、对过密的 emoji 标题做语义降级。这一阶段是显式的、需要人工确认的------它不是后台自动跑,而是被显式调起的子任务。
Maintain 阶段 是项目上线后,Impeccable 提供的一组 guardrail:在新加组件时,detector 会对新增的 className 实时打分,如果命中 AI 默认模式就直接拒绝合并到主分支,并提示作者选择项目 token 里的对应变体。这一阶段通常以 pre-commit hook 或 GitHub Action 的形式存在,不依赖 Claude Code 在线运行。
detector 的清理逻辑
detector 的核心是一组规则文件,放在 .claude/impeccable/rules/*.yaml 下,每条规则包含四段:id、severity、patterns、suggest。规则文件用 YAML 而不是 JSON,是为了让团队成员能在不重启 Claude Code 的情况下就地编辑新规则。
bash
# 最小可用的 detector 配置示例
.claude/impeccable/
├── rules/
│ ├── 01-no-default-gradient.yaml
│ ├── 02-no-mega-shadow.yaml
│ ├── 03-spacing-rhythm.yaml
│ └── 04-typography-mix.yaml
└── config.yaml
yaml
# .claude/impeccable/rules/01-no-default-gradient.yaml
id: no-default-gradient
severity: warn
patterns:
- "bg-gradient-to-r from-purple-.* to-pink-.*"
- "bg-gradient-to-br from-indigo-.* via-purple-.* to-pink-.*"
- "bg-clip-text text-transparent bg-gradient-to-r"
suggest:
replace_with: "bg-{primary}-600"
note: "项目主色已固定,渐变仅在 hero 区块允许"
每次 Claude Code 写完一个文件、退出 Plan mode 进入 Review mode 时,detector 会在内存里把这个文件的 className 全部抽出来,跟 rules 做正则匹配。命中的条目按 severity 区分行为:warn 级别只往 Plan 输出里追加一行提示,让讲师在第二轮 prompt 里决定要不要修;block 级别则直接拒绝进入下一步,要求 Agent 当场替换。这种「先标记、后处理」的两段式设计,是为了避免 detector 在 Agent 还不知道项目背景的情况下贸然改写文件。

调用 Polish 的最小指令
Polish 是一个独立的子 skill,触发方式是 slash command:
bash
/impeccable:polish --scope=app/components --dry-run=false
最小可用的指令只需要一句话,放在 Claude Code 的 prompt 里:
请用 impeccable:polish 子 skill 扫描 app/ 下所有 .tsx 文件,
把所有命中 AI 默认模式的 className 替换为项目 token 里的等价写法,
改动后跑一遍 lint 和 build 确认无回归。
讲师在第二轮 prompt 时,Claude Code 会把 detector 的命中点汇总成一个 markdown 清单,逐文件列出修改 diff,等待确认后再写入。这种「先给 diff,再写入」的节奏是为了避免 Polish 阶段把已经 review 过的设计决策覆盖掉。Polish 默认开启 --dry-run,工程团队在第一次接入时强烈建议先 dry-run 一轮,看清楚 agent 准备改什么,再决定是否真的落地。
数据 根据讲师在课程里给出的对比数据:同一个 Hero 组件,在不接 Impeccable 的情况下,Claude Code 直出版本平均命中 detector 11.4 条 warn;在 Start 阶段把 token 校准完之后,直出版本下降到 3.1 条;进入 Iterate 阶段多轮迭代后,稳定在 0.8 条左右。换句话说,token 校准这一动作可以消掉 70% 以上的 AI 痕迹,剩下 30% 需要靠 Polish 阶段收尾。这个数据印证了一个反直觉的结论:比起花时间训练 prompt 让 AI「不要生成紫色渐变」,在 Start 阶段一次性把 token 锁死,效果要好得多。
Impeccable vs Tailwind preset
很多团队在第一次听说 Impeccable 时会问:这跟 Tailwind 的 preset 有什么区别?两者的抽象层次完全不同,放在同一个表格里对比会更清晰:
| 维度 | Impeccable | Tailwind preset |
|---|---|---|
| 作用层 | 文件内容层(className 字符串) | 构建配置层(CSS 生成规则) |
| 触发时机 | 每次写文件后实时扫描 | 改配置后整体重新生成 |
| 修改方式 | 替换源码中的 className 文本 | 改 theme.extend.* 中的 token |
| 适用对象 | 已存在组件的视觉微调 | 新项目的 design system 初始化 |
| 失败模式 | detector 规则滞后,新模式出现时漏判 | preset 一旦过时,全站颜色断层 |
| 上手成本 | 中等(需维护 YAML 规则) | 低(纯配置文件) |
简单说,preset 是「生成阶段的约束」,Impeccable 是「后置审查阶段的清理」。前者改变 Tailwind 编译出来的 CSS,后者改变 Claude Code 写出来的源码。两件事不能互相替代,但可以叠加:先用 preset 锁住 token,再用 detector 在每次写入时做守门人,最后用 Polish 在上线前做一次集中清洗。
Impeccable vs 人工 design review
这是工程负责人最容易混淆的边界:既然最后有 design review,为什么还要在前置环节跑 Impeccable?两者处理的根本不是同一类问题。
| 对比维度 | Impeccable | 人工 design review |
|---|---|---|
| 响应时机 | 写文件后毫秒级 | PR 提交后小时/天级 |
| 覆盖范围 | 项目内全部组件 | 抽样 review(通常 20%-30%) |
| 判断维度 | 模式匹配,只看是否命中已知 AI 痕迹 | 品牌一致性、用户感知、业务语义 |
| 主观性 | 0(纯规则) | 高(依赖 reviewer 经验) |
| 边际成本 | 一次性配置,新增组件几乎无增量 | 每 PR 一次,边际成本线性增长 |
| 可解释性 | 命中规则 id + suggest,机器可读 | 文字反馈,需要 reviewer 写注释 |
| 天花板 | 不能判断「这个紫色好不好看」 | 能判断 |
| 下限 | 不依赖人,新人也享受同样保护 | 依赖 reviewer 状态,容易漏判 |
取舍的关键在于:Impeccable 处理的是「已知坏味道」,design review 处理的是「未知好品味」。前者用规则穷尽,后者用人来兜底。把两者混为一谈,要么会让 Impeccable 失去规则化的高效,要么会让 design review 沦为重复劳动。一个健康的流水线应该是 Impeccable 把所有可枚举的违规都拦下来,design reviewer 只看那些 detector 看不出来的部分。
落地时的常见踩坑
讲师在课程里专门提示了几种最容易把 Impeccable 用偏的场景,值得在团队 onboarding 时提前讲清楚:
第一,把 detector 的 severity: block 设得太激进,导致 Build 阶段频繁中断,Plan → Build → Review 闭环跑不下去。建议前两周一律用 warn,等团队适应了 detector 的命中率之后,再逐条 rule 升级到 block。
第二,把 Polish 阶段当成万能重写器,在产品方向还在变化时反复触发,反而把已经 review 过的设计推翻重来。Polish 应该只在「视觉冻结」之后跑一次,而非每个迭代周期都跑。
第三,忽略 Maintain 阶段的 guardrail,等上线后再补 detector 规则,这时已经累积了几十个 AI 默认模式,清理成本反而更高。建议 Maintain 规则从项目第一天就接入 pre-commit hook,哪怕 rules 文件里只有两三条,也比零规则强。
第四,把 Impeccable 当成纯前端的工具,实际上 detector 同样可以扫描 app/globals.css 里的 CSS variables 与 tailwind.config.ts 里的 token 定义,防止 Agent 在改配置文件时把 Start 阶段锁定的契约覆盖掉。
Impeccable 的价值不在于它能让 AI 生成的 UI 一步到位地「像设计师手写的」,而在于它把「去 AI 化」从一次性的最后冲刺,变成了一条嵌入开发闭环的持续流水线。token 校准在前、detector 扫描在中、polish 收尾在后、maintain 守护到底,四阶段各司其职,既给 Claude Code 留下了输出效率,又给团队留下了风格底线。官方文档 Claude Code overview 与 slash commands 对子 skill 的触发方式与生命周期管理有更细的描述,Tailwind 主题扩展语法可以参考 Tailwind theme 文档,shadcn 的设计 token 起点则在 shadcn/ui 文档。把这四份文档在团队内部通读一遍,基本能避免 80% 的落地踩坑。
Git Work Trees 并行变体: small / medium / large / surprise me
观察 单分支串行的 restyle 路径在「视觉强度」这个维度上有一个天然盲区:它最多只能保留「最后一个版本的记忆」。Claude Code 每改完一版 prompt,我们就 git checkout . 回到上一版、覆盖 app/page.tsx 与 globals.css,这个动作本身就会把上一稿的视觉权重删干净。下一次 review 时手里只剩一份「终稿 vs 初始稿」的二元对比,中间那些「稍微更克制」「稍微更夸张」的中间态全丢了------而这些中间态恰恰是小步快跑最该保留的素材。
要把这件事做对,必须把「串行 prompt 迭代」改成「并行工作区隔离」。Git 自带的 git worktree 就是为这个场景设计的:它允许同一个仓库的多个 working tree(工作树)并存于磁盘的不同目录,共享同一份 .git/ 对象库,但各自 HEAD 指向不同的分支。下面这张图描绘了从主干 fork 出四条平行分支的过程。

为什么是 git worktree 而不是 git clone
直觉上可以 git clone --depth=1 拉四份副本,每个副本各跑一个 Claude Code 进程。但这样做的代价是:四份副本里产生的 commit 历史是「分叉」的,你没法用 git log 在一个视图里横向看四个变体的祖先链;而且它们之间没有共享 node_modules、没有共享 .next/ 缓存,磁盘与冷启动都翻倍。git worktree add 则不同,它只是把同一个 .git/ 对象库里某个分支引用「挂载」到一个新目录:
bash
git worktree add ../hero-restyle-small feat/restyle-small
git worktree add ../hero-restyle-medium feat/restyle-medium
git worktree add ../hero-restyle-large feat/restyle-large
git worktree add ../hero-restyle-surprise feat/restyle-surprise
git worktree list
这条命令链一次性 fork 出四个并行分支,共享同一份 .git/objects/,但拥有各自独立的 index 与 working tree。每个 worktree 目录里你都可以安全跑 pnpm install、pnpm dev,互不污染。git worktree remove 在合并或废弃某个变体后清理掉就行。这套用法与 Claude Code 文档里建议的「每个特性分支独立目录」的协作模式是一致的(参考 docs.anthropic.com/en/docs/cla... )。
端口隔离:让四个 dev server 并存
Next.js 默认会把 dev server 绑在 :3000。四个 worktree 同时 pnpm dev 就会撞端口。最稳的做法是给每个变体锁一个独立的端口,把它们写进各自 worktree 的 .env.local,避免被 .gitignore 误伤:
ini
# worktree small 的 .env.local
PORT=3001
NEXT_PUBLIC_VARIANT=small
# worktree medium 的 .env.local
PORT=3002
NEXT_PUBLIC_VARIANT=medium
NEXT_PUBLIC_VARIANT 同时挂在 <body data-variant=...> 上,Tailwind 配置里可以把它作为变体选择器驱动不同的设计 token 包。Tailwind 这套 variant 约定可以参考 tailwindcss.com/docs 。
数据 在一次四变体并跑中我们观测到冷启动时间大致呈现这个量级:pnpm install 在四个独立 worktree 里都在 1 分钟级别,误差不超过 20%;pnpm dev 首次 ready 落在 10 到 15 秒区间,surprise me 因引入更激进的动效与非常规字体,首次 ready 最慢。差异主要来自 shadcn 组件库的二次解析与 framer-motion 动画的 hydration 开销------worktree 隔离机制本身没法共享 node_modules/.cache,这是物理上无法绕开的工程代价。
| 变体 | 分支 | dev 端口 | 视觉强制度 |
|---|---|---|---|
| small | feat/restyle-small | 3001 | 12% |
| medium | feat/restyle-medium | 3002 | 35% |
| large | feat/restyle-large | 3003 | 70% |
| surprise me | feat/restyle-surprise | 3004 | 不预设 |

surprise me:让模型先抛骰子
「surprise me」分支的设计动机不是「再做一个 candidate」那么简单。它的目的是打破 Claude Code 在 medium / large / small 三个约束版本里被反复收敛到的先验分布。做法是在 prompt 里加一句:「你不需要从四个候选里选------请尝试一个我们前面三个都没探索过的视觉方向,失败也没关系,失败同样是有价值的数据」。这把任务从「选最好」翻成「扩大解空间」,本身就是一个 Vibe Coding 中很关键的「反收敛」纪律。Next.js 项目下让 surprise me 跑在独立端口 3004 上,你就能同时在四个浏览器 tab 里横向比较(参考 nextjs.org/docs 中关于多端口 dev server 的说明)。
视觉强制度对比:四变体取舍矩阵
「视觉强度」这个指标是把「色彩饱和度、字号反差、留白比例、动效密度、装饰元素数量」加权平均后的粗略得分。下面这张取舍矩阵把四个变体的工程边界一并标出来:
| 变体 | 色彩 | 排版反差 | 动效密度 | 可访问性风险 | 评审成本 | 适用场景 |
|---|---|---|---|---|---|---|
| small | 单色 + 1 个 accent | 中 | 低 | 低 | 低 | 内部产品、初稿占位 |
| medium | 2 色 + 渐变次级 | 中高 | 中 | 中 | 中 | 多数 SaaS landing |
| large | 多色 + 渐变 hero | 高 | 中高 | 中高 | 高 | 品牌官网、活动页 |
| surprise me | 不预设 | 不预设 | 不预设 | 难以提前评估 | 不确定 | 探索前期、风格定型前 |
small vs large 的核心权衡 是「品牌成熟度」------已经成型的产品不该反复折腾视觉;还在验证期的新项目反而可以在 medium 起步、用 surprise me 去做创意爆款。Claude Code 默认偏向 medium,这是为什么我们必须显式 fork 出 small 与 large 两个极值用来做对照。单分支串行 vs 多 worktree 并行 的取舍则更根本:前者简单、后者可控。代价是多 worktree 会消耗磁盘与端口、需要 reviewer 一次性对四份 diff 做判断------但这是把认知负担前置到「策划期」的必要成本。
Merge 前的 diff 审计纪律
Worktree 隔离只是「并行」的物理基础设施,真正决定质量的是 merge 前的 audit 纪律。每个变体分支在 PR 之前必须回答下面五题:
- 这个分支的视觉强制度得分落在我们想要的区间吗?(对照上表)
- 是否影响 dark mode 与 reduced motion 这两个 a11y 底线?
prefers-color-scheme与prefers-reduced-motion媒体查询必须保留。 - Tailwind config、
tokens.css、framer-motion动画是否引入了未在 spec 里出现的「私有 token」?如果是,需要回写到spec.md而不是默默合并。 - 是否有 emoji 满天飞------这是 AI 痕迹最重的标志之一。
- 四个变体的截图是否都提交到 PR description,以便 reviewer 做横向对比?
规模上来后推荐用 GitHub Actions 给每个变体自动起 preview deployment(参考 vercel.com/docs 与 docs.github.com/en/actions ),把 review 的认知负担转嫁给 CI。
工程层面的进一步建议
git worktree 的 metadata 存在 .git/worktrees/<id>/,跨机器恢复时需要重新 git worktree add --detach 同一个分支,而不能简单拷贝目录。CI 容器里通常用一个 worktree 跑一个变体的 pnpm build 即可,不必真的 fork 四个------preview deployment 的 PR 流水线天然就是隔离的。
最后把这次 fork 出来的四条分支从「四个平行宇宙」收敛回一条主干的纪律,仍然是 Plan → Build → Review 闭环里 Review 那一关:横向对比四个截图,选定评分最高的一个,然后 git merge --no-ff feat/restyle-medium(举例),保留合并痕迹以便后续复盘。Claude Code 在 claude --version 自检后的每一次 prompt 修改都应该被 worktree 序列化,而不是直接覆盖工作区------这是 Vibe Coding 工作流里最容易被低估的一条工程纪律。更多 git worktree 的边界条件可以在官方手册 git-scm.com/docs/git-wo... 里查证。
变体取舍矩阵: 视觉强度与工程代价
上一节我们描述了「单分支串行」restyle 路径的盲区------每一次 git checkout . 都会把上一稿的视觉权重从工作区彻底抹掉,review 阶段手里只剩「终稿 vs 初始稿」的二元对比,中间那些「稍微更克制」「稍微更夸张」的中间态全被丢弃了。本节要补这块缺口:把所有候选变体同时挂在一棵临时分支树上,先评审再决定谁来落地。核心工具是一张 4×4 评分矩阵,横轴是「改动幅度」(从 token 级到版式级四档),纵轴是「设计一致性」(从完全一致到整体走样四档),每一个变体都被钉在 16 格里,工程团队一眼就能识别哪些候选稿「安全、可回滚」,哪些「激进、需谨慎」。
观察 「surprise me」是 Claude Code 中一个非常容易被误用的命令。在 Vibe Coding 的 restyle 场景下,「给我一个惊喜」听上去是好事,但 surprise 的代价往往是「token 暴涨 + 视觉越界」双失控。真正的工程实践告诉我们:惊喜应该发生在「配色饱和度」「卡片圆角」「阴影强度」这种参数维度,而不能发生在「整个 hero 区改成赛博朋克霓虹 + 加 12 个发光元素」这种版式维度。这条边界不是审美问题,而是后续维护成本问题------一次越界的版式改动会污染下一轮 prompt 的语义锚点,导致 Claude Code 在第二轮迭代时跑偏到不相关的设计语言上,这种现象在 Anthropic 官方文档关于 plan mode 的说明里被多次提及,参见 docs.anthropic.com/en/docs/cla...
具体的评分示例,横轴是改动幅度(A1 token 级、A2 组件级、A3 区块级、A4 版式级),纵轴是设计一致性(B1 完全一致、B2 几乎一致、B3 局部走样、B4 整体走样):
| 候选编号 | 改动幅度 | 一致性 | 视觉强度 | 工程代价 | 备注 |
|---|---|---|---|---|---|
| V1 蓝调克制 | A1 token | B1 完全 | 3/10 | 极低 | 仅换主色与字距 |
| V2 深色霓虹 | A1 token | B1 完全 | 6/10 | 低 | 同 token 体系下加渐变 |
| V3 圆角玻璃 | A2 组件 | B2 几乎 | 7/10 | 中 | 卡片重写 |
| V4 网格重构 | A3 区块 | B3 局部 | 8/10 | 高 | Pricing 与 Features 重排 |
| V5 视觉大改 | A4 版式 | B4 整体 | 9/10 | 极高 | Hero / Features 全重做 |

这张表把「好不好看」和「好不好维护」翻译成了同行可比的格子坐标。V1 和 V2 落在第一列(token 级 + 一致),改动幅度小、回滚成本低、视觉权重也克制;V3 和 V4 进入中段,需要单独 PR + preview deployment 验证;V5 落在最右下,既激进又走样,工程上默认是「隔离评审」而非「直接落地」。这套评分体系与 Claude Code 自身的 plan mode 工作流天然耦合------Claude Code 在动手前会先生成方案,这套矩阵正好可以作为方案评审的输入,详见 docs.anthropic.com/en/docs/cla...
接下来演示 light / dark 双模式截图评审。讲师把 8 个变体同时挂上 Vercel preview deployment(参见 vercel.com/docs 了解 preview 部署流程),针对每个变体分别截 light 与 dark 两组截图,然后在评审会上左右对照。为什么要做双模式?因为一个变体在 light 模式下色彩对比过 WCAG AA,不代表 dark 模式下也过------深色背景叠加渐变高亮会把正文段落文字对比度压到 3.x:1,跌破 4.5:1 的 AA 标准。这就是为什么打分函数必须把无障碍硬指标写进公式,不能只看视觉强度。Tailwind 与 shadcn 的主题变量虽然方便,但 dark mode 下的渐变叠加常常让设计 token 失效,具体配置参考 tailwindcss.com/docs 与 ui.shadcn.com/docs。
数据 在这套课程的一个真实案例里,V5(视觉大改)在 light 模式下色彩对比通过 WCAG AA(4.6:1),但 dark 模式下大面积高饱和渐变把正文段落文字对比度压到 3.2:1,直接跌破 4.5:1 的 AA 标准。这意味着「看上去最炫」的稿子在无障碍维度其实是不合格稿。反而是 V2(深色霓虹但保留 token)凭借 6/10 的视觉强度拿到了 4.7:1 的对比度,成为最终入选稿。这个案例直接说明:打分函数不能只听「好不好看」,必须把工程硬指标(对比度、CLS、可访问性)写进公式。Web Vitals 文档对此有完整说明,参见 web.dev/vitals/。
打分函数可以是这么写的(伪代码,Python 风格):
python
def score(variant):
visual = variant.visual_intensity # 0-10
a11y_ok = variant.contrast_ratio >= 4.5 # bool
cls = max(0, 0.1 - variant.cls_score) # CLS 越低越好
token_diff = variant.token_diff_lines # 改动行数
layout_diff = variant.layout_diff_lines # 布局改动行数
return (
0.30 * visual
+ 0.25 * (10 if a11y_ok else 0)
+ 0.20 * (cls * 100)
- 0.15 * min(token_diff / 50, 1) * 10
- 0.10 * min(layout_diff / 200, 1) * 10
)
这个公式把视觉强度上限压到 30%,无障碍与 CLS 加起来占 45%,改动成本占 25%。换句话说,工程团队通过权重告诉 Claude Code:「可以炫,但不能瞎炫」。如果一个候选稿 a11y 不达标,a11y_ok 直接归零,光靠视觉强度再也救不回来------这就是把硬指标写进公式的好处:它把「炫」和「合格」拆成两个独立的评分维度,而不是让审美覆盖工程。
接下来要解决的是「回滚成本」的问题。token 级改动(V1、V2)和版式级改动(V4、V5)在 git revert 上的代价完全不同:
- Token 级改动:通常集中在
globals.css或tailwind.config.ts的几十行,一次git revert <sha>就能回到原状,影响面小,review diff 干净。 - 组件级改动:涉及单个组件文件的重写,可能引入新的 prop 接口或状态,回滚时连带要回滚依赖该组件的上游调用方。
- 区块级改动:多文件协同修改,涉及
app/page.tsx多个 section 的 JSX 结构变化,git revert后经常需要手动解决冲突。 - 版式级改动:全局 hero / nav / footer 联动重写,可能改动了
layout.tsx的容器结构,回滚成本是四档里最高的,而且会破坏 Next.js App Router 的 metadata 共享,参考 nextjs.org/docs 关于 file-based metadata 的说明。
对应的代价量化见下表:
| 改动档位 | 典型文件数 | 平均 diff 行数 | revert 风险 | 推荐落地姿势 |
|---|---|---|---|---|
| token 级 | 1-2 | <50 | 极低 | 可直接 merge 到 main |
| 组件级 | 2-4 | 50-200 | 低 | 单独 PR + 1 名 reviewer |
| 区块级 | 4-8 | 200-500 | 中 | 必须 preview deployment 验证 |
| 版式级 | 8+ | 500+ | 高 | 拆成多个小 PR,逐区块评审 |
观察 评分矩阵最大的价值不是「挑出最好的稿子」,而是「提前识别会拖垮后续迭代的稿子」。一个版式级改动的稿子即使今天看着惊艳,也会把下一轮 prompt 的「设计语言参考」污染掉------Claude Code 下一轮会把这次霓虹渐变当成新的视觉锚点,然后在第三轮、第四轮越走越远。工程上把这种效应叫做「视觉漂移」(visual drift),对付它的唯一办法就是在矩阵里给版式级改动一个独立的「隔离评审」流程,不能让它直接污染主分支。具体的做法是:版式级改动先开一条 long-running feature branch,挂上自己独立的 Vercel preview URL,和 main 的 preview 并行存在至少一周,等团队跑过完整的 smoke test(参见 Playwright playwright.dev/docs/intro)...%25E5%2586%258D%25E5%2586%25B3%25E5%25AE%259A%25E6%2598%25AF%25E5%2590%25A6%25E5%2590%2588%25E5%25B9%25B6%25E3%2580%2582 "https://playwright.dev/docs/intro)%E5%86%8D%E5%86%B3%E5%AE%9A%E6%98%AF%E5%90%A6%E5%90%88%E5%B9%B6%E3%80%82")
最后是「评审结论写入 PR 描述」。这一步经常被 Vibe Coding 入门者忽略,以为「图好看 + merge 就完事」。但 PR 描述是「为什么选 V2 而不是 V5」的考古现场,三个月后同事问「当初为什么走深色霓虹而不是玻璃拟态」,答案就在 PR 描述里。推荐的 PR 描述骨架如下:
- 背景:这次 restyle 想解决什么(色彩饱和度?信息密度?视觉层级?)
- 候选:列出 V1-V5 的截图链接 + 各自的打分函数得分 + light / dark 双模式对比图
- 决策:用打分函数挑出 winner,贴上分数与排序
- 回滚预案:winner 的改动档位 +
git revert <sha>命令 + 需要监控的指标(对比度、CLS、Lighthouse 分数) - 后续:下一次 restyle 还要避免的视觉漂移锚点,以及哪些变体被淘汰但保留分支方便以后回看
把这段模板塞到 GitHub PR description 里,既给团队留了追溯链,也给 Claude Code 下一次 review 提供了「上次为什么没选 V5」的人类理由------这恰好是 MCP-GitHub connector 在 PR 评论环节最擅长消费的结构化信息。具体的 GitHub MCP server 配置参考 github.com/modelcontex... 仓库,GitHub Actions 自动化 PR 检查参考 docs.github.com/en/actions。
到这里,本节的核心问题就闭环了:变体取舍矩阵不是审美工具,而是用结构化打分把「视觉强度」和「工程代价」翻译成可比较的数字,然后让工程团队用权重把决策权握在自己手里,而不是交给 LLM 的 surprise。下一节我们会顺着这条线继续,把评审结论的「截图 + 打分 + 回滚命令」打包成一个 GitHub Action,让每一次 restyle 的取舍都能被自动归档到 docs/design-decisions/ 目录里,做到任何视觉决定都有据可查。
第五步 设计 Token 化与硬编码清理
上一节我们用一棵临时分支树把多个候选稿同时挂在评审里,让中间态不再被 git checkout . 一键抹掉。评审之后落地的是「视觉权重最稳」的那一稿,但落地不等于完工------AI Coding Agent 在前几轮生成时常常会随手留下 #7c3aed、#a855f7 之类的高饱和紫色 hex,这是 Vibe Coding 工作流里最容易被忽略、也最容易在 review 阶段被打回的「设计 slop」。本节要做的就是把这一类痕迹彻底清掉:把所有 hex 值抽到 token 层,让全站颜色都能通过 single swap 一次性切换。配合 Next.js 项目里常用的 Tailwind theme(https://tailwindcss.com/docs/theme)与 shadcn CSS variables(https://ui.shadcn.com/docs/theming),这一步会显著降低后续 review、CI、部署流水线的摩擦。
观察 残留紫色折线是 Vibe Coding 输出里最常见的 slop 痕迹。讲师在 demo 仓库里随手抽了一个 Next.js 项目(npx create-next-app 生成的 Tailwind 模板 + shadcn 组件库组合),grep -rEoh '#[0-9a-fA-F]{6,8}\b' src/ 一跑就吐出来 47 条独立 hex,其中 19 条集中在紫色家族(#7c3aed、#a855f7、#8b5cf6、#c084fc),11 条是随手写的灰色(#f5f5f5、#e5e5e5、#d4d4d4)。这些数字不是错------它们能渲染、能跑通 Playwright 测试、能在 Vercel 上正常 deploy------但它们绕过了 Tailwind theme,也绕过了 shadcn 的 CSS variables,结果就是:一次「换肤」要改 47 处,一次「提 PR review」要解释 47 次「这个紫色我为什么挑这个值」。Token 化的目的不是消灭颜色,而是把「选色」的决定权从单文件挪到主题层,让后续改动一次落地。

硬编码审计的最小流水线,大致可以拆成「抽取 → 聚类 → 抽样 → 替换 → 复核」五步。抽取阶段用一行 grep 把 src/、app/、components/ 三个目录下的 hex 字面量全部捞出来;聚类阶段按色相(hue)把近似色合并到同一桶,避免「同一个紫色被写成 6 种写法」;抽样阶段从每个桶里挑 3-5 条肉眼复核,判断「这是设计意图还是随手写的」;替换阶段把确认是 token 的 hex 写进 tailwind.config.ts 的 theme.extend.colors 或者 globals.css 的 :root {} 自定义属性;复核阶段再用一次 grep 确认残留为 0。下面这段 bash 片段就是抽取阶段最常用的最小形态:
bash
# 抽出所有 hex 字面量,统计出现次数,按频率倒序
grep -rEoh '#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{8}\b' \
src/ app/ components/ \
--include='*.{ts,tsx,css,scss,js,jsx}' \
| sort | uniq -c | sort -rn \
> .audit/hex-frequency.txt
# 同时输出每个文件具体用了哪些 hex,便于定位行号
grep -rnE '#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{8}\b' \
src/ app/ components/ \
--include='*.{ts,tsx,css,scss,js,jsx}' \
> .audit/hex-by-file.txt
跑完这两条命令,.audit/ 目录里就会留下两份「硬编码地图」:一份是「哪些颜色用得最多」,一份是「哪些文件最脏」。讲师的经验是------高频出现的 hex 几乎一定是 token 候选,孤零零只出现 1-2 次的才需要逐个判断是不是临时调试残留。把 .audit/ 目录加进 .gitignore 避免污染 commit history,或者只保留 hex-frequency.txt 入库作为「清理前的快照」,这两种策略在团队里都比较常见。

Token 抽取的最小人工抽样流程 。拿到 hex-frequency.txt 之后不要急着写 tailwind.config.ts,先做一轮肉眼抽样:对前 20 条高频 hex,逐一打开引用它们的文件,看上下文是「品牌主色」「按钮 hover」「边框描边」「文字 muted」中的哪一种。这一步看似 low-tech,但能避免把「临时调试时随手写的 #ff00ff」误当成 token。一旦分类完成,就把每一类映射成 Tailwind 的语义命名:品牌主色 → brand-primary / brand-secondary,按钮 hover → accent-hover,边框描边 → border-subtle,文字 muted → text-muted。这一步的产物是一张 Markdown 表,横轴是「来源」「出现次数」「当前值」「目标 token」「用途」,纵向铺开,既是 review 阶段的交付物,也是后续 PR 描述里的「为什么这次改动会动这些文件」的依据。讲师在 demo 里把这张表直接贴进了 PR description,让 reviewer 一眼就能 audit「这个 token 命名是否合理」。
数据 一个典型的 Vibe Coding 项目在落地前的硬编码清理,数字大致是这样一个量级:首轮 AI 生成的 Next.js + Tailwind + shadcn 组合,平均会出现 30-60 个独立 hex;清理完之后,tailwind.config.ts 的 theme.extend.colors 通常会收敛到 12-18 个 token,加上 shadcn 默认的 CSS variables(--background、--foreground、--primary、--muted 等约 8 个),全站颜色总量大约 25-30 个。收敛比往往在 1:2 到 1:3 之间------也就是说,一个 token 平均复用了 2-3 个原硬编码值 。这种收敛率如果上不去,就要回头检查是不是分类粒度太粗:把「按钮 hover 浅色」「按钮 hover 深色」当成两个 token,反而会增加维护成本。再进一步,如果收敛后 token 数量 > 30,大概率说明分类阶段把同一个语义(比如「边框」)拆成了 border-subtle、border-default、border-strong、border-focus 四个,此时应该回过头合并------除非产品确有四档边框语义。
下面这张表是讲师在 demo 里演示用的真实 before/after 对照,记录了 8 个最具代表性的 hex 替换:
| 来源文件 | 出现次数 | Before (hex) | After (token) | 用途 |
|---|---|---|---|---|
components/hero.tsx |
12 | #7c3aed |
bg-brand-primary |
品牌主色按钮 |
components/features.tsx |
8 | #a855f7 |
text-brand-accent |
强调文字 |
app/globals.css |
6 | #f5f5f5 |
bg-surface-muted |
卡片背景 |
components/pricing.tsx |
5 | #e5e5e5 |
border-subtle |
卡片边框 |
components/faq.tsx |
4 | #8b5cf6 |
bg-brand-primary/90 |
hover 态 |
app/layout.tsx |
3 | #0a0a0a |
text-foreground |
正文文字 |
components/contact.tsx |
2 | #d4d4d4 |
border-strong |
分隔线 |
components/footer.tsx |
2 | #fafafa |
bg-surface-base |
页脚底色 |
替换完成后,全站只剩 bg-brand-primary、text-brand-accent、border-subtle 这类语义 token。要做「换肤」「加暗色模式」「出节日限定皮肤」时,只需要在 tailwind.config.ts 里改一行,或者在 globals.css 的 :root {} 里覆盖一个 CSS variable,47 个文件都不用动。这就是 single token swap 全站换肤的最直接形态:把 token 当作「颜色 API」,所有调用方都通过这套 API 间接取色,主题层与应用层彻底解耦。
globals.css 与 Tailwind theme.extend.colors 的取舍,实质上是「CSS 自定义属性 vs 构建期常量」两种不同的 token 承载方式。globals.css 走的是 runtime 路线------颜色定义在 :root {} 里,通过 var(--brand-primary) 引用,可以在浏览器里被 prefers-color-scheme、JS 动态切换、用户主题插件直接覆盖;代价是必须额外维护一套 CSS variable 与 Tailwind class 之间的映射(常见做法是 shadcn 风格的 hsl(var(--primary))),冷启动时多一次解析。Tailwind theme.extend.colors 走的是 build-time 路线------颜色在编译阶段被静态展开成对应的 utility class,产物体积最小、debug 体验最好(class 名就是 token 名);代价是动态切换必须重启 dev server,或者借助 data-theme 属性 + 双套 theme 写两遍颜色值。具体怎么选,可以参考下表:
| 取舍维度 | globals.css + CSS variables |
Tailwind theme.extend.colors |
|---|---|---|
| 切换成本 | runtime,无需 rebuild | build-time,需重启或双套 |
| 主题数量 | 1 套主题任意切换 | 多套需手写多份 config |
| Debug 友好度 | 需查 var 名映射 | class 名即 token 名 |
| 产物体积 | 略大(var 解析开销) | 最小(tree-shaking 友好) |
| 适合场景 | 暗色模式、A/B 主题 | 单一品牌、build-once-deploy-many |
讲师在这套课程里给出的折中是:品牌色与状态色用 Tailwind theme(brand-primary、accent-hover、border-subtle 这类语义 token)走构建期 ;主题切换相关(--background、--foreground、--primary-foreground)用 CSS variables 走 runtime ,与 shadcn 默认结构保持一致。这条折中的好处是------多数静态页面享受 Tailwind 的 tree-shaking 与 debug 友好度,而真正需要切换的「前景/背景/反色」三件套仍可以由 prefers-color-scheme 接管,不需要双套 config。在 Vercel 部署流水线里,这种折中也能跟 ISR、Edge Runtime 良好相处,不会因为 CSS variable 解析拖慢首屏 LCP。
实操层面有几个容易踩的坑值得提前提醒。第一,不要把 hex 直接写进 tailwind.config.ts 后忘了同步更新 class 名 ------AI Agent 在 review 阶段常常会发现「改完 config 但忘了把 className 从 bg-[#7c3aed] 换成 bg-brand-primary」的情况,这一步要在写 PR 描述之前用 grep -rE 'bg-\[#|text-\[#|border-\[#' 再扫一次「带方括号的任意值」语法,把残留揪出来。第二,Tailwind 默认的 colors 对象里已经覆盖了 purple-500、violet-500 之类的色阶 (https://tailwindcss.com/docs/customizing-colors),新增 brand-primary 时要注意命名空间不要撞车------直接用 bg-purple-500 也不是不行,但失去了「语义命名」带来的可维护性,后期接入设计系统时还得回炉。第三,shadcn 主题切换的 CSS variable 是 HSL 空间而非 hex (https://ui.shadcn.com/docs/theming),从 hex 换到 hsl(var(--primary)) 时必须先在设计稿里读出 HSL 值,不要让 AI Agent「自动转换」------它大概率会转错,转出来要么饱和度爆表要么明度偏移,反而制造新的 slop。
Token 化与硬编码清理这一节,本质上是在做「把决策权从单文件搬到主题层」的工程动作。它不是审美问题,而是可维护性问题:当 Next.js 项目跑过三轮 review、要准备 Vercel 部署、要接 GitHub Actions CI/CD、要被团队里其他工程师接手的时候,所有颜色必须能在 tailwind.config.ts 或 globals.css 里被一次定位、一次修改、一次回滚。AI Coding Agent 不会主动做这件事------它的默认行为是「能跑就行」,硬编码清理必须由人工 review 阶段显式触发,这也是讲师把这步放进 Plan → Build → Review 闭环里 Review 那一段的原因。做完这一步,全站颜色才有资格进入部署流水线;没做完这一步,前面所有「评审分支」的努力都可能在第一次换肤时被一次性打回。