AI Slop 治理实战

0 阅读1小时+

在这里插入图片描述

在这里插入图片描述

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 越刺眼。

具体来说,缺失的上下文至少包含四层:

  1. 业务上下文:这个页面服务于什么用户、解决什么痛点、转化漏斗在哪一环、目标 CTA 是什么。
  2. 设计上下文:品牌色 token、字体家族、组件库、动效节奏、栅格系统、响应式断点。
  3. 工程上下文:Next.js App Router 还是 Pages Router、Tailwind 还是 CSS Modules、shadcn/ui 还是 Radix 裸用、TypeScript 严格度。
  4. 约束上下文:性能预算(LCP / INP / CLS 阈值,详见 web.dev/vitals/)、无障… 元信息、可访问性文案长度。

四层上下文缺任何两层,Agent 都会用它的"先验"来补,而先验恰恰就是 slop 的来源。先验越强、上下文越弱,产出的页面越像"标准答案"——而"标准答案"从来都不是产品,只是模板。

四、五步治理框架:全篇索引

基于上述分析,本指南后续章节会围绕一个五步治理框架展开,这是阅读全篇的索引:

  1. Spec 契约:把意图写成可被 Agent 解析的 blueprint,固化业务/设计/工程/约束四层上下文。
  2. Plan mode 锁定:在 Claude Code 进入动手阶段前,先冻结方案,避免 Agent 在中途漂移。
  3. Review mode 校验:用 diff + 测试(Vitest/Playwright)双轨验证 Agent 输出,详见 vitest.dev/guide/playwright.dev/docs/intro。
  4. Context hygiene:管理 .env、token、context window,避免上下文污染与 token 泄漏。
  5. 工程师角色翻转:从"写代码"转向"拆需求 + 编排工具",让 Agent 流水线真正跑起来。

五步之间不是串行流水线,而是双环:外环是 Spec → Plan → Build → Deploy,内环是 Review → Test → Context hygiene。后续章节会按这个双环展开,本文先建立宏观心智。Claude Code 官方文档(docs.anthropic.com/en/docs/cla… Plan mode 与 Review mode 也有原生支持,这是工具层与治理层的天然对齐。

五、一个最小可运行的 spec 示例

为了让"spec 契约"这个抽象概念落地,这里给出一段伪代码片段,展示工程师在动手前应该先交付的最小 spec:

# 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)也强调配置文… 与此精神完全一致。

六、中国大陆工程师为什么要建立 spec 契约习惯

[数据] 对于中国大陆的工程师团队而言,spec 契约习惯的紧迫性比海外团队更高。原因有三:

  1. 模型语境差异:Anthropic Claude、OpenAI GPT 系列在中文场景下的"先验审美"更倾向于国际化极简风,直接套用到中文产品上,容易出现"信息密度过低、转化文案过长、字号过小"的不匹配。一段 18 字符以内的英文 headline,翻译成中文往往要 24 字以上,如果 spec 里不显式声明 headline_max_chars,Agent 会按英文节奏裁剪,最终落到页面上就出现断行错乱。
  2. 合规与备案:境内上线产品需要 ICP 备案、内容审核、可识别的开发者信息、必要的实名跳转链接,这些约束必须在 spec 阶段就被显式写入,否则 Agent 生成的页面会在 review 阶段被整段打回,造成返工成本指数级放大。
  3. 团队协作粒度:国内多数团队仍以"前端 + 后端 + 产品"三段式分工为主,引入 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/)正在成为 Agent 与外部工具对接的事实标准。GitHub MCP Server 仓库(github.com/modelcontex… 流转、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… 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-drivencopy-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…

治理闭环:可复用模板

# 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-skill-architecture

Claude Code 与 Codex CLI 的运行时差异

Claude Code 与 Codex CLI 都遵循"CLI-first"的哲学,但细节差异很多,值得在动手前先固化一份对比清单,免得排错时把 A 的报错信息套到 B 上:

维度Claude CodeCodex CLI
鉴权入口ANTHROPIC_API_KEY 环境变量或 OAuth 登录OPENAI_API_KEY 环境变量或 ChatGPT 订阅 OAuth
配置根目录~/.claude/(含 skills/commands/agents/)~/.codex/(含 skills/config.tomlsessions/)
Plan 模式-p / --plan 子命令,先出方案再改文件通过 --search flag 切到只读模式,默认直接动手
Skills 机制~/.claude/skills/<name>/SKILL.md + manifest通过 plugin manifest 挂载,目录约定不同
自检命令claude --versionclaude doctorcodex --versioncodex doctor
文档入口docs.anthropic.com/en/docs/claude-code/overviewdevelopers.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:

  1. User scope~/.claude/skills/<skill-name>/SKILL.md,对当前用户的所有项目生效
  2. Project scope<repo>/.claude/skills/<skill-name>/SKILL.md,随仓库走,适合团队共享
  3. Plugin scope — 通过 claude plugin install <plugin-id> 装载,目录落在 ~/.claude/plugins/<id>/skills/

每条 Skill 都是一个目录,目录里至少要有 SKILL.md(运行时识别的入口 manifest),可选地附带 manifest.jsonexamples/scripts/references/SKILL.md 的 frontmatter 用 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/ 展开,通常会长成这样:

~/.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_modelpermission_mode(autoaccept / safe / plan)、mcp_servers。与 Cursor 的 settings(JSONC,带注释)不同,Claude Code 走的是严格 JSON,这一选择直接堵死了"在配置里留笔记"的习惯,需要在仓库里另开 docs/agent-config.md踩坑提醒:升级 CLI 大版本时,settings.json 偶尔会出现字段弃用(例如 modeldefault_model),用 claude doctor 一次性 export 出迁移报告比手动 diff 稳得多。

agent-runtime-comparison

本教程需要的 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 CLIMCP 集成
接入成本低,现成 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.jsonmcp_servers 字段里显式声明,版本升级时也要留意 schema 兼容性。

官方文档入口:

把 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"的工作模式,拆成了"一段有节奏的对话",而这段对话本身是可被反复重放、被复盘、被审计的资产。

grilling-me-flow

触发这个 Skill 的成本极低。在 Claude Code 中,你可以把它注册为一个 slash command(参考 Slash Commands 文档 docs.anthropic.com/en/docs/cla… Skill 内部的伪代码骨架大致如下:

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

实际触发时只需要一行命令:

/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目标用户"理想的首批付费用户是谁?"
2Mock 数据"Hero 需要展示什么形式的社会化证明?"
3技术栈"是否接受 Next.js + Tailwind + shadcn 默认组合?"
4v1 范围"五件套里 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 的取舍

维度一次性 PromptGrilling-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-vs-spec

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 误以为是局部规范:

# 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。…:

# 把 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.mdspec.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 模板

[观察] 当 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 模板的精装复制品更有用——因为它生长在真实的领域语义里,而不是套着漂亮壳子的空架子。

克隆源四指标筛查

为了让零基础用户在两条路径间做选择时不再凭直觉,可以再固化一组可执行的检查项:

  1. 仓库的 commit 历史是否包含至少一次「修复竞态条件」「处理空数据」「兼容旧版 API」的提交——三条都满足,说明作者真的在生产环境跑过。
  2. README 是否提到具体的性能数字(LCP、INP、CLS 等 Web Vitals 指标可参考 web.dev/vitals/),还是… demo。
  3. issue 区是否有过「rate limit」「pagination 失效」「404 兜底」类讨论——这种讨论比 star 数更能反映仓库的真实成熟度。
  4. License 与作者活跃度——MIT 或 Apache License 可商用;主分支最近 6 个月有 commit 算健康。

把这四条做成 Markdown 里的 checklist,塞进项目的 CONTRIBUTING.md 顶部,Claude Code 在 Plan 阶段就会自动按这几条筛候选仓库。

[观察] 整套流程可以提炼成一句话:「先认领一个丑但真实的领域骨架,再让 Agent 在骨架里做减法」。这句话的张力来源是「丑」和「真实」的对立——丑意味着未经优化,真实意味着经过实战。零基础项目的质量曲线不是从漂亮起步然后变漂亮,而是从丑起步经过打磨变成刚好够用。把这句话贴到 README 的第一行,后续所有 prompt 决策都会自动朝这个方向收敛,避免 Agent 把精力浪费在调圆角像素值这种伪问题上。

收尾

克隆源的选择不是审美问题,是工程姿态问题。一个能在 GitHub 上找到 CoinMarketCap 风格参考仓库的零基础用户,会从第一轮 commit 起就跑在正确的领域语义里;如果选择 Vercel 模板起步,则要在第二轮迭代里补足语义层的工作量——这部分工作量看起来不起眼,实际会让上下文窗口大量消耗在「为什么这个页面应该长这样」的反复解释上。无论选哪条路,「目标域相似度优先于 UI 美观度」这条原则都不能松——结构对了,UI 迟早会跟上来;结构错了,再精致的视觉也是空中楼阁。

## Deep Research Skill: 还原目标站点的 UI 技术栈

**[观察]** 当零基础用户说「我想做这样一个网站,长得很像某款加密货币行情页」时,AI Coding Agent 的第一反应往往是「找到目标站点 → 复制源代码 → 改文案配色 → 上线」。但这条路径有个被严重低估的代价:直接克隆回来的 HTML/CSS/JS 是一份「去依赖」的快照——Tailwind 工具类被预编译成了一坨原子 CSS,shadcn/uiRadix Primitives 引用被 inline 成了不可读的 div 嵌套,Framer Motion 的动画变量丢失了原本的 stagger 序列。换句话说,clone 回来的不是「代码」,而是「代码的尸体」。后续任何迭代——加一个图表、加一个暗色模式切换、接入 WebSocket 实时行情——都会被这份尸体反噬,因为 Agent 不再拥有修改的支点:它不知道哪一行 divButton、哪一个 `data-state` 是来自 Radix 的弹层状态机。这正是 Claude Code 团队把 `deep-research` 单独抽成一个 Skill 而不是一条普通 slash command 的原因——它要在动手写代码之前,先把「还原技术栈」这件事从「体力活」升级为「工程前置」。

**Deep Research Skill 的逆向调研流程**

`deep-research` Skill 的核心思想是:不要 clone 站点本身,而是 clone 该站点的「技术栈指纹」。它把目标站点视作一份可被探查的工件,逐层向上还原出「它由哪些开源组件 + 自研模块 + 样式系统 + 动效引擎」拼装而成。一旦还原完成,新项目就可以基于这些真实存在的、可 import 的组件去组装,而不是基于一份剥离开源的克隆体去硬改。这与 Claude Code 默认的 PlanBuildReview 闭环是同构的——Plan 模式关心「做什么」,deep-research 关心「用什么现成的去做」,两者串起来才是完整的工程起点。

这套流程大致分为四步:第一步,**抓取目标站点的 DOM 与静态资源**,通过 GitHub MCP Server 暴露的 `fetch` 能力拿到 HTMLJS chunks、字体、图标 SVGbuild manifest,这一步直接受益于 Model Context Protocol 把外部 IO 抽象成统一工具调用;第二步,**解析资源指纹**,识别出 React/Next.js/Vuehydration 标记(`__NEXT_DATA__` / `data-reactroot`)、Tailwindutility 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 反复引用。

![deep-research-pipeline](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/64b54b30b1e74acf983e717edebf03c4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2hvY2thbmc=:q75.awebp?rk3s=f64ab15b&x-expires=1788796802&x-signature=Tauz5reIo5jjgs2eRBbvPK3%2Bf2A%3D)

**最小调用样例: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「不写代码」承诺的关键支点。

ui-library-detection

候选开源组件记录表

下表是一个典型的 deep-research 调研输出——把目标站点的指纹映射到 GitHub 上现成可用的开源组件,并标注 license、版本、复用门槛。这是后续 Plan 模式的「采购清单」。

指纹类别命中特征候选组件License复用门槛
字体图标<link href="...lucide-static...">lucide-reactISC直接 npm 装,无门槛
样式系统text-xs / bg-muted / border-border 出现频次 > 50%Tailwind CSS + shadcn themeMIT需要接 Tailwind 配置
弹层与下拉[data-state][data-radix-collection-item]Radix UI PrimitivesMIT需要按需 import
动效style="transform: translateY(...); opacity: ..."Framer MotionMITAPI 简单,无门槛
图表canvas 节点 + WebGL contextlightweight-charts / rechartsApache-2.0需读文档
数据表格role="table" + 自定义滚动tanstack/react-tableMIT学习曲线中等
行情刷新setInterval + WebSocket fallback自研 + swr / react-query必须自研
主题切换class="dark" + CSS variablesnext-themesMIT接 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 成本对比

维度手动 CloneDeep-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 CopilotIDE 临时上下文极高,关 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 片段:

// 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 长这样:

// 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… 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...ErrorEmpty 三态——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 数据设计

web-folder-layout

在解决了上一节 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:

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/ 永远只 importlib/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,指系统中某个数据或定义只有一处权威位置,所有其他位置都从这里派生)。

实体字段(最小集合)类型说明
influencerid, handle, displayName, platform, followers, tier, avatarUrl, biostring / number / enumtier 用 seed / growth / whale 三档枚举
callid, influencerId, ticker, side, action, entryPrice, targetPrice, thesis, postedAt, confidencestring / number / enumside 限 long / short,confidence 是 0-1 的小数
performancecallId, hitRate, returnPct, sampleSize, lastUpdatednumber / stringhitRate 与 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 实体的简化版:

{
  "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 都拿到真实渲染所需的全部形态:

[
  {
    "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 的样子:

// 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 hardcodefixture 文件 + 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 化」这件事从一次性的最后冲刺,前置成一条嵌入开发闭环的持续流水线。

impeccable-phases

四阶段: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-radiusshadowspacing 比例,避免后续页面继续沿用 shadcn 默认的 rounded-md。这一阶段产出的不是页面,而是一组「视觉契约」,后面所有改动都要遵循它。

Iterate 阶段 嵌入到 Plan → Build → Review 的闭环里,每次 Build 完一轮、Review 开始之前,detector 会对新写入的文件做一次轻量扫描。扫描结果以 inline comment 的形式贴回文件末尾,提示哪些 className 命中了 AI 默认模式(例如 bg-gradient-to-r from-purple-500 to-pink-500shadow-2xlrounded-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 的情况下就地编辑新规则。

# 最小可用的 detector 配置示例
.claude/impeccable/
├── rules/
│   ├── 01-no-default-gradient.yaml
│   ├── 02-no-mega-shadow.yaml
│   ├── 03-spacing-rhythm.yaml
│   └── 04-typography-mix.yaml
└── config.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 还不知道项目背景的情况下贸然改写文件。

detector-loop

调用 Polish 的最小指令

Polish 是一个独立的子 skill,触发方式是 slash command:

/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 有什么区别?两者的抽象层次完全不同,放在同一个表格里对比会更清晰:

维度ImpeccableTailwind 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 overviewslash 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.tsxglobals.css,这个动作本身就会把上一稿的视觉权重删干净。下一次 review 时手里只剩一份「终稿 vs 初始稿」的二元对比,中间那些「稍微更克制」「稍微更夸张」的中间态全丢了——而这些中间态恰恰是小步快跑最该保留的素材。

要把这件事做对,必须把「串行 prompt 迭代」改成「并行工作区隔离」。Git 自带的 git worktree 就是为这个场景设计的:它允许同一个仓库的多个 working tree(工作树)并存于磁盘的不同目录,共享同一份 .git/ 对象库,但各自 HEAD 指向不同的分支。下面这张图描绘了从主干 fork 出四条平行分支的过程。

git-worktree-fork

为什么是 git worktree 而不是 git clone

直觉上可以 git clone --depth=1 拉四份副本,每个副本各跑一个 Claude Code 进程。但这样做的代价是:四份副本里产生的 commit 历史是「分叉」的,你没法用 git log 在一个视图里横向看四个变体的祖先链;而且它们之间没有共享 node_modules、没有共享 .next/ 缓存,磁盘与冷启动都翻倍。git worktree add 则不同,它只是把同一个 .git/ 对象库里某个分支引用「挂载」到一个新目录:

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 installpnpm 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 误伤:

# 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 端口视觉强制度
smallfeat/restyle-small300112%
mediumfeat/restyle-medium300235%
largefeat/restyle-large300370%
surprise mefeat/restyle-surprise3004不预设

parallel-restyle-runtime

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内部产品、初稿占位
medium2 色 + 渐变次级中高多数 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 之前必须回答下面五题:

  1. 这个分支的视觉强制度得分落在我们想要的区间吗?(对照上表)
  2. 是否影响 dark mode 与 reduced motion 这两个 a11y 底线?prefers-color-schemeprefers-reduced-motion 媒体查询必须保留。
  3. Tailwind config、tokens.cssframer-motion 动画是否引入了未在 spec 里出现的「私有 token」?如果是,需要回写到 spec.md 而不是默默合并。
  4. 是否有 emoji 满天飞——这是 AI 痕迹最重的标志之一。
  5. 四个变体的截图是否都提交到 PR description,以便 reviewer 做横向对比?

规模上来后推荐用 GitHub Actions 给每个变体自动起 preview deployment(参考 vercel.com/docsdocs.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 tokenB1 完全3/10极低仅换主色与字距
V2 深色霓虹A1 tokenB1 完全6/10同 token 体系下加渐变
V3 圆角玻璃A2 组件B2 几乎7/10卡片重写
V4 网格重构A3 区块B3 局部8/10Pricing 与 Features 重排
V5 视觉大改A4 版式B4 整体9/10极高Hero / Features 全重做

variant-tradeoff-matrix

这张表把「好不好看」和「好不好维护」翻译成了同行可比的格子坐标。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/docsui.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 风格):

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.csstailwind.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-450-200单独 PR + 1 名 reviewer
区块级4-8200-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)…

最后是「评审结论写入 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 化的目的不是消灭颜色,而是把「选色」的决定权从单文件挪到主题层,让后续改动一次落地。

hardcode-audit-pipeline

硬编码审计的最小流水线,大致可以拆成「抽取 → 聚类 → 抽样 → 替换 → 复核」五步。抽取阶段用一行 grepsrc/app/components/ 三个目录下的 hex 字面量全部捞出来;聚类阶段按色相(hue)把近似色合并到同一桶,避免「同一个紫色被写成 6 种写法」;抽样阶段从每个桶里挑 3-5 条肉眼复核,判断「这是设计意图还是随手写的」;替换阶段把确认是 token 的 hex 写进 tailwind.config.tstheme.extend.colors 或者 globals.css:root {} 自定义属性;复核阶段再用一次 grep 确认残留为 0。下面这段 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-extraction-flow

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.tstheme.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-subtleborder-defaultborder-strongborder-focus 四个,此时应该回过头合并——除非产品确有四档边框语义。

下面这张表是讲师在 demo 里演示用的真实 before/after 对照,记录了 8 个最具代表性的 hex 替换:

来源文件出现次数Before (hex)After (token)用途
components/hero.tsx12#7c3aedbg-brand-primary品牌主色按钮
components/features.tsx8#a855f7text-brand-accent强调文字
app/globals.css6#f5f5f5bg-surface-muted卡片背景
components/pricing.tsx5#e5e5e5border-subtle卡片边框
components/faq.tsx4#8b5cf6bg-brand-primary/90hover 态
app/layout.tsx3#0a0a0atext-foreground正文文字
components/contact.tsx2#d4d4d4border-strong分隔线
components/footer.tsx2#fafafabg-surface-base页脚底色

替换完成后,全站只剩 bg-brand-primarytext-brand-accentborder-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 variablesTailwind theme.extend.colors
切换成本runtime,无需 rebuildbuild-time,需重启或双套
主题数量1 套主题任意切换多套需手写多份 config
Debug 友好度需查 var 名映射class 名即 token 名
产物体积略大(var 解析开销)最小(tree-shaking 友好)
适合场景暗色模式、A/B 主题单一品牌、build-once-deploy-many

讲师在这套课程里给出的折中是:品牌色与状态色用 Tailwind theme(brand-primaryaccent-hoverborder-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-500violet-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.tsglobals.css 里被一次定位、一次修改、一次回滚。AI Coding Agent 不会主动做这件事——它的默认行为是「能跑就行」,硬编码清理必须由人工 review 阶段显式触发,这也是讲师把这步放进 Plan → Build → Review 闭环里 Review 那一段的原因。做完这一步,全站颜色才有资格进入部署流水线;没做完这一步,前面所有「评审分支」的努力都可能在第一次换肤时被一次性打回。