你的 AI 写的页面有一股味,impeccable 用 59 条规则把它闻出来了

发布于 · 4,221 字 · 约 11 分钟#Github 解读#Agent 框架原文链接
你的 AI 写的页面有一股味,impeccable 用 59 条规则把它闻出来了 封面图
  • 用 59 条确定性规则将模糊的 AI 味拆解为可机器判定的反模式,区分 slop 和 quality 两类
  • 三层检测引擎(正则、静态 HTML、浏览器)从快到精,用大量防御性代码减少假阳性
  • hook 系统以“绝不打断 Agent”为契约,在编辑后和对话结束时自动检测并反馈
  • 通过 PRODUCT.md 和 DESIGN.md 将项目设计语境结构化,供 Agent 遵循,实现设计系统实时校验
  • 示范“确定性规则做地板、LLM 做天花板”的混合模式,适用于多种 AI 辅助场景

大家好,我是若风。

你大概有过这种体验。让 AI Agent 帮你搭一个落地页,代码跑得起来,功能也对,但就是看着别扭。Inter 字体、紫蓝渐变、卡片里套卡片、每个标题上方都顶着一块圆角图标方块。单看每个元素都挑不出大毛病,放一起就是一股说不清的味。

这种味,你能感觉到,但很难讲清楚,更难自动化拦截。Claude Code 帮你写完页面就交付了,它不会回头告诉你「这个 left border 4px 配 border-radius 太典型了,像个 AI」。

Paul Bakaus 的 impeccable 干的事,就是把这股主观的味,拆成 59 条机器能确定性判断的规则,再挂到 Claude Code、Cursor、Codex 这些 Agent 的编辑钩子上。Agent 每改一个 UI 文件,59 条规则立刻扫一遍,发现问题塞回上下文。57320 个 Star 背后的核心赌注只有一句,审美这件事能不能从「全靠模型感觉」退化成「规则先查一遍,剩下的再交给模型」。

一句话定位,和它为什么必须存在

impeccable 官方给自己的定位叫「The design language that makes your AI harness better at design」(让你的 AI 工具更懂设计的设计语言)。它不是又一个 UI 组件库,也不是 design token 生成器,它是一套挂在 Agent 写代码流程上的设计守门系统。

形态上是一「技能」加 23 条子命令,外加一个跨 14 个主流 AI 编码工具都能装的钩子。README 里 Paul 自己点明了动机,现在的模型都在同一批 SaaS 模板上训练,跳过引导就会产出同一批「tells」(暴露身份的特征),Inter 字体、紫蓝渐变、卡片嵌套、灰字压在彩色背景上、每个标题上方的圆角图标方块。

这套话术听起来像营销,但你去翻 antipatterns.mjs 的注册表就信了,他真的一条条列出来还编了号。

把「味」拆成 59 条规则,这件事的硬核在哪

整个项目最值钱的部分,是 scripts/detector/registry/antipatterns.mjs 里那张 59 条的反模式注册表。它把模糊的「AI 味」分成了两个类别。

32 条 slop(烂味)规则专门识别 AI 生成的典型特征。举几条让你感受下颗粒度。

side-tab,侧边装饰性粗边框。原文描述是「卡片某一边一条粗彩色边框,这是最能一眼认出 AI 生成 UI 的特征」。检测逻辑在 checks.mjs 的 checkBorders 函数里,它判定的是,某一边的 border 宽度 ≥ 2px,同时其他边的最大宽度 ≤ 1px 或者这一边至少是其他边的两倍宽,并且这个颜色不是中性色。满足这套组合,就判你用了 side-tab。

overused-font,过度使用的字体。Inter、Roboto、Fraunces、Geist、Plus Jakarta Sans、Space Grotesk 直接进了黑名单,理由很直接,「被用在太多站点上,已经不再显得有辨识度了,每一波 AI 生成 UI 都收敛到这几张脸上」。

ai-color-palette,紫色或紫罗兰渐变配暗色背景上的青色,原文写「最易辨识的 AI 生成 UI 特征」。cream-palette 更狠,连奶油色和米色背景都算,「这已经成了 AI 下意识伸手的那种『安全有品味』的默认底色」。

我读完最大的感受是,这哥们是真在审美层面上和 AI 的惰性较劲,连 italic-serif-display(斜体衬线体大标题)、hero-eyebrow-chip(hero 区的药丸标签)、numbered-section-labels(小号编号章节标签)这种很细节的排版套路都没放过。

27 条 quality(质量)规则管的是更普适的前端基本功,比如 low-contrast(WCAG AA 对比度不足)、line-length(行长超长)、cramped-padding(内边距拥挤)、tiny-text(正文文字过小)、skipped-heading(标题层级跳级)。这批规则的存在说明 impeccable 不是只抓 AI 味,也在抓普通写代码会犯的错。

59 这个数字背后真正的工程含量,不在「列了几条」,在「怎么判定」。

检测器的三层引擎,以及它怎么跟假阳性死磕

读完 checks.mjs 那个 5580 行的文件,你才明白这种工具最难的不是列规则,是判得准。

impeccable 的检测器有三个执行引擎,分别对应三种输入场景。

engines/regex/detect-text.mjs 是纯文本正则引擎,扫源码字符串,最快,用于 LLM 不在场时的快速排查。engines/static-html/detect-html.mjs 是静态 HTML 引擎,带 CSS 级联解析(css-cascade.mjs),能算出每个元素最终继承到的样式。engines/browser/detect-url.mjs 配合 engines/visual/screenshot-contrast.mjs,是浏览器引擎,用 Puppeteer 跑真实渲染,还能做视觉层截图对比。

这三层引擎的设计逻辑很清楚,能用正则解决的不开浏览器,能用静态解析解决的不跑 Puppeteer,性能从高到低,精度也从粗到细。

但真正让我停下来读了好几遍的,是那些和假阳性死磕的代码。

比如 checkBorders 里有一个 BORDER_SAFE_TAGS 白名单,a、button、table 这些标签天然有边框语义,直接跳过 side-tab 检测。但紧接着它又补了一条例外,如果一个 <span> 自己有不透明背景或者渐变背景,它实际上是个被样式化的 chip 或 badge,这时候边框就不再是文本级装饰,而是真正的视觉条纹,所以即使它是 span 也要继续查。

checkColors 里处理 WCAG 对比度时,有一段专门处理 background-clip: text(渐变剪裁文字)的逻辑。因为这时候 color 属性根本不会被实际绘制,渐变才是真正的填充,如果还拿 color 去和背景算对比度,必然是假阳性。所以代码里直接 isGradientClippedText 判断,跳过背景对比度检查,让专门的 gradient-text 规则去抓这个模式本身。代码注释里直接写了「跳过一条规则胜过报一个假阳性」(Skipping a rule beats a false positive here)。

还有一段处理 var(--X) 颜色变量的注释,jsdom 模式下检测器解析不了 CSS 变量,一个深色区块夹在文字和 body 的装饰性渐变之间时,检测器看不见它,最后变成在跟 body 的纸纹噪点算对比度。代码诚实地标注了这种盲区。

读到这些你才理解,一个设计检测工具的成熟度,不看它能抓多少问题,看它为了少误报愿意写多少防御性代码。impeccable 的防御性代码密度是我最近读过最高的之一。

把上面讲的三层引擎、规则注册表、检测调度、共享原语拼到一起,整个项目的架构长这样。

impeccable 系统架构
impeccable 系统架构

从上往下五层。接入层给每家 AI 工具写一个 hook 清单加一条 /impeccable 技能入口。命令与上下文层把项目设计语境固化成 PRODUCT.md 和 DESIGN.md,每条命令先读再动手。检测调度层分两阶段,逐编辑即时扫加对话结束深扫,结果塞回 Agent 上下文。最核心的规则引擎层用 59 条确定性规则做地板,三层引擎从正则到 Puppeteer 从快到精。最底下的共享原语层提供颜色、字体、对比度的数学计算和常量黑名单。这张图也能帮你理解后面要讲的「确定性前兜底,LLM 后收尾」,地板和天花板在哪一层各司其职。

hook 系统,以及它「绝不打断 Agent」的契约

规则写好了,怎么让 Agent 用上。impeccable 走的是 hook(钩子)路线,这也是它能融入现有 AI 编码工作流的关键。

安装时它会往各家工具的 hook 配置里写清单。Claude Code 写进 .claude/settings.local.json,Cursor 写进 .cursor/hooks.json,Codex 写进 .codex/hooks.json,GitHub Copilot 写进 .github/hooks/impeccable.json。每家工具的 hook 协议不一样,impeccable 给每家都写了对应的适配。

钩子挂上去之后,工作流是这样的。

PostToolUse 阶段,Agent 每次直接编辑一个 UI 文件,立刻跑一遍即时层(immediate-tier)规则,发现问题就通过 hookSpecificOutput.additionalContext 字段把提示塞回 Agent 的上下文。不同工具的塞法不一样,Claude Code、Copilot、Codex 是编辑后提示,Cursor 更激进,是写入前拦截(hook-before-edit.mjs),让坏代码根本落不下去。

Stop 阶段,Agent 一轮对话结束时,再跑一遍完整的检测规则(the deep pass),扫这一轮碰过的所有 UI 文件,跟刚才逐文件那次的结果去重,然后统一报一次。

hook.mjs 顶部的注释把整个契约写得清清楚楚,其中最硬的一条是「Contract: never break a turn. Always exit 0」(契约,绝不打断一轮对话,永远以退出码 0 收尾)。

这是个很关键的设计决策。设计检测是辅助不是裁判,如果它因为自己报错就把 Agent 的整轮工作搞崩,用户第一件事就是把它卸了。所以 main() 函数最外层有一个 catch-all,任何意料外的异常都吞掉,只在调试模式(IMPECCABLE_HOOK_DEBUG)下往 stderr 写一行,主进程永远 exit(0)。

这种「我知道自己是个可能出问题的组件,所以我主动把自己降级成一个最多只发提醒的东西」的工程自觉,值得所有想做 Agent 钩子的项目学。

那张 PRODUCT.md 和 DESIGN.md,其实是上下文工程

很多人会把 impeccable 当成一个 lint 工具用,跑跑 npx impeccable detect 查查问题。但你真正用起来会发现,它还有一套更软的设计,就是 /impeccable init 写的两个文件。

PRODUCT.md 记录产品上下文,受众是谁,品牌还是产品定位,语气是激进还是克制,anti-references(反面参考,明确不要什么)。DESIGN.md 记录设计系统,颜色、字体、组件、间距。

这俩文件不是给设计师看的文档,是给 Agent 看的上下文。后面每一条 /impeccable 命令,不管是 polish 还是 critique,都会先读这两个文件,拿到项目的设计语境再做判断。

这其实是把 Agent 设计能力的上限,从「模型自己记不记得这个项目的风格」转移到了「项目自己有没有把风格写成 Agent 能读的结构化文件」。SKILL.md 里有句话点得很透,「Visual authority is evidence, not a filename」(视觉权威靠的是证据,不是一个文件名),意思是光有个 DESIGN.md 不代表你的项目就定型了,模型会根据实际代码里的设计真相来判断该保留还是该替换。

顺带一提,那 4 条 design-system-* 检测规则(design-system-font、design-system-color、design-system-radius、design-system-font-size)就是跟这套机制配套的,它们检查你代码里用的字体、颜色、圆角、字号是不是都在 DESIGN.md 声明的范围内。超出范围就报,等于把 design system 从「写完就忘的文档」变成了「写代码时会被实时校验的契约」。

它的代价,以及几个没绕过去的坎

聊了这么多好的,得讲讲代价。impeccable 不是银弹,拆下来有几个矛盾你自己掂量。

我最担心的一件事,是 59 条规则本身可能变成新的同质化源头。impeccable 解决的是「AI 都长得一样」,可它的解法偏偏是「大家都来遵守同一套规则」。当检测器默认把 Inter、Plus Jakarta Sans、奶油色全判成问题,所有用 impeccable 的项目会不会反过来趋同成另一种「安全的叛逆」。Paul 显然意识到了这点,保留了 detector.ignoreValues 和 detector.ignoreRules 两层配置,让你声明「Inter 是我们的品牌字体,别报了」,还支持文件级内联豁免 <!-- impeccable-disable overused-font: 这是导出的品牌文档 -->。但豁免机制本身也是维护成本,用不好就退化成关警报。

再往工程里钻,hook 会往你的项目根目录写东西,这事在社区吵得挺凶。issue #422 的作者原话是,「对于我们这种严格保持项目根目录干净的人来说,哪怕只是一个『有正当理由产生的』.impeccable/hook.cache.json,我们也宁可完全不放进仓库」。后来团队做了优化,让 hook 状态只在有新发现时才惰性创建,可 issue 还开着,社区一直在催一个 IMPECCABLE_CACHE_ROOT 环境变量把缓存挪出项目根。你看,一个想深度嵌入别人工作流的工具,迟早要撞上别人对工作流洁癖的抵抗。

live 模式踩的是另一个坑,碰到有状态的应用会丢状态。issue #150 是个 SvelteKit 用户的抱怨,他想用 live 改一个有状态的游戏化页面,结果 impeccable 把变体写进了 app.html,整页重载,页面状态全没了。社区给的出路是让 live 按目标框架的实际组件方式注入变体,别粗暴改 HTML。impeccable 的 live 模式现在对 SvelteKit、Next.js、Nuxt、TanStack Start 都有适配器(在 scripts/live/frameworks/ 下),但状态保持这件事远比改 HTML 复杂,适配器只是开了个头。

prompt 膨胀是更隐蔽的一类。issue #525 指出,live 的 Apply 操作里 op 级别的批次字段没做压缩,单个页面的 subprocess prompt 能膨胀到超过 200KB。对任何走 LLM 的流程这都是风险,prompt 越大越贵越慢,还更容易触发模型的上下文衰减。

最后说个不那么结构性的,bus factor(巴士因子)。contributor 列表里 pbakaus 一个人 1145 次提交,第二名人类贡献者 abdulwahabone 54 次,再往下就是个位数。这种 20 倍以上的集中度,搁一个 57K Star 的项目上是个隐患。好在是 Apache 2.0,社区能 fork 兜底,但 Paul 本人就是这项目的灵魂,他要是停更,走向很难讲。

这个项目真正教会我的,是一种「降级兜底」的工程哲学

拆完 impeccable,我带走的不只是「又一个 Agent 设计工具」。

它示范了一种我愿意命名为**「确定性前兜底,LLM 后收尾」**(deterministic floor, LLM ceiling)的混合模式。

这种模式的核心洞察是,LLM 的判断力在审美这种高度主观的领域是不稳定的,今天判得好明天判得歪,而且你没法验证它到底判了没有。而确定性规则恰恰相反,稳定、可验证、可审计,但覆盖面有限,抓得了 side-tab 抓不了「这个布局有没有传达出品牌情绪」。

impeccable 的做法是把两者叠加。59 条确定性规则做地板(floor),它们负责「至少把这些低级 AI 味和基础质量问题拦掉」,这部分不需要 LLM、不需要 API key、可以跑在 CI 里。LLM 审美判断做天花板(ceiling),它负责「在规则之上的主观设计决策」,这部分靠 SKILL.md 那套 23 条命令的 prompt 工程和 init/DESIGN.md 的上下文工程来托底。

地板是硬保证,天花板是软提升。两层之间不冲突,规则负责兜底,模型负责发挥。

这个模式其实不止适用于设计检测。任何「LLM 能力不稳定但又有部分规则可枚举」的场景都能套。代码审查(确定性的 lint 规则做地板,LLM 做架构级审查做天花板),文案生成(禁忌词和品牌 voice 规则做地板,LLM 做创意写作做天花板),甚至安全审计(已知漏洞模式做地板,LLM 做业务逻辑漏洞分析做天花板)。

你想想,为什么这个项目能 9 个月从 0 涨到 57K Star。不是因为它检测规则写得多全,市面上设计 lint 工具有的是。是因为它第一个把「Agent 时代的设计守门」这件事,做成了一个能跨 14 家工具、能挂进编辑钩子、规则和模型分工明确的产品形态。它踩中的是「AI 写代码已经普及,但 AI 写的代码质量还不可靠」这个时间窗口,给了一个工程上能落地的答案。而这个答案的解法,比它的产品形态本身更值得拆来看。

Paul Bakaus 这个人也值得多说一句。jQuery UI 的作者,现在在 Google,bio 里写自己「Created jQuery UI, Google for Creators, Spotter Studio」。一个有前端工程根基、又懂创意的人来做这件事,比纯模型团队来做靠谱得多,他知道哪些味是训练数据带来的,哪些是工程能拦的。impeccable 的每一处防御性代码和假阳性处理,都透着这种「我亲手写过足够多前端,我知道什么报错是误报」的经验感。

想试的话,在你的项目根目录跑一句 npx impeccable install,然后在你用的 AI 编码工具里敲 /impeccable init。如果你用 Claude Code,装完之后它会在你每次改 UI 文件时自动跑那 59 条规则,第一次它把你习以为常的 side-tab 或紫蓝渐变标出来的时候,你大概会和我一样,重新审视一下自己到底被 AI 训练出了多少审美惰性。

评论互动

© 2026 王若风的技术博客 · Powered by Astro