用 AI 构建代码重构工具链:初步实践中的认识与方法论沉淀

1 阅读17分钟

用 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 实现、程序员把控边界的批量重构工具链:

  1. AI 分析代码模式,设计并编写 AST 转换脚本
  2. 工具链批量无人值守执行转换
  3. AI + 人工协作解读 warnings,处理边界情况
  4. 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 做单文件辅助转换仅在人工确认触发,不自动调用