AI 写前端,优化的是演示,不是交付

0 阅读5分钟

AI 写前端,优化的是演示,不是交付

专栏:把 AI 管住(第 00 篇)

大模型写页面时,拟合的是它见得最多的那种前端:文档示例、博客 Demo、十几条字段齐全的 mock。那个分布里,列表很短,请求不会并发,用户也不会在返回前离开页面。

中后台的真实分布几乎处处相反。所以同一句「写个订单列表」,会得到一份能演示的代码,和一份能合并的代码。差的通常不是模型档位,而是规格里有没有写入真实分布。

下面用同一需求的两份生成物说明这件事。

两种规格

A 的规格是愿望:

写个订单列表页,Vue3,Element Plus。

B 的规格是分布:

只改 src/views/order/。按 3000 条设计。
筛选防抖 300ms,过期请求丢弃;空值不抛错;卸载 abort。
key 用 id。分页或虚拟列表,注释写明选择。

A 省略了四个教程里几乎不出现的约束:基数、竞态、空值、生命周期。模型不会主动补问。不问,就按八条数据生成。

两份生成物

A:

<script setup>
import { ref, watch } from 'vue'
import { getOrders } from '@/api/order'
const keyword = ref('')
const orders = ref([])
watch(keyword, async () => {
  orders.value = await getOrders({ keyword: keyword.value })
})
</script>
<template>
  <el-input v-model="keyword" />
  <el-table :data="orders">
    <el-table-column type="index" />
    <el-table-column label="金额">
      <template #default="{ row }">{{ row.amount.toFixed(2) }}</template>
    </el-table-column>
  </el-table>
</template>

B:

<script setup>
import { ref, watch, onBeforeUnmount } from 'vue'
import { getOrders } from '@/api/order'
const keyword = ref('')
const orders = ref([])
const loading = ref(false)
let timer = 0
let seq = 0
const ac = new AbortController()
async function load() {
  const my = ++seq
  loading.value = true
  try {
    // 3000 条走分页,禁止一次渲染全表
    const data = await getOrders({ keyword: keyword.value, pageSize: 20 }, ac.signal)
    if (my === seq) orders.value = data
  } finally {
    if (my === seq) loading.value = false
  }
}
watch(keyword, () => {
  clearTimeout(timer)
  timer = setTimeout(load, 300)
})
load()
onBeforeUnmount(() => { ac.abort(); clearTimeout(timer) })
</script>
<template>
  <el-input v-model="keyword" />
  <el-table v-loading="loading" :data="orders">
    <el-table-column prop="id" />
    <el-table-column label="金额">
      <template #default="{ row }">{{ row.amount?.toFixed(2) ?? '-' }}</template>
    </el-table-column>
    <template #empty>暂无订单</template>
  </el-table>
</template>

B 在合并前仍改了空态文案。生成物是初稿。把初稿当交付,是 A 真正的问题。

表格

被省略的约束教程默认真实分布A 的结果
基数8~20 条10³ 量级当短列表写
输入一次提交连续按键无防抖,请求乱序
字段总有值可空toFixed 抛错
生命周期页面常驻会卸载卸载后仍写入

前端不可替代的部分,经常就是把这四类约束从潜台词写成接口。这不是「写得更细」,这是在改模型的目标函数:从「像能跑」改到「在真实分布下可运维」。

和模型协作的三层接口

Rule、Skill 只是各工具的文件名。工程上它们对应三层早就存在的东西:

表格

有效期常见载体作用
仓库不变量跨任务AGENTS.md、Cursor Rule、Copilot instructions、CLAUDE.md禁令:永远不准怎样
任务类型 SOP某类活Skill、斜杠命令、提示词模板大列表、新页面怎么做
单次规格这一单当前提示词范围、数据量、验收

只使用第三层,且第三层还是一句话,生成物会落在教程分布里。三层都写成可判定的句子,模型才有机会偏离 Demo。

工具可以换,层不能省。形容词不能当不变量:「注意性能」没有真值;「N>200 必须分页」有。

最小不变量

放入仓库根目录 AGENTS.md

Vue3 + TS + Vite + Pinia + Element Plus。禁止另引 UI 库。
src/views/ 页面;src/api/ 接口;src/components/ 公共组件。
必须:script setup;列表用稳定 id;空/错/加载有 UI;卸载取消请求;金额先判空。
禁止:密钥进前端;v‑html 渲染接口数据;公共函数 any;顺手重构无关文件;
列表可能超过 200 条时按短列表实现(分页或虚拟列表,注释写明)。
提交前:空数组不抛错;快筛不串数据;离开页面无报错;diff 不含无关文件。

没有这一层,Rule 的文件格式再正确也没有约束可执行。格式放到下一篇。

单次规格

说明书管跨任务,管不住这一单的含糊。最低限度:

范围:
目标(用户侧可观察的结果,禁止「更好用」):
数据量 / 并发假设:
禁止:
验收(全满足才结束):

愿望填不进这些格子。填不进的格子,就是生产里先坏的那几处。

另外三件事会把规格再次打回教程分布:一次要求性能、样式、重构同时完成;不声明基数;不声明不可修改的路径。

本系列

先把「如何调用模型」写完,再谈如何把模型接到产品里。

表格

问题
01不变量写成作文,为何等于没有
02SOP 为何必须进仓库,而不是进聊天
03提示词作为规格,而不是愿望
04未注入的上下文等于不存在
05补全、行内修改、Agent 的适用边界
06生成物的审查:diff、测试、生命周期
07哪些第一版不该由模型写
08同一页面,规格完整与否的六处差别

结论

同一模型。改变的是规格密度,以及生成物在流程里的地位:初稿,还是交付。

专业前端要的不是页面存在,而是:真实基数下可交互,空值不抛错,生命周期结束后不再写入,下一迭代仍可修改。这些不会出现在五个字的愿望里。它们只出现在不变量、SOP 和单次规格里。

下一篇写不变量:为何两百行「编码规范」拦不住 v‑html