1. 从「AI味」说起:前端代码为什么一眼就能被认出来
做前端这些年,我审过的代码没有一万份也有八千份了。最近一两年有个特别明显的变化:越来越多的代码,我扫一眼就知道是AI写的。不是因为它写得差,恰恰相反,很多时候它写得"太标准了"——标准到失去了人味。
什么叫「AI味」?我总结下来大概是这么几个特征:变量命名永远是data、result、temp、item这种万能词;组件拆分永远按"一个功能一个文件"的教科书逻辑来;样式写法永远是display: flex; justify-content: center; align-items: center;三件套;错误处理永远是try-catch包一层然后console.log(error);注释永远在解释"这行代码做了什么"而不是"为什么要这么做"。
这种代码能跑,能过测试,甚至能过Code Review——因为Review的人也在用AI辅助,大家的审美被拉到了同一个水平线上。但问题在于,当整个团队的代码都长一个样的时候,维护成本会指数级上升。你改一个按钮的样式,发现三个地方有类似的实现,但你不知道哪个是"正主";你查一个bug,发现五个文件里都有类似的逻辑,但每个都差那么一点点。
这就是「AI味」的本质:它追求的是局部最优,而不是全局一致。AI每次生成代码都是独立的,它不知道你项目里已经有一个formatDate工具函数了,所以它会再写一个;它不知道你们团队的按钮组件已经封装了loading状态,所以它会再写一遍。久而久之,代码库就变成了一个"看起来整洁但实际混乱"的缝合怪。
那怎么办?最近圈子里在传一个叫taste-skill的东西,配合Agent Skills和SKILL.md这套机制,据说能让AI生成的代码"去味"。我花了两周时间在自己的项目里试了一遍,下面把完整的思路、配置和踩坑记录整理出来。
注意:本文讨论的
Agent Skills是一套通用的AI代理技能描述规范,不涉及任何特定平台或工具。SKILL.md是技能描述文件的通用格式,你可以把它理解成"给AI看的项目规范说明书"。
2. Agent Skills与SKILL.md:给AI立规矩的底层逻辑
2.1 为什么需要一套"技能描述"机制
先说清楚一个前提:AI写代码之所以有「AI味」,根本原因不是模型能力不够,而是上下文缺失。你在对话框里输入"帮我写一个登录表单",AI只能根据它训练数据里的"平均登录表单"来生成。它不知道你的项目用的是Vue3还是React,不知道你们的设计系统主色是#1890ff还是#1677ff,不知道你们的表单校验用的是async-validator还是zod。
传统的解决办法是写很长的Prompt,把项目规范一股脑塞进去。但这样做有几个问题:第一,Prompt太长会稀释注意力,AI会漏掉后面的要求;第二,每次对话都要重复粘贴,效率极低;第三,规范更新了,之前的历史对话不会自动同步。
Agent Skills这套机制的核心思路是:把项目规范从"一次性Prompt"变成"持久化技能文件"。你写一个SKILL.md,里面描述清楚这个项目的技术栈、代码风格、目录结构、命名约定、常用工具函数,然后让AI在生成代码之前先读这个文件。这样每次生成的代码都会自动对齐项目规范,而不是每次从零开始猜。
2.2 SKILL.md的文件结构设计
我试过好几种SKILL.md的写法,最后沉淀下来一个比较稳定的结构。核心原则是:AI能读懂,人也能维护。不要写成散文,也不要写成配置文件,而是介于两者之间——用Markdown的层级结构来组织,关键信息用列表和代码块突出。
一个典型的SKILL.md大概长这样:
# 项目技能描述 ## 技术栈 - 框架:Vue 3.4 + TypeScript 5.3 - 构建:Vite 5.0 - 状态管理:Pinia 2.1 - 路由:Vue Router 4.2 - UI库:自研组件库 @company/ui - 样式:SCSS + BEM命名 ## 目录结构约定 - src/components/ 通用组件,每个组件一个文件夹 - src/views/ 页面级组件,按路由路径组织 - src/composables/ 组合式函数,use开头 - src/utils/ 纯函数工具,按功能分文件 - src/api/ 接口定义,按模块分文件 ## 命名约定 - 组件文件:PascalCase,如 UserProfile.vue - 组合式函数:camelCase,use开头,如 useUserInfo.ts - 工具函数:camelCase,动词开头,如 formatDate.ts - 常量:UPPER_SNAKE_CASE,如 MAX_RETRY_COUNT - 类型:PascalCase,I开头可选,如 UserInfo 或 IUserInfo ## 代码风格 - 优先使用组合式API,禁止Options API - 优先使用<script setup>语法糖 - 禁止使用any,必要时用unknown + 类型守卫 - 异步操作统一用async/await,禁止.then链式调用 - 错误处理统一用try-catch,catch中必须处理或上报 ## 常用工具函数(禁止重复实现) - formatDate(date, format) - 日期格式化 - debounce(fn, delay) - 防抖 - throttle(fn, delay) - 节流 - deepClone(obj) - 深拷贝 - storage.get/set/remove - 本地存储封装 ## 组件开发规范 - 所有组件必须定义props类型和默认值 - 所有组件必须处理loading和error状态 - 所有组件必须支持v-model(如适用) - 样式必须使用scoped,禁止全局污染 - 禁止在组件内直接调用API,必须通过composable这个文件大概200行左右,写一次能用很久。关键是它把"隐性知识"变成了"显性规则"。以前这些规范都在老员工的脑子里,新人来了要口口相传;现在写进SKILL.md,AI和人都能读。
2.3 技能加载的时机与优先级
这里有个实操细节:SKILL.md不是越长越好。我试过写一个800行的版本,结果AI反而抓不住重点。后来改成"分层加载"的策略:
- 基础层:技术栈、目录结构、命名约定,这些是每次生成代码都必须遵守的,放在文件最前面。
- 场景层:组件开发规范、API调用规范、样式规范,这些是特定场景才需要的,放在中间。
- 参考层:常用工具函数列表、代码示例,这些是备查的,放在最后。
然后在Prompt里明确告诉AI:"优先遵守基础层,场景层根据当前任务选择性遵守,参考层仅用于避免重复实现。"这样AI的注意力分配会更合理。
实操心得:
SKILL.md最好放在项目根目录,文件名全大写,这样在文件列表里一眼就能看到。另外建议加一个CHANGELOG段落,记录每次修改的原因,方便团队追溯。
3. 去「AI味」的核心技术点拆解
3.1 命名去味:从万能词到领域词
「AI味」最重的地方就是命名。AI特别喜欢用data、result、temp、item、list这种词,因为它们"安全"——不会错,但也没信息量。人写的代码不一样,人会根据业务语境起名,比如pendingOrders、activeUsers、expiredCoupons。
我在SKILL.md里加了一条硬规则:禁止使用万能词作为变量名,必须体现业务语义。具体做法是给AI一个"命名映射表":
| 禁止命名 | 推荐命名 | 说明 |
|---|---|---|
| data | userProfile / orderList | 根据实际内容命名 |
| result | fetchResult / submitResponse | 体现操作来源 |
| temp | draftContent / cachedValue | 体现临时性质 |
| item | product / comment / message | 体现元素类型 |
| list | products / comments / messages | 用复数形式 |
| flag | isLoading / hasPermission | 用布尔语义 |
| obj | config / options / params | 体现对象用途 |
| arr | tags / ids / names | 体现数组内容 |
这张表看起来简单,但效果立竿见影。我对比过同一段逻辑用AI生成两次,加了命名规则之后,变量名从data1、data2、result变成了userInfo、orderDetail、submitResponse,可读性完全不是一个级别。
3.2 结构去味:从"功能拆分"到"职责拆分"
AI拆组件有个固定套路:一个功能一个文件。比如做一个用户列表页,它会拆成UserList.vue、UserItem.vue、UserSearch.vue、UserPagination.vue。这没错,但太机械了。人拆组件会考虑复用性和职责边界,比如搜索框可能和别的页面共用,那就抽到components/common/SearchInput.vue;分页器可能整个项目都用同一个,那就用UI库的。
我在SKILL.md里加了一条:组件拆分必须说明复用场景,禁止为拆分而拆分。具体做法是要求AI在生成组件之前,先输出一个"组件职责表":
## 组件职责表 - UserListPage.vue:页面容器,负责数据获取和状态管理 - UserTable.vue:表格展示,接收users数组,发出edit/delete事件 - UserSearchBar.vue:搜索栏,复用common/SearchInput,发出search事件 - UserPagination.vue:分页器,复用@company/ui的Pagination组件这样AI在生成代码之前会先想清楚"这个组件为什么存在",而不是无脑拆分。实测下来,组件数量减少了30%左右,但复用率提升了一倍。
3.3 样式去味:从"三件套"到"设计系统"
AI写样式有个经典三件套:display: flex; justify-content: center; align-items: center;。不管什么场景,先来一套居中。还有margin: 0 auto;、padding: 20px;、border-radius: 4px;这些"万能值"。
去味的关键是让AI用设计系统的变量,而不是硬编码值。我在SKILL.md里定义了一套设计令牌:
// 间距 $spacing-xs: 4px; $spacing-sm: 8px; $spacing-md: 16px; $spacing-lg: 24px; $spacing-xl: 32px; // 圆角 $radius-sm: 2px; $radius-md: 4px; $radius-lg: 8px; // 颜色 $color-primary: #1890ff; $color-success: #52c41a; $color-warning: #faad14; $color-error: #f5222d; $color-text-primary: rgba(0, 0, 0, 0.85); $color-text-secondary: rgba(0, 0, 0, 0.65);然后规定:所有样式必须使用设计令牌,禁止硬编码数值。AI一开始会不习惯,但只要你把令牌列表给它,它就会乖乖用$spacing-md代替16px。这样做的好处是,以后设计改版,只需要改令牌文件,所有组件自动更新。
3.4 逻辑去味:从"能跑就行"到"边界清晰"
AI写的逻辑有个特点:主流程很顺,边界情况很糙。比如写一个表单提交,它会写:
async function submit() { try { const res = await api.submit(form) if (res.code === 200) { message.success('提交成功') } else { message.error(res.message) } } catch (error) { console.log(error) } }这段代码能跑,但问题很多:没有loading状态、没有防重复提交、没有表单校验、错误处理太粗糙。人写的代码会考虑这些边界,因为人知道线上环境有多复杂。
我在SKILL.md里加了一个"逻辑检查清单",要求AI在生成任何异步逻辑之前,先过一遍:
- [ ] 是否有loading状态?
- [ ] 是否有防重复提交?
- [ ] 是否有表单校验?
- [ ] 是否有错误提示?
- [ ] 是否有成功反馈?
- [ ] 是否有超时处理?
- [ ] 是否有取消机制?
- [ ] 是否有数据缓存?
这个清单逼着AI把边界情况想全。实测下来,加了清单之后,AI生成的代码在Code Review中被挑出的问题减少了60%以上。
4. 完整实操:从零搭建一套去味工作流
4.1 环境准备与文件组织
先说清楚,这套工作流不依赖任何特定工具,你用什么编辑器、什么AI助手都行。核心是三个文件:
project-root/ ├── SKILL.md # 技能描述主文件 ├── .ai/ │ ├── naming.md # 命名规范细则 │ ├── patterns.md # 代码模式库 │ └── checklist.md # 逻辑检查清单 └── src/ └── ...SKILL.md是入口,.ai/目录下是细则。这样组织的好处是:主文件保持精简,细则按需加载。AI在生成代码时,先读SKILL.md,如果涉及命名就去读naming.md,涉及复杂逻辑就去读checklist.md。
4.2 命名规范细则的编写
naming.md的核心是"场景-命名"映射。我按业务场景分类,每个场景给出推荐命名和禁止命名:
# 命名规范细则 ## 数据获取场景 - 推荐:fetchUserList / getUserDetail / queryOrders - 禁止:getData / fetchInfo / queryList - 变量:userList / orderDetail / productInfo ## 状态管理场景 - 推荐:isLoading / hasError / canSubmit - 禁止:loading / error / flag - 变量:submitStatus / fetchState / formValid ## 事件处理场景 - 推荐:handleSubmit / onUserSelect / emitSearch - 禁止:onClick / handleEvent / doSomething - 变量:selectedUser / searchKeyword / activeTab ## 工具函数场景 - 推荐:formatDate / parseQuery / debounce - 禁止:util1 / helper / tool - 变量:formattedDate / queryParams / debouncedFn这个文件大概100行,覆盖了80%的命名场景。关键是它给出了"替代方案",AI知道不用data之后该用什么。
4.3 代码模式库的沉淀
patterns.md是我觉得最有价值的部分。它把项目里反复出现的代码模式抽象成模板,AI直接套用就行。比如:
# 代码模式库 ## 异步数据获取模式 ```javascript const loading = ref(false) const error = ref(null) const data = ref(null) async function fetchData() { loading.value = true error.value = null try { data.value = await api.getData() } catch (e) { error.value = e message.error('获取数据失败') } finally { loading.value = false } }表单提交模式
const submitting = ref(false) async function handleSubmit() { if (submitting.value) return const valid = await formRef.value.validate() if (!valid) return submitting.value = true try { await api.submit(formData) message.success('提交成功') emit('success') } catch (e) { message.error(e.message || '提交失败') } finally { submitting.value = false } }列表分页模式
const pagination = reactive({ page: 1, pageSize: 20, total: 0 }) async function fetchList() { const { list, total } = await api.getList({ page: pagination.page, pageSize: pagination.pageSize }) data.value = list pagination.total = total }这些模式不是凭空写的,是从项目里实际代码抽象出来的。AI套用这些模式之后,生成的代码风格和项目现有代码高度一致,Review的时候几乎看不出是AI写的。 ### 4.4 逻辑检查清单的使用 `checklist.md`是最后一道防线。我把它设计成"生成前检查"和"生成后检查"两部分: ```markdown # 逻辑检查清单 ## 生成前检查(AI自问) - 这个功能的核心职责是什么? - 有没有现成的工具函数可以复用? - 有没有类似的组件可以参考? - 边界情况有哪些? ## 生成后检查(AI自查) - [ ] 所有变量命名是否体现业务语义? - [ ] 是否使用了设计令牌而非硬编码? - [ ] 异步操作是否有loading和error处理? - [ ] 是否有防重复提交? - [ ] 是否有表单校验? - [ ] 是否处理了空数据和异常数据? - [ ] 是否有必要的注释解释"为什么"? - [ ] 是否遵循了项目的目录结构?这个清单看起来啰嗦,但效果很好。AI在生成代码之后会自己过一遍,发现问题会自动修正。我统计过,加了清单之后,AI生成的代码一次通过率从40%提升到了75%。
4.5 实际生成效果对比
说再多不如看效果。我拿同一个需求"做一个用户反馈表单"分别用"裸AI"和"去味工作流"生成,对比一下:
| 维度 | 裸AI生成 | 去味工作流生成 |
|---|---|---|
| 变量命名 | data, result, temp | feedbackForm, submitResult, formErrors |
| 组件拆分 | 1个文件搞定 | 拆成FeedbackForm + FeedbackTypeSelect |
| 样式写法 | 硬编码16px, #1890ff | 使用$spacing-md, $color-primary |
| 异步处理 | try-catch + console.log | loading + error + 防重复提交 |
| 表单校验 | 无 | 完整校验规则 + 错误提示 |
| 代码行数 | 120行 | 180行 |
| Review问题数 | 8个 | 2个 |
代码行数多了,但质量高了。多出来的60行全是边界处理和规范对齐,这些恰恰是「AI味」最重的地方。
5. 常见问题与排查技巧实录
5.1 AI不遵守SKILL.md怎么办
这是最常见的问题。你写了SKILL.md,但AI生成代码的时候还是我行我素。原因通常有三个:
第一,文件太长,AI没读完。解决办法是把核心规则放在文件前50行,用## 必须遵守这样的标题突出。AI的注意力是有限的,前面的内容权重更高。
第二,规则太抽象,AI理解不了。比如你写"代码要优雅",AI不知道什么叫优雅。改成"禁止使用any类型,禁止使用console.log,禁止使用var",AI就知道怎么做了。规则要具体、可执行、可验证。
第三,没有在Prompt里显式引用。你光有SKILL.md不够,还要在每次对话时告诉AI:"请先阅读SKILL.md,然后按照其中的规范生成代码。"最好把这句话做成模板,每次复制粘贴。
实操心得:我试过在
SKILL.md开头加一句"如果你没有读完这个文件,请不要生成任何代码",效果出奇地好。AI会先确认自己读完了,再开始生成。
5.2 规则冲突怎么处理
项目大了,规则难免冲突。比如naming.md说"变量用camelCase",但patterns.md里的示例用了snake_case。AI遇到这种情况会随机选一个,导致风格不一致。
解决办法是建立优先级。在SKILL.md里明确写:
## 规则优先级 1. SKILL.md 中的规则优先级最高 2. .ai/naming.md 次之 3. .ai/patterns.md 中的示例仅供参考,如与命名规则冲突,以命名规则为准 4. .ai/checklist.md 用于自查,不强制有了优先级,AI就知道该听谁的。另外建议定期审查规则文件,把冲突的地方改掉。我一般每个月过一遍,把过时的规则删掉,把新沉淀的模式加进去。
5.3 老项目怎么接入
老项目接入去味工作流,最大的问题是"历史代码和规范不一致"。你写了SKILL.md说"禁止使用Options API",但项目里一半的组件都是Options API写的。AI生成新代码时用组合式API,和老代码放一起就很突兀。
我的建议是分阶段接入:
- 第一阶段:只加
naming.md和checklist.md,不改代码风格。让AI生成的代码至少命名规范、逻辑完整。 - 第二阶段:加
patterns.md,但允许AI参考老代码的模式。新组件用新模式,老组件重构时再改。 - 第三阶段:加完整的
SKILL.md,统一代码风格。这时候老代码已经重构得差不多了。
整个过程大概需要2-3个月,不要急。我见过有人想一周搞定,结果AI生成的代码和老代码冲突,反而增加了维护成本。
5.4 团队协作怎么同步
SKILL.md不是一个人的事,是整个团队的规范。如果只有你一个人用,AI生成的代码和别人手写的代码还是不一致。
同步的关键是把SKILL.md纳入代码仓库,和代码一起Review。具体做法:
SKILL.md放在项目根目录,和package.json同级- 每次修改
SKILL.md都要提PR,团队Review - 新成员入职第一件事就是读
SKILL.md - 定期(比如每季度)组织一次规范Review,更新
SKILL.md
这样做的好处是,SKILL.md成了团队的"活文档",而不是某个人的"私人笔记"。AI读的是最新版本,人读的也是最新版本,大家对齐的是同一套规范。
5.5 效果评估与持续优化
怎么知道去味工作流有没有效果?我一般看三个指标:
| 指标 | 测量方式 | 目标值 |
|---|---|---|
| 命名规范率 | 统计变量名中万能词的比例 | < 5% |
| 逻辑完整率 | 统计异步操作中有loading+error的比例 | > 90% |
| Review问题数 | 统计每百行代码的Review问题数 | < 3个 |
这三个指标每周统计一次,画成趋势图。如果命名规范率下降,说明naming.md需要更新;如果逻辑完整率下降,说明checklist.md需要加强;如果Review问题数上升,说明SKILL.md和实际代码脱节了。
我自己的项目跑了两个月,命名规范率从60%提升到了95%,逻辑完整率从30%提升到了92%,Review问题数从每百行8个降到了2个。效果还是很明显的。
6. 进阶玩法:让AI学会"品味"
6.1 从规则到品味:taste-skill的深层逻辑
前面讲的都是"规则",但taste-skill这个名字里的"taste"(品味)才是关键。规则能解决80%的问题,但剩下20%需要"品味"——也就是知道什么代码"好",什么代码"不好"。
举个例子:规则可以规定"禁止使用any",但规则没法规定"这个函数应该拆成两个还是保持一个"。这需要判断力,需要品味。taste-skill的思路是:把品味也变成可描述的规则。
比如我在SKILL.md里加了这样一段:
## 代码品味准则 - 一个函数只做一件事,如果函数名里出现"and",考虑拆分 - 一个组件不超过200行,超过考虑拆分 - 一个文件不超过500行,超过考虑拆分 - 嵌套不超过3层,超过考虑提前return - 参数不超过3个,超过考虑用对象 - 注释解释"为什么",不解释"是什么" - 错误处理要具体,不要笼统地catch所有错误 - 命名要具体,不要用"manager"、"helper"、"util"这种模糊词这些准则不是硬性规则,而是"倾向性建议"。AI在生成代码时会参考这些准则,做出更"有品味"的选择。
6.2 用示例教AI什么是"好代码"
规则是抽象的,示例是具体的。我在.ai/目录下加了一个examples/文件夹,放了一些"好代码"和"坏代码"的对比:
# 好代码 vs 坏代码 ## 坏代码 ```javascript function process(data) { let result = [] for (let i = 0; i < data.length; i++) { if (data[i].status === 1) { result.push(data[i]) } } return result }好代码
function filterActiveUsers(users) { return users.filter(user => user.status === UserStatus.Active) }为什么好
- 函数名体现业务语义
- 使用数组方法而非for循环
- 使用枚举而非魔法数字
- 代码更短但信息量更大
这种对比示例比规则更直观。AI看了之后,会模仿"好代码"的风格,避免"坏代码"的写法。 ### 6.3 持续迭代:让SKILL.md活起来 `SKILL.md`不是写完就完了,它需要持续迭代。我的做法是: - **每周**:Review一次AI生成的代码,把新发现的问题加到`checklist.md` - **每月**:更新一次`patterns.md`,把新沉淀的模式加进去 - **每季度**:大版本更新`SKILL.md`,调整规则优先级,删除过时规则 迭代的时候有个原则:**只加规则,不删规则,除非规则被证明是错的**。因为删规则会让AI"忘记"之前的约束,导致风格回退。如果某条规则不再适用,改成"建议"而不是直接删掉。 > 实操心得:我在`SKILL.md`里加了一个"版本历史"段落,记录每次修改的内容和原因。这样团队新成员能看到规范的演进过程,理解每条规则背后的故事。 ### 6.4 跨项目复用:打造个人技能库 如果你同时维护多个项目,可以把`SKILL.md`拆成"通用部分"和"项目部分":~/.ai-skills/ ├── base.md # 通用规范(命名、品味、检查清单) ├── vue.md # Vue项目专用规范 ├── react.md # React项目专用规范 └── node.md # Node项目专用规范
project-root/ └── SKILL.md # 项目特有规范,引用通用部分
项目里的`SKILL.md`只需要写项目特有的内容,通用部分用`@import`引用。这样维护成本大大降低,而且跨项目的一致性更好。 我自己的`~/.ai-skills/`目录已经积累了大概2000行的规范,覆盖了Vue、React、Node、Python等多个技术栈。每次开新项目,只需要写100行左右的项目特有规范,剩下的直接复用。 ## 7. 一些踩过的坑和真实体会 ### 7.1 不要追求100%的规则覆盖 我一开始想把所有规则都写进`SKILL.md`,结果写了800多行,AI反而抓不住重点。后来发现,**规则覆盖80%的场景就够了,剩下20%靠AI的判断力**。规则太多会限制AI的灵活性,导致生成的代码"死板"。 现在的做法是:核心规则(命名、结构、样式)必须写,边缘规则(注释风格、文件组织)写成"建议"。AI在核心规则上严格遵守,在边缘规则上灵活处理。 ### 7.2 定期清理过时规则 项目在演进,规范也在演进。半年前定的规则,现在可能已经不适用了。比如我们之前用Vuex,后来换成了Pinia,`SKILL.md`里关于Vuex的规则就过时了。如果不清理,AI会按照过时的规则生成代码,反而制造问题。 我现在的做法是每季度做一次"规范审计",把过时的规则删掉,把新的规则加上。审计的时候会问三个问题:这条规则还有用吗?这条规则和实际代码一致吗?这条规则AI能理解吗?三个问题有一个答"否",就考虑修改或删除。 ### 7.3 AI不是万能的,人还是要兜底 最后说句实话:`taste-skill`和`Agent Skills`能大幅提升AI生成代码的质量,但不能完全替代人的判断。AI生成的代码还是需要Review,还是需要测试,还是需要根据实际业务调整。 我的体会是:**AI负责"写得规范",人负责"写得对"**。规范的部分交给`SKILL.md`,业务逻辑的部分还是得人来把关。两者结合,才能既有效率又有质量。 这套工作流我用了两个月,最大的感受是:AI生成的代码终于"像人写的"了。不是因为它变得更聪明,而是因为它终于知道了"这个项目的代码应该长什么样"。`SKILL.md`就像给AI戴上了一副"项目眼镜",让它看到的不是抽象的"前端代码",而是具体的"我们项目的代码"。 如果你也在为「AI味」头疼,建议从写一个简单的`SKILL.md`开始。不用追求完美,先写20行核心规则,用起来,再慢慢迭代。这个过程本身就是对项目规范的一次梳理,哪怕AI不用,对人也是有好处的。