面向代理使用的设计系统
这项技能指导设计决策,但并非强制规定。每一条建议都可能因为合理的项目目标(专家表达、品牌需求、平台限制、现有约定)而被否决。请指明权衡,并做出有意的相反选择。示例采用 React 和 TypeScript,因为模型在这些技术栈上的先验最为清晰;但推理方法可适用于任何技术栈。
目标
代码生成模型是一个采样器。它会根据训练中学到的内容(先验)和会话中的内容(上下文)输出最合理的程序,然后保留那些能通过检查的执行结果。一个人本 API 问的是“正确的事情能否被表达出来?”而代理优先的 API 问的是“在看起来正确的事情中,有多少是真正正确的?”因此目标有两部分:让最可能的程序成为正确的程序,让不正确的程序在发布前失败。
这本质上是普通 API 设计(Bloch 的“易于正确使用,难于错误使用”,Minsky 的“使非法状态不可表示”,成功之坑)在两个更严格假设下的应用:消费者的熟悉程度是公开代码上的可测量分布,消费者仅验证可执行的内容。
消费者的三个属性
以下所有内容均源自这些属性。当一个主张对决策至关重要或需要本地测量时,请阅读 证据账本;其中有来源、范围说明和测量协议。
-
先验是频率塑造、版本混合且不可修补的。 模型知道某个模式的概率与其在训练数据中出现的频率成正比;准确率随语料库频率呈对数线性增长。一个库的所有历史版本都叠加成单一分布,这就是模型 25% 到 38% 的时间会输出已弃用 API,且在版本锁定任务上得分约 50% 的原因。库自身无法编辑此先验,只能匹配或覆盖它。
-
上下文覆盖先验,但只有实际存在的上下文才有效。 从过时代码中提取的提示 70% 到 90% 会产生已弃用用法;从当前代码中提取的提示 9% 到 18% 会产生已弃用用法。文件中的兄弟代码对输出的影响远大于库发布的任何东西。检索到的文档有帮助;一个简单的版本标签也有少许帮助。但代理从未加载的已发布文档则完全无效。
-
只有可执行的反馈才是验证。 类型检查器是确定性的、廉价的,并且始终处于循环中,LLM 生成的 TypeScript 中 94% 的编译错误是类型错误,因此它能捕获大量问题。渲染成本高、需要设置,而且即使有截图在手,视觉-语言模型对布局的判断也很弱。代码中类型未编码的任何错误模式,如果模型预测错误,就会直接发布。
两个推论。错误的成本是不对称的:人类看到糟糕的间隙能在几秒钟内修复,而代理的糟糕间隙会直接发布,因此“易于修复”的价值远低于“难以出错”。并且演进必须是增量的:重命名或语义变更会困扰代理直到下一次训练,旧的形式永远不会完全离开分布;因此当必须破坏性变更时,将迁移信息放在类型和关联文档中,而不仅仅是变更日志。
三个策略
每个设计杠杆都属于其中之一。评估设计时,请问这个设计使用了哪个策略,又削弱了哪个。
策略 1:让默认猜测正确
代理的第一猜测是先验加上可见上下文。有两件事塑造它。
名称是对分布的赌注。 名称的价值等于清晰度乘以一致性:其公开用法有多少单一含义,乘以该含义是否是你的定义。清晰度随着语料库大小、年龄和稳定性而变化,因此 HTML 和 CSS 规范词汇优于 ARIA 模式名称优于跨库词汇(sm/md/lg、primary/danger/solid/outline/ghost)优于任何单一库的惯用法;尽可能深入语义层。一致性是二元的,一个熟悉的名字却带有不同行为是最坏的情况:模型会自信地应用已知含义,且代码可编译。当没有清晰的名称符合你的语义时,从已学习的语法中组合出一个新名称。Page/PageHeader/PageTitle 在任何主流库中都不存在,但 X/XHeader/XTitle 的变体模式免费传达了层级和子元素顺序,并且在自动补全和代理自身计划中保持可发现性。一个其含义依赖于另一属性的 prop(例如在 direction 切换下的 align)是两个耦合的赌注;将它们解耦为一个组件一个轴或显式命名。收益是减少了一个自由维度,而不是拯救了一个先验;模型处理 CSS 自身的轴翻转没有问题。
可见的代码库就是提示。 兄弟用法、目录和规范示例才是代理实际条件的输入。规律性压缩了该提示:如果每个组件都用相同的词汇表达相同的轴,那么上下文中的两个组件就能教会整个目录;而每一个同义词或按组件处理的例外都是一个需要单独教授的事实,通常不会被教授。目录划分了输出:代理在阅读代码之前就以“页面标题”和“工具栏”等单位规划,并将计划强行通过现有组件;因此请根据这些单位命名组件。示例展示的内容将成为工作流,包含 className 覆盖;展示预期路径,并保持示例版本匹配且与代理必须接触的代码同位置。
先验强度证明了在代理必须编写的语法上进行合规的合理性。它从不证明暴露代理不应做出的决策是合理的;那是策略 3。
策略 2:让错误输出在发布前失败
任何渲染或运行时本应捕获的问题必须在更早的阶段失败,否则它们就会发布。
在类型能表达的地方,就放在类型里。 使用封闭联合类型而不是没人设计的布尔组合。使用带判别联合类型处理模式相关的 props。对于某部分不支持的内容使用 Omit,而不是记录“不要在这里使用这个”。使用必需 props 传递那些库本应处理但留给消费者的含义行为层(对话框标题、图标按钮标签、错误文本;Radix 和 React Aria 仅在控制台中警告,代理看不到)。为 asChild/as 的不支持情况产生编译错误,或提供显式的非多态变体如 ButtonLink。每次测试都一样:补全 prop 的代理会看到合法的词汇表,非法程序无法编译。
在类型无法表达的地方,让差异在代码编写处可见。 名称承载着类型无法编码的布局期望(容器拥有其内边距,块元素填充其父元素,间距来自父元素的间隔;案例研究列出了它们)。一个打破其名称创建的期望的组件可以编译并通过渲染,但在代理看不到的维度上默默出错。在调用点(一个必需的 prop,一个不冲突的名称)或代理可靠加载的上下文中(代码旁边的规则)标记这个差异。代理不会遇到的文档不算数。
策略 3:移除决策
一个只有渲染后才能判断正确性的决策属于库,而不是调用点。一个原则四个粒度:
- 值。 量化每个量级,通过语义角色解析外观:调用点使用
gap="3"和tone="danger",px 和 hex 在主题中。模型了解 flex 和 grid 机制,但它们无法在盲选时选出 12 还是 16,且独立选择会随着文件漂移成无人能见的节奏。规范关键词(justify="space-between")保持语义自由,同时量级去除猜测。 - 组合。 拥有布局,而不仅仅是小部件。Braid 的规则是简洁版:组件不输出外部空白,布局组件拥有所有间距。一个止步于小部件边界的系统会让每个调用点即兴发挥。
- 模式。 当组合重复出现时(
FormField、EmptyState、PageHeader、ConfirmDialog),将其作为组件发布。每次从原始组件重新推导都是一次偏离的机会。 - 适配。 通过构造处理视口、内容长度、书写方向、动态和对比偏好以及加载/空/错误状态,或在类型中强制要求。调用点的媒体逻辑是一个盲决策。
逃生舱(className、style、CSS 变量)应该存在,并位于默认路径之下:可用、排序、从不展示为主。如果它们是示例的工作流,那么策略 1 使它们成为默认猜测。
模式决定可读性还是契约
存在两种一致的哲学;shadcn 是第一种最清晰的表述。
- 可读性。 将源码发布到仓库中。代理读取并编辑它,样式在代码编写处可见。当代理拥有组件时正确:它会打开文件,而可读的实现(同位置样式、最小间接层)正是它需要的。
- 契约。 暴露类型化 API 并隐藏实现。当代理从应用代码消费组件时正确:它看到导入、类型和少量兄弟用法,很少打开实现文件。可读的样式就是可写的样式,而调用点样式正是策略 3 要移除的熵。
大多数系统两者都需要,按受众划分:为维护 components/ 的人提供可读性,为从那里导入的人提供契约。在应用策略之前,确定文件处于哪种模式。当系统构建在 shadcn 之上或与之对比时,阅读 shadcn 案例研究;它详细说明了保留什么、更改什么以及原因。
张力
两个,每个有多面。指明权衡;不要假装它是免费的。
- 集中的概率与表达力。 每移除一个自由度,就是某人失去的一项能力。这表现为意图组件与原始组件、封闭轴与正交 props、以及扩展一个轴与拆分它(在扩展时每个值回答相同的问题;shadcn 的
Buttonvariant,融合了意图、样式和元素身份,是典型的融合轴)。排序的逃生舱是通常的答案。 - 先验对齐与所有权。 匹配约定换来免费的正确性,但也引入了约定的缺陷和变动。这表现为熟悉名称与独特名称(独特名称成本是发现性;遵循语法的创新名称可恢复大部分发现性)以及优先友好与更好设计。偏离一次付出对齐成本;保留一个缺陷永远付出正确性成本。
测量而非轻信
三个属性在模型级别测量。策略是与这些测量一致的机制,而非在任何 UI 库上测量的;证据文件标记了哪些是哪个。在采纳一条规则前,运行它:20 到 40 个现实的 UI 任务,每个场景采样几次(无上下文、类型加两个兄弟用法、完整源码);评分首次尝试的编译、被类型捕获的误用与静默错误、兄弟调用点之间的 token 方差、可访问性 lint 和简短视觉检查;更改一个设计决策;重新运行。保留能改进数字的规则。
审查清单
- 策略 1:一个熟悉类似库的模型会在第一次尝试时猜对吗?如果名称是借用的,行为是否完全相同?规范示例是否展示预期路径?
- 策略 2:每种合理的误用是否都无法编译?当名称的视觉预期被打破时,这种打破是否在代码编写处可见?
- 策略 3:是否有任何值、组合、模式或适配留给调用点盲选?
- 模式:这个文件是由代理拥有还是消费?可读性/契约的选择是否经过深思熟虑?
- 演进:此更改是否会破坏根据去年先验编写的代码?迁移信息是否在类型和关联文档中可见?