用 AI 构建代码重构工具链:实践中的思考与方法论沉淀
以 Vue3
defineComponent→<script setup>批量迁移为载体,记录在 AI 全程参与构建专属代码重构工具链的过程中,程序员视角下的设计决策、AI 协作边界划定,以及反复踩坑后提炼的工程方法论。
零、核心命题:AI 写工具链,程序员管边界
这次实践的特殊性在于:代码重构工具链本身,是完全由 AI 实现的。
不是"AI 辅助写几个工具函数",而是从需求分析、技术选型、脚本架构设计、核心转换逻辑、错误处理、质检集成,全程由 AI 编写。
这带来了一个新的工程问题:当 AI 是实现者时,程序员的职责是什么?
答案不是"审查每一行代码",而是:
程序员的职责
├── 划定 AI 的工作边界(做什么 / 不做什么)
├── 提供真实反例驱动迭代(发现 AI 考虑不全的情况)
├── 设计守卫和约束(告诉 AI 如何保护自己不做错)
└── 把控工具链的整体质量标准(什么叫"可以批量运行")
核心原则:AI 负责实现,程序员负责定义"正确"的边界。
| 程序员工作 | 具体行为 |
|---|---|
| 提供反例 | 找到 AI 没考虑到的边界情况,用真实文件证明 |
| 设计守卫 | 将业务约束翻译成精确的代码判断条件(充要条件) |
| 划定不做的边界 | 明确"跳过"比"强行处理"更安全的场景 |
| 定义质量标准 | 什么叫"可以批量运行",什么叫"结果可信" |
| 提出系统性需求 | 分层设计、质检闭环、异常可观测性、安全机制 |
一、背景与目标
场景与痛点
项目中大量 Vue3 组件仍采用 defineComponent + setup() 函数式写法,需要批量迁移为 <script setup> 语法糖。
代码层面的痛点:
- 代码冗余:每个组件都需要手写
return {}导出所有响应式变量 - 模板引用不直观:
setup()内部逻辑和模板绑定通过return解耦,阅读链路长 - 手动迁移风险大:逐文件改动,容易遗漏、引入 bug
规模层面的瓶颈:几十上百个文件,纯人工迁移成本高、一致性差;直接让 AI 逐文件改写则 token 消耗巨大、速度极慢,难以工程化落地。
为什么构建 AST 工具链,而不是直接让 AI 改写
这是本次实践的核心技术判断。
当批量代码变换任务满足"规则可枚举 + 文件量大 + 对正确性要求高"时,应优先构建专属工具链,而非直接让 AI 逐文件改写。本次迁移任务恰好满足这三个条件:转换规则明确、源/目标语法结构固定、文件数量多。
| 维度 | AI 直接改写 | AST 脚本 + 人工/AI 兜底 |
|---|---|---|
| 速度 | 每文件一次 LLM 调用,几十个文件耗时几十分钟 | 脚本批量执行,百个文件几分钟完成,快几个量级 |
| Token 消耗 | 每个文件完整上下文输入+输出,随文件数线性暴涨 | 脚本处理零 token,仅对边界情况精准调用 AI |
| 一致性 | LLM 输出不完全确定,相同模式可能产生不同写法 | 规则固化在脚本中,输出 100% 一致,可 diff、可回滚 |
| 可审计性 | 改了什么不透明,难以批量 diff | --dry-run 预览,Git diff 清晰可见 |
| 错误处理 | 失败静默或输出错误代码,难以批量发现 | 脚本输出结构化 warning,问题集中可见,便于决策 |
方案核心:AST 脚本处理确定性逻辑,边界情况交给人工决策 + AI 兜底
确定性转换(95%+ 文件) → AST 脚本批量执行,零 token 消耗
↓
脚本输出 warnings → 人工审阅,判断是否需要干预
↓
边界/复杂模式(少量) → AI 辅助单文件处理,精准消耗 token
这个分层模型的价值在于:将 AI 的能力集中用在真正需要语义理解的少数边界场景,而不是把 token 平摊到所有机械性重复工作上。
目标
构建一套由 AI 实现、程序员把控边界的批量重构工具链:
- AI 分析代码模式,设计并编写 AST 转换脚本
- 工具链批量无人值守执行转换
- AI + 人工协作解读 warnings,处理边界情况
- linter 集成质检,闭环验证
二、技术架构
核心工具链
@vue/compiler-sfc → 解析 .vue 文件,提取 <script> 块;compileTemplate 编译模板 AST
@babel/parser → 将 JS/TS 代码解析为 AST
@babel/traverse → 遍历 AST 节点,做模式匹配和替换
@babel/generator → 将修改后的 AST 重新生成代码
@babel/types → AST 节点类型判断与构造工具
脚本架构(setup2sugar.mjs)
transformFile()
├── parseSfc() # 解析 .vue 文件
├── [转换模式] transformScript()
│ ├── 提取 defineComponent 选项
│ ├── 生成 defineProps / defineEmits
│ ├── 检测 return 非标准导出(警告)
│ ├── 移除 return 语句
│ ├── normalizeLetToConst() # E4
│ ├── flattenStateReactive() # E6
│ └── extractReactiveFactory() # E7
├── checkTemplateRefs() # 模板引用 vs script 声明交叉检测
└── [优化模式] optimizeSetupScript()
└── extractReactiveFactory() # E7(对已有 script setup 文件)
命令行参数
node setup2sugar.mjs <file.vue> # 转换单个文件
node setup2sugar.mjs <dir> # 批量转换目录
node setup2sugar.mjs <file.vue> --dry-run # 预览,不写入
node setup2sugar.mjs <file.vue> --verify # 转换后 eslint+prettier 质检
node setup2sugar.mjs <file.vue> --verify --fix # 质检并自动修复
双模式架构
脚本自动判断文件类型,可安全地对整个目录批量运行:
| 模式 | 触发条件 | 执行内容 |
|---|---|---|
| 转换模式 | 含 <script> + defineComponent + setup() | 完整转换:E1/E4/E6/E7 + import 清理 + 模板引用检测 |
| 优化模式 | 已是 <script setup> | 仅执行 E7 等后处理优化 + 模板引用检测 |
| 跳过 | 两者都不符合 | 不做任何修改 |
三、AI 构建工具链过程中的典型问题
记录的不是"AI 写错了代码",而是"AI 在边界考虑上的系统性盲点",以及程序员如何识别、干预、收敛这些问题。这 9 类盲点在第五章汇总为可复用的应对规律。
3.1 AI 的意图识别偏差:理解了目标,但遗漏了约束
现象:AI 正确理解了"把 reactive({...}) 拆分为独立变量"的意图,编写了完整的 E6 转换逻辑,能跑通大多数文件。
问题暴露:在处理含 ...props.parentOptions 展开运算符的 reactive 对象时,AI 将其中的数组字段错误地拆成独立 ref([]),产生大量未使用变量,ESLint 报 no-unused-vars。
根因分析:AI 理解了"拆分"这个意图,但没有主动思考"什么情况下不应该拆分"——即守卫条件(guard)。SpreadElement 的存在意味着对象结构在运行时才确定,静态拆分是不安全的,这是一个需要领域知识才能识别的约束。
程序员的介入:
- 提供了含
SpreadElement的真实文件作为反例 - 明确告知 AI:含扩展运算符的 reactive 对象,结构不确定,应跳过,不强行拆分
- AI 随即加入前置守卫:
const hasSpread = objExpr.properties.some(p => t.isSpreadElement(p))
if (hasSpread) continue
规律提炼:AI 倾向于实现"正向路径",对"负向约束"(什么时候不该做)的覆盖往往不足。程序员需要主动提供反例驱动,而不是等 AI 自己发现边界。
3.2 AI 的过度推断:把"相似"当"相同"
现象:E7 优化识别哪些 Object.assign(form, {...}) 是"重置表单"操作,AI 初版用 key 重合度来判断。
问题暴露:Object.assign(form, { title: res.data.title }) 是从接口回填表单的业务赋值,key 高度重合,被错误替换成了 initFormData(),直接破坏业务逻辑。
根因分析:AI 用了"相关但不充分"的特征(key 重合度)做判断,没有区分"用字面量重置"和"用变量赋值"这两种语义截然不同的操作——典型的过度推断。
程序员的介入:设计守卫——重置操作的充要条件是"所有属性值都是纯字面量":
function isResetObject(objExpr) {
return objExpr.properties.every(p => isResetValue(p.value))
}
规律提炼:当 AI 用"间接特征"做判断时往往产生误判。程序员需要追问:这个特征是充分条件还是必要条件? 然后帮 AI 找到更精确的判断依据。
3.3 AI 的静默失败:能跑通,但悄悄丢失了数据
现象:脚本无条件移除 setup() 的 return 语句。当 return 里包含内联计算表达式时,这个值在 setup body 里没有声明,删除后消失,模板运行时报 undefined,但脚本没有任何警告输出。
根因分析:AI 实现了"移除 return"这个功能,但没有考虑"移除 return 可能产生数据丢失"的副作用——静默失败:脚本执行成功,结果是错的,且没有任何提示。
程序员的介入:明确要求"任何有损操作都必须有显式警告",AI 加入移除前的扫描,对 return 中非简单 Identifier 的属性值输出 warning。
规律提炼:有损操作必须有显式警告,这是工具链的基本可信度要求。程序员需要将"不允许静默失败"作为显式约束注入到每个有损操作的设计中。
3.4 AI 的执行时序盲点:逻辑正确但顺序错误
现象:checkTemplateRefs() 功能本身逻辑正确,但警告始终没有输出。调试后发现:console.warn(warnings) 的打印在第 270 行,checkTemplateRefs() 的调用在第 278 行——检测在打印之后执行,警告进了数组但已错过打印时机。
根因分析:AI 将"产生警告"和"打印警告"视为两个独立模块,没有意识到执行时序依赖——逻辑正确但组合顺序错误,且没有报错,极难发现。
程序员的介入:通过 --dry-run 测试"预期有警告但没有输出"的场景发现问题,要求 AI 将检测调用移到打印之前。
规律提炼:AI 对执行时序的全局感知较弱。程序员需要在验收时显式测试"预期应该有输出但没有"的场景,而不只是测试正常路径。
3.5 AI 缺乏分层设计意识:能跑就行,不考虑职责边界
现象:AI 交付的第一版脚本是单体函数——解析、转换、优化、写文件全部写在一起,没有任何模块划分。导致无法单独测试某一步骤,新增优化项风险高,出错时难以定位在哪一层。
根因分析:AI 的目标是"让当前请求跑通",不会主动思考"这个工具以后还要迭代"——分层设计是面向未来的,而 AI 默认面向当下。
程序员的介入:要求明确分层(解析/转换/优化/IO/CLI);每个优化项独立函数;双模式架构也是程序员主动提出的,AI 初版没有这个概念。
规律提炼:分层、模块化、单一职责��些设计原则,要作为显式约束告诉 AI,不能期待 AI 自己提出。
3.6 AI 缺乏质检闭环意识:完成转换就算完成
现象:AI 初版工具链执行完转换就退出,没有任何验证步骤。转换后文件可能有 ESLint 错误、格式混乱、模板引用缺失,需要人工逐一排查。
根因分析:AI 将"转换任务"和"验证任务"视为两件独立的事——质检闭环是全链路视角,AI 缺乏这种系统性思维。
程序员的介入:主动提出质检需求,设计 --verify / --fix 参数集成 linter;提出 checkTemplateRefs 作为完整性校验。
规律提炼:"功能能跑"≠"结果可信"。程序员需要将"如何验证结果正确"作为设计需求显式提出。
3.7 AI 的高风险操作不告知:直接改文件,不提示风险
现象:AI 实现的脚本默认直接写入原文件(fs.writeFileSync),没有任何风险提示,没有备份机制,没有确认步骤。批量模式下一条命令可能同时覆盖几十个文件。
根因分析:AI 不会主动站在更高层面问"这个操作对用户来说安全吗"——AI 对逻辑正确性的感知远强于对操作风险性的感知。
程序员的介入:要求加入 --dry-run 模式;在文档和 SKILL 中标注"Git 先行"为前置步骤;批量运行强调先单文件验证。
规律提炼:凡是涉及不可逆操作,程序员必须主动要求加入安全机制(dry-run、备份、确认步骤),不能期待 AI 自己识别并处理风险。
3.8 AI 缺乏异常处理设计意识:能跑通就不考虑失败路径
现象:AI 初版脚本错误处理极为简陋——Babel 解析失败只输出 e.message,无行列号、无代码上下文。更严重的是没有全局异常兜底,未捕获异常直接导致 Node 进程崩溃,批量任务中断。
根因分析:AI 的重点是"正常路径能跑通",不会主动思考"批量运行时需要什么级别的可观测性"——异常处理设计是系统化思维,不是功能实现。
程序员的介入:提出 formatError(err, sourceCode) 设计要求(行列号 + 代码上下文 + 堆栈);要求注册全局异常兜底;要求错误处理分层(全局 → 文件级 → 转换级 → 节点级警告)。
规律提炼:"功能能跑"≠"出错时能定位"。可观测性需要程序员作为显式需求提出,AI 不会主动考虑。
3.9 AI 的算法偏差:正确的算法,错误的分母
现象:E7 用 key 重合度判断是否为重置操作,初版公式 ratio = overlap / Math.max(initKeys, assignKeys)。当开发者写重置逻辑时漏掉部分字段,Math.max 取较大值作分母,ratio 偏低,E7 不触发。
根因分析:AI 用了"对称比较"的分母,但业务语义是"assign 的字段是 init 的子集"——应该用 assignKeys 作分母,衡量"assign 有多完整地覆盖了 init 的 key"。AI 倾向于使用通用算法,忽略业务的非对称性。
程序员的介入:解释业务语义,改为子集比例:ratio = overlap / assignKeys.size。
规律提炼:程序员需要将业务语义翻译成精确的数学约束,而不是让 AI 自己选择算法。
四、完整链路中的技术问题与解法
4.1 AST 遍历副作用
@babel/traverse 会在节点上写入 _scope、_paths 等内部元数据,多次遍历时引发 TDZ 错误。
解法:纯检测场景改用自定义递归 walker,不触碰 traverse;只有需要"替换节点"时才使用 traverse。
4.2 数组引用共享导致的 splice bug
多处持有同一 topLevelNodes 引用,splice 操作在意外位置生效。
解法:构建临时 AST 时用展开运算符创建副本,遍历完成后同步回原数组:
const tempProgram = t.program([...topLevelNodes])
topLevelNodes.length = 0
topLevelNodes.push(...tempProgram.body)
4.3 质检工具与业务代码耦合
最初用关键词搜索做质检,换项目即失效。
解法:集成项目已有 linter 工具链,零业务耦合:npx eslint --fix + npx prettier --write。
4.4 模板引用与 script 声明的交叉检测
转换后 <template> 可能引用已被丢弃的变量,运行时报错但难以溯源。
解法:checkTemplateRefs() 交叉检测——解析 <script setup> 收集顶层声明,编译 <template> 收集 Identifier 引用,交叉比对后输出缺失警告。
五、工程化方法论总结
5.1 AI 边界盲点系统认知
9 类盲点的完整汇总(各盲点的详细案例见第三章):
| 盲点类型 | 表现 | 程序员应对 |
|---|---|---|
| 守卫缺失 | 实现正向路径,忘了什么情况不该做 | 主动提供反例,要求补充守卫 |
| 过度推断 | 用间接特征做判断,把相似当相同 | 追问充要条件,提供精确判断依据 |
| 静默失败 | 有损操作无警告,跑通但结果错误 | 将"有损操作必须有警告"作为显式约束 |
| 时序盲点 | 逻辑正确但执行顺序错,功能失效 | 测试"预期有输出但没有"的场景 |
| 缺乏分层 | 单体实现能跑,不考虑职责边界和可扩展性 | 将分层、模块化作为显式设计约束注入 |
| 质检盲区 | 完成转换即认为任务结束,不考虑结果验证 | 将"如何验证结果正确"作为设计需求显式提出 |
| 风险不感知 | 直接写文件/覆盖数据,不告知操作风险 | 凡涉及不可逆操作,主动要求加入 dry-run、备份机制 |
| 异常处理缺失 | 只关注正常路径,错误信息模糊,进程可能崩溃 | 将可观测性作为显式需求:错误需可定位,失败需可继续 |
| 算法偏差 | 用通用算法,忽略业务的非对称性 | 将业务语义翻译成精确的数学约束 |
5.2 渐进式验证流程
AI 分析模式 + 设计转换规则
↓
AI 编写脚本初版
↓
程序员提供真实文件 → --dry-run 预览 ← AI 辅助解读输出
↓ 发现边界问题 → AI 修复 → 循环迭代
实际写入
↓
linter 质检 (--verify --fix)
↓
人工复查 warnings ← AI 辅助判断是否需要手动干预
5.3 工具链核心设计原则
选型判断:当批量代码变换任务满足"规则可枚举 + 需要批量 + 对正确性要求高"时,优先构建专属工具链,而非直接让 AI 逐文件改写。
安全边界:能不动就不动,能局部就不全局;对结构不确定的模式直接跳过;有损操作必须有显式警告。
质检闭环:转换完成不等于任务完成,必须内置结果验证(linter + 模板引用检测)。
批量安全:双模式架构(转换/优化/跳过)保证整目录批量运行不误伤;Git 先行保证失败可回滚。
工具复用:质检用 eslint + prettier,解析用 @vue/compiler-sfc,不引入额外依赖,不自造轮子。
六、沉淀成果
6.1 通用工具链工程化约束规则
本次实践的方法论已提炼为通用规则文件,适用于后续任何批量处理代码/文件的工程化工具链项目:
.aicode/rules/toolchain-engineering.md
涵盖 8 个约束域:角色分工、架构设计、安全操作、异常处理、质检闭环、守卫设计、迭代协作、文档规范。
使用方式:启动新工具链项目时,将该文件作为基础约束注入给 AI,可提前规避本次实践中踩过的系统性盲点。
6.2 专属工具链 Skill
本次实践同步沉淀了专属 Skill 文件,供后续在同一项目中继续使用转换工具链:
.aicode/skills/vue-setup-sugar/SKILL.md
Skill 的价值与意义:
CodeMod 工具链解决了"大规模机械性代码迁移"的人力瓶颈——基于 AST 的语义级转换,比人工更准确、比正则更安全,百个文件几分钟完成,且每次执行规则一致、可 diff、可回滚。
但工具链本身有一个隐性成本:其使用知识如果不显式沉淀,下次使用时仍需重新探索——参数含义、边界警告、操作规范全部藏在源码或对话历史里。Skill 文件解决的正是这个问题:将工具链的使用规范固化为 AI 可检索的结构化知识,确保 AI 每次调用工具链时都有明确的操作依据,而不是凭经验猜测。
工具链是执行能力,Skill 是使用知识。CodeMod + Skill = 可持续复用的工程化资产。
七、已知局限与后续方向
| 局限 | 说明 | 后续方向 |
|---|---|---|
✅ 已通过 checkTemplateRefs 实现模板引用 vs 脚本声明交叉检测 | vue-tsc --noEmit 可进一步补充类型维度检测 | |
| TypeScript 类型注解部分丢失 | 复杂泛型/类型断言在 AST 转换后可能简化 | vue-tsc --noEmit 补充类型检查 |
| 非标准 setup 返回 | return 中含计算表达式时已输出警告,需人工处理 | 可考虑自动提升为 computed() 声明 |
| 模板变量误报 | ESLint 无法感知模板中的变量引用,部分 warning 为误报 | 配合 eslint-plugin-vue 的模板感知规则 |
| AI 边界兜底 | 对工具链无法处理的复杂模式,可引入 AI 做单文件辅助转换 | 仅在人工确认触发,不自动调用 |