AI code generation leads to messy project components
🤖 AI写代码越跑越快,项目组件却越来越乱?一套工程闭环根治重复造轮子 ⚡
For developers, this is a practical guide to managing AI-generated code and preventing the creation of redundant components in their projects.
AI code generation tools produce new components for every request, leading to duplicated code and increased maintenance costs. The article proposes a solution to this problem by integrating AI with existing project components and design systems.
🤖 AI写代码越跑越快,项目组件却越来越乱?一套工程闭环根治重复造轮子 ⚡
前言 Hello~大家好,我是秋天的一阵风 AI 现在已经是前端开发的标配。让它写个 Vue 组件、TSX 页面,几秒钟就能出代码,迭代快得肉眼可见。 但快是有代价的。AI 只负责"快速产出",不关心你项目里已经有什么——设计系统、组件规范、存量组件,它一概不知。 最常见的画面:你让它写个提交按钮,它现编一个;隔壁同事让它写"保存并提交",又现编一个。 两个组件长得几乎一样,只是文件名和入参不同。等代码进了 PR,评审才发现:又重复了。 一次两次还好,日积月累,业务目录里堆满同质化组件。等哪天要全局换主题色、适配无障碍、统一交互逻辑,就得人肉全网排查,漏改错改是家常便饭,维护成本直接失控。 一、AI为什么总爱"从零造轮子" 把 AI 想象成一个能力很强、但完全不了解你项目的临时工。 你让它写个基础提交按钮,它按网上最常见的写法随手写一个,根本不知道你们项目里早就有封装好的 SubmitAction ,也不知道配色、圆角、间距都要用统一的 CSS 变量。 把这个问题捋一捋,真正的痛点其实就三个: AI 看不见存量 :写代码前不会主动去读组件目录、导出清单,项目里有什么可复用的,它一无所知; AI 默认新写 :没有存量参考时,它总是选"现场编一套"这条最省事的路,而不是先查查能不能复用; 事后补救太贵 :代码已经落盘、进了 diff,再回头做复用优化,AI 生成的成本白花了,还得搭上人工清理、重构、对齐规范的时间。 所以治本的路子不是调提示词、训 AI"乖一点",而是改造工程工具链: 写代码前让它知道项目有哪些组件,写的时候按项目真实 API 来,合入主干前过一遍复用校验。 二、关键词搜索根本防不住"换皮组件" 很多人第一反应是:用 IDE 全局搜索不就行了?Ctrl+Shift+F 搜个"Button",看看有没有现成的。 这个办法只能拦住"长得一模一样"的代码。真正麻烦的是那些换皮、异形、同源的隐性重复,关键词根本搜不出来。看几个典型场景: 漏检场景 核心原因 命名体系分裂 项目里新旧两套命名并存,老组件叫 FfBtn ,新组件统一叫 ActionTrigger ,关键词对不上,搜索完全落空 实现形态不同 同一个功能的组件,一个用 Vue 单文件写,一个用 render 函数工厂写,文本差异巨大,关键词检索识别不了同源 DOM 结构不同、职责一致 同样是可点击操作,一个用原生 button ,一个用带按钮角色的 div ,语义和结构都不同,但业务功能完全重合 所以靠谱的治理得两条腿走路: 确定性清单校验 :把组件导出表、Storybook 文档、样式变量规范整理好,在 AI 写代码前主动喂进它的上下文; 智能近似匹配 :用 AST 结构指纹、向量检索,在代码生成后和 CI 阶段精准揪出"换皮不换功能"的重复组件。 三、生成前:先让AI"看见"项目库存 这一步是整个方案的地基: 强制 AI 在调用写文件、改文件工具之前,先加载项目的可复用组件清单 。 不管钩子叫 PreToolUse 还是 before_submit ,核心都一样——前置拦截,把存量约束塞进去,从源头拦住盲目新建。 (一)自动扫描组件,生成库存清单 靠人工口头叮嘱"项目有哪些通用组件"完全不靠谱,也跟不上迭代。写个脚本自动扫组件目录,把名称、路径、props、用到的全局样式变量全解析出来,生成一份标准 JSON 库存,一劳永逸。 // scripts/inventory-components.ts import fg from 'fast-glob' import { parse } from '@vue/compiler-sfc' import fs from 'node:fs/promises' import path from 'node:path' export type ComponentCard = { name : string file : string exportName : string props : string[] tokensUsed : string[] } // 匹配CSS变量 const TOKEN_RE = /var(--([a-z0-9-]+))/gi // 构建项目组件库存清单 export async function buildInventory ( root = 'packages' ): Promise < ComponentCard []> { // 遍历目标目录下的组件文件,过滤无效文件 const files = await fg ([ '**/components/**/*.{vue,tsx,ts}' ], { cwd : root, absolute : true , ignore : [ '**/node_modules/**' , '**/dist/**' , '**/*.test.*' ], }) const cards : ComponentCard [] = [] for ( const file of files) { const source = await fs. readFile (file, 'utf8' ) const base = path. basename (file, path. extname (file)) // 提取当前组件使用的所有全局样式变量 const tokensUsed = [...source. matchAll ( TOKEN_RE )]. map ( ( m ) => `-- ${m[ 1 ]} ` ) // 解析Vue单文件组件 if (file. endsWith ( '.vue' )) { const { descriptor } = parse (source) // 提取组件props定义 const props = descriptor. scriptSetup ?. content . match ( /defineProps<\s*{([^}]+)}/ s)?.[ 1 ] ?. split ( '\n' ) . map ( ( l ) => l. trim (). split ( /[?:]/ )[ 0 ]) . filter ( Boolean ) ?? [] cards. push ({ name : base, file : path. relative (process. cwd (), file), exportName : base, props, tokensUsed : [... new Set (tokensUsed)], }) continue } // 解析TS/TSX组件导出 const exports = [...source. matchAll ( /export\s+(?:function|const)\s+([A-Z]\w+)/g )]. map ( ( m ) => m[ 1 ], ) for ( const exportName of exports ) { cards. push ({ name : exportName, file : path. relative (process. cwd (), file), exportName, props : [], tokensUsed : [... new Set (tokensUsed)], }) } } return cards } // 执行脚本,生成库存JSON文件 if ( import . meta . url === `file:// ${process.argv[ 1 ]} ` ) { const inventory = await buildInventory () await fs. writeFile ( 'component-inventory.json' , JSON . stringify (inventory, null , 2 )) console . log ( `组件库存扫描完成,共 ${inventory.length} 个可复用组件` ) } 生成出来的库存文件长这样,信息一次到位,直接喂给 AI: [ { "name" : "SubmitAction" , "file" : "packages/ui/src/components/SubmitAction.vue" , "exportName" : "SubmitAction" , "props" : [ "pending" , "tone" , "disabled" ], "tokensUsed" : [ "--color-action" , "--radius-control" ] } ] (二)写文件前注入复用约束 AI 每次执行写入、编辑、补丁操作前,自动在它的上下文里注入组件复用规范和库存清单,逼它优先用现成的,禁止顺手内联实现同类控件。 // agent-hooks/before-write.ts import inventory from '../component-inventory.json' export function beforeWriteTool ( toolName: string, input: { prompt?: string } ) { // 仅拦截文件写入、编辑、补丁操作 if (![ 'Write' , 'Edit' , 'ApplyPatch' ]. includes (toolName)) return input // 截取核心组件清单注入上下文 const digest = inventory . slice ( 0 , 40 ) . map ( ( c ) => `- ${c.exportName} @ ${c.file} | props: ${c.props.join( ', ' ) || '-' } | tokens: ${c.tokensUsed.join( ', ' ) || '-' } ` , ) . join ( '\n' ) // 落地强制约束规则 const guard = ` 【代码落盘强制约束】 1. 优先导入使用下列已有组件,禁止在业务页面内联实现同职责控件 2. 页面颜色、圆角、间距等样式,仅使用项目CSS变量,禁止手写固定色值、尺寸 3. 清单无适配组件时,需先说明组件缺口、提出新增建议,再进行代码修改 可复用组件清单(节选): ${digest} ` return { ...input, prompt : ` ${guard} \n\n ${input.prompt ?? '' } ` , } } 这套钩子解决的问题,其实还有更轻的做法: 把同样的约束直接写进项目的 AGENTS.md。 AGENTS.md 是给 AI 看的"项目说明书",AI 每次启动都会自动读一遍, Claude Code 、 Cursor 、 Copilot 这些工具都认它。 把 "优先复用已有组件、禁止内联实现同类控件" 写进它的编码规范模块,再把 component-inventory.json 的路径写进文档索引,AI 从一开始就知道项目里有什么、去哪儿找,不用等钩子临时注入。 Claude Code 自带的 Hooks 就是这类钩子的现成实现,可以直接用。 钩子负责"写之前强制注入",AGENTS.md 负责"启动时就知道",两层叠一起,效果更稳。 AGENTS.md 具体怎么编写、怎么使用,我在另一篇《AGENTS.md:你的AI代码助手,需要一份"项目说明书"》里拆过完整模板,从工具适配到进阶技巧都有,需要的话可以直接看: AGENTS.md:你的AI代码助手,需要一份"项目说明书" 。 (三)代码落盘后自动拦截重复 光有前置约束还不够。AI 写完代码后,再自动把新文件和库存组件做结构相似度比对,重合度高的直接报错阻断,让它老老实实回去复用。 // agent-hooks/after-write.ts import { jaccardAstSimilarity } from '../lib/ast-similarity' import inventory from '../component-inventory.json' export async function afterWriteTool ( createdFiles: string[] ) { // 遍历所有新增文件,逐一比对库存组件 for ( const file of createdFiles) { for ( const item of inventory) { const score = await jaccardAstSimilarity (file, item. file ) // 相似度阈值0.85,可根据团队规范调整 if (score >= 0.85 ) { throw new Error ( ` ${file} 与已有组件 ${item.file} 结构高度重合(相似度 ${score.toFixed( 2 )} ),请直接复用 ${item.exportName} 组件,禁止重复实现` , ) } } } } 前置约束定方向,后置校验卡结果,这套组合拳比人肉评审盯 PR 靠谱得多。 四、生成中:让AI查着"说明书"写代码 库存清单解决了"项目里有什么",解决不了"该怎么用"。 组件 API 一直在变,光靠清单,AI 很容易凭旧印象写出过期 props、错误用法,照样出 bug。 最省事的办法是搭一个最小组件知识库——MCP 工具,强制 AI 写 UI 前先查一下组件真实文档和最新 API,一切以查询结果为准。 组件知识库MCP服务 // mcp-component-kb/server.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { z } from 'zod' import inventory from '../component-inventory.json' import docs from './component-docs.json' const server = new McpServer ({ name : 'component-kb' , version : '0.1.0' }) // 注册组件查询工具 server. tool ( 'lookup_component' , '按组件名称、业务职责查询项目内组件的完整API和使用示例' , { query : z. string (). describe ( '查询关键词,例如「提交按钮」「危险操作确认」' ), }, async ({ query }) => { const q = query. toLowerCase () // 模糊匹配符合需求的组件 const hits = inventory. filter ( ( c ) => c. name . toLowerCase (). includes (q) || c. file . toLowerCase (). includes (q) || (docs[c. name ]?. summary ?? '' ). toLowerCase (). includes (q), ) // 返回组件完整信息(API+示例) const payload = hits. slice ( 0 , 5 ). map ( ( c ) => ({ ...c, api : docs[c. name ]?. api ?? null , example : docs[c. name ]?. example ?? null , })) return { content : [{ type : 'text' , text : JSON . stringify (payload, null , 2 ) }], } }, ) 再把调用逻辑写进团队编码规范(规范清单),强制 AI 遵守: 修改、新增界面组件前,必须先调用 lookup_component 查询存量组件; 组件的 props、事件名以查询返回的最新 API 为准,禁止自定义编造; 存量组件能满足需求就默认复用;确实要新增,先说清楚理由和差异化场景。 不用堆超长系统提示词,把"查询校验"变成固定动作,API 误用和组件瞎编自然就少了。 五、生成后:扫描兜底,揪出隐性重复 前后钩子拦得住显性问题,但改几个变量、调一点逻辑的隐性换皮组件,还是可能漏网。 这步用可重复执行的扫描脚本补位,专治结构重复、样式变量漂移、样式写死。 AST结构相似度查重 文本 diff 对"改名换皮、微调逻辑"完全无效。AST 比对不一样——它忽略变量名、注释这些无关信息,只比核心节点结构,同源重复一眼就能认出来。 // lib/ast-similarity.ts import { parse } from '@babel/parser' import traverse from '@babel/traverse' import fs from 'node:fs/promises' // 提取代码核心结构Token,忽略无关细节 function structuralTokens ( code: string ): Set <string> { const ast = parse (code, { sourceType : 'module' , plugins : [ 'jsx' , 'typescript' ] }) const tokens = new Set <string>() traverse (ast, { enter ( path ) { // 记录节点类型 tokens. add (path. node . type ) // 记录JSX标签名 if (path. isJSXIdentifier ()) tokens. add ( `jsx: ${path.node.name} ` ) // 记录自定义组件标识符 if (path. isIdentifier () && /^[A-Z]/ . test (path. node . name )) { tokens. add ( `id: ${path.node.name} ` ) } }, }) return tokens } // 计算两个文件的杰卡德结构相似度 export async function jaccardAstSimilarity ( aPath: string, bPath: string ): Promise <number> { const [a, b] = await Promise . all ([fs. readFile (aPath, 'utf8' ), fs. readFile (bPath, 'utf8' )]) const A = structuralTokens (a) const B = structuralTokens (b) // 计算交集、并集 let inter = 0 for ( const t of A) if (B. has (t)) inter++ const union = A. size + B. size - inter return union === 0 ? 0 : inter / union } 遇上结构微调、语义仍重复的复杂场景,可以再叠一层向量嵌入(Embedding),把去标识后的 AST 序列向量化比对,双保险。配一个 pnpm scan:near-dup 命令,一键扫全项目。 禁止业务代码写死色值 AI 特别喜欢随手写死十六进制色值、固定尺寸,组件一写死就脱离设计系统,全局迭代直接报废。用一条自定义 ESLint 规则把它摁住: // 自定义ESLint规则:禁止硬编码颜色值 module . exports = { meta : { type : 'problem' , docs : { description : '禁止业务代码写死十六进制色值,统一使用全局样式变量' } }, create ( context ) { // 匹配所有十六进制颜色值 const HEX = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/ return { // 校验普通字符串色值 Literal (node) { if ( typeof node. value === 'string' && HEX . test (node. value )) { context. report ({ node, message : `禁止写死颜色值 ${node.value} ,请使用项目CSS变量(例:var(--color-action))` , }) } }, // 校验模板字符串内色值 TemplateElement (node) { if ( HEX . test (node. value . raw )) { context. report ({ node, message : '模板字符串中禁止写死颜色值,请替换为全局样式变量' , }) } }, } }, } 效果对比: <!-- AI容易生成的不规范代码 --> < button style = "background:#2563EB" > 提交 </ button > <!-- 符合设计系统的规范代码 --> < SubmitAction tone = "primary" > 提交 </ SubmitAction > 这条扫描管道还能继续扩展:无效导入、废弃组件引用、只改展示名的复制组件,都能查。规范固化成工具,就不靠口头自觉了。 这里有个判断标准值得记住:像写死色值这种 linter 能自动抓的错,不用写进 AGENTS.md; 真正值得写进去的,是"复用组件"这种 linter 抓不住、只能靠 AST 扫描发现的硬约束。 AGENTS.md 只写 AI 自己推断不出来的规则,其他交给工具链。 六、CI门禁:PR合并前最后一道闸 本地钩子、本地扫描都能被人为跳过,但 CI 不行。代码要合入主干,就必须过复用校验。 # .github/workflows/reuse-gate.yml name: component-reuse-gate on: [pull_request] jobs : gate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - run: pnpm i # 重新扫描生成最新组件库存 - run: pnpm exec tsx scripts/inventory-components.ts # 执行组件重复相似度扫描 - run: pnpm exec tsx scripts/scan-near-duplicates.ts --threshold 0.85 # 校验样式变量规范,禁止硬编码样式 - run: pnpm lint:design-tokens 只要扫出高相似度重复组件或违规硬编码样式,CI 直接失败、阻断合并。冗余代码入库这条路,从工程上就堵死了。 总结 让 AI 写代码很容易,难的是让它贴着你项目的规范、设计系统和存量能力来写。 组件库存让 AI 知道"项目里有什么",AGENTS.md 让它启动就懂规矩,MCP 知识库让它搞懂"该怎么用",扫描校验和 CI 门禁守住"不能乱改"的底线,闭环迭代让这套体系越用越聪明。 把这套链路跑通之后,代码评审不用再人肉查重、人肉纠错,AI 才真正从"运维负担"变成"提效工具"。
This report is categorized under AITOP's "How-to" desk, focusing on China's AI industry and domestic foundation model ecosystem. Within our cross-regional analytical framework, it directly maps to AI developer toolchains, coding agents, and engineering workflows. Key entities and technical topics under continuous tracking include AI code generation, frontend development, component management, prompt engineering.
Source attribution: curated from 掘金本周最热 (@秋天的一阵风).