手把手教你用 AST+Babel+GPT-4 实现祖传代码自动重构工具

5 阅读9分钟

标签:前端工程化、人工智能、TypeScript、AST、代码重构

这几年,很多团队都在聊 AI 写代码,但真正落到老项目治理时,大家很快会发现一个很现实的问题:

AI 很会“新建”,却不太会“改祖传代码”。

新项目从 0 到 1,给 GPT 一段需求,它能生成一个八九不离十的页面;
但如果你扔给它一坨 10 万行的 jQuery、混着全局变量、DOM 直改、事件散落、注释不齐、命名混乱的老代码,它很快就会露出短板:

  • 改动范围不可控
  • 输出格式容易漂移
  • 注释和结构经常丢失
  • 稍复杂一点就开始幻觉
  • 看起来“能跑”,其实埋了一堆隐患

这也是为什么很多团队试过“直接让 AI 帮忙迁移老代码”之后,很快就失望了。
不是模型没价值,而是直接让 AI 生成最终代码,本身就是错位使用

真正靠谱的思路,不是让 AI 直接操刀改代码,而是让它做自己擅长的部分:理解、判断、规划
至于“精准动刀”,应该交给 AST。

所以这篇文章我想讲一个非常实用的方案:

AI 负责决策,AST 负责执行。

也就是:

  • 用 GPT-4 分析旧代码,输出结构化重构策略
  • 用 Babel AST 根据策略做精准、可控、可审计的代码修改
  • 最终生成迁移结果和变更报告,而不是一段来路不明的大模型自由发挥代码

如果你也在做下面这些事情,这套思路会非常适合:

  • jQuery 迁移到 React
  • Vue2 批量迁移到 Vue3
  • class component 迁移到 Hooks
  • 祖传接口调用统一接入新请求层
  • 大量重复逻辑抽离为自定义 Hook 或工具函数

这不是一个“AI 魔法重写器”,而是一套可工程化接入的自动重构方案


一、背景与痛点:为什么“纯 AI 重构”往往翻车

先说一个典型场景。

你接手一个 10 万行的后台系统,里面充满了这样的写法:

  • $('#id').text(...)
  • $('.btn').on('click', ...)
  • $.ajax({...})
  • 一堆匿名函数嵌套
  • 逻辑和 DOM 操作强耦合
  • 全局变量四处飞
  • 注释有,但不是每段都完整

现在产品还在继续迭代,业务不能停,你又必须逐步 React 化。

传统方案一般有两种。

方案一:纯人工迁移

优点很明显:稳。
缺点也同样明显:慢。

一个熟悉业务的工程师,面对一块中等复杂的 jQuery 模块,通常要做这些事:

  1. 先读懂原逻辑
  2. 分清楚状态、事件、DOM、副作用
  3. 再决定哪些转成 state,哪些放进 effect
  4. 最后再补上组件结构和生命周期清理

如果项目里这种模块有几十上百个,迁移周期很容易拉到按月计算。
很多团队最后不是“不会迁”,而是“没时间迁”。

方案二:直接让 AI 重写

这个方案刚试的时候通常很爽。

你把一段旧代码贴给模型,说“帮我改成 React Hooks”,它真的会吐出一份看起来还不错的结果。
但一旦代码稍微复杂,问题就开始出现了:

  • 某些变量生命周期判断错了
  • 原本的副作用清理没有处理
  • DOM 查询逻辑被生硬翻译成 useEffect
  • 注释消失
  • 命名被改乱
  • 边界行为被 silently 改掉

更麻烦的是,这种错误不是编译就能全部发现的。
它往往是“代码看着很像那么回事,但运行细节已经偏了”。

所以问题不在于 AI 不聪明,而在于你把它放到了一个它不擅长独立闭环的位置。

更合理的方案:AI 决策 + AST 执行

这就是本文的核心方案。

我们不要求 AI 直接输出 React 代码,而是要求它输出结构化重构指令,比如:

  • 哪些变量应该转成 useState
  • 哪些 DOM 副作用应该转进 useEffect
  • 哪些逻辑可以抽成自定义 Hook
  • 哪些地方有风险,需要人工 Review

然后由 AST 工具去做真正的代码修改。

这样做有三个明显好处:

1. 可控

AI 输出的是 JSON 指令,不是整段自由发挥代码。
你可以验证、过滤、修正这些指令。

2. 可审计

每一次改动都能落到明确节点上。
你能知道“改了什么”“为什么改”“谁建议的”。

3. 可工程化

一旦这套模式打通,就不只是改一个文件,而是可以批量跑在整个代码库上。

这时候,AI 不是写代码的“演员”,而是提供策略的“大脑”;
AST 则是执行具体动作的“手术刀”。


二、整体架构:把“生成代码”改成“生成指令”

整套流程其实可以概括为三阶段:

输入旧代码 → AI 分析并输出 JSON 策略 → AST 精准执行并生成结果

如果展开一点,大致是下面这个链路:

第一阶段:代码分析

先用 Babel Parser 把 jQuery 代码解析成 AST,然后通过遍历提取关键特征:

  • DOM 操作
  • 事件绑定
  • AJAX 调用
  • 定时器
  • 全局变量依赖
  • 可能的副作用位置

这一阶段不做修改,只做“摸底”。
它的目标是让后面的 AI 决策有结构化上下文,而不是只靠原始源码硬猜。

第二阶段:AI 决策

把分析结果而不是完整上下文一股脑扔给 GPT-4,让模型输出一个固定格式的 JSON,例如:

  • strategy:整体迁移策略
  • instructions:逐条改造指令
  • risks:潜在风险列表
  • manualReviewRequired:是否需要人工复核

这里最关键的设计点是:

让模型输出“指令”,不要输出“最终代码”。

因为一旦让模型直接写最终代码,约束就会迅速失控;
而输出结构化指令,意味着你可以把模型能力限定在“分析与规划”这个区间。

第三阶段:AST 执行

拿到 JSON 后,由 Babel Traverse + Types 完成精准修改:

  • 把变量声明重构为状态
  • 把某些 DOM 操作迁移为 effect
  • 把重复逻辑抽离为 hook
  • 把 AJAX 调用替换为 fetch 或请求层封装
  • 用 Generator 输出新代码,并尽量保留注释

最后再生成一份变更报告,告诉开发者:

  • 本次采用了什么迁移策略
  • 自动修改了哪些点
  • 哪些地方有风险
  • 哪些地方需要人工确认

这套链路的核心思想只有一句话:

把 AI 从“代码生成器”降级为“重构策略生成器”,整个系统反而更可靠。


三、核心实现:从分析器到执行器完整打通

下面直接看核心代码。

为了方便你复制到掘金,我这里用 JavaScript 演示,真实项目里建议用 TypeScript 封装类型。


3.1 代码分析器:提取 jQuery 时代的核心特征

先实现一个 JQueryAnalyzer,目标不是理解全部业务,而是先提取最值得迁移的几个信号:

  • $('selector') 这类 DOM 查询
  • .on('click', ...) 这类事件绑定
  • $.ajax(...) 这类网络请求
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;

class JQueryAnalyzer {
  constructor(sourceCode) {
    this.sourceCode = sourceCode;
    this.features = {
      domOperations: [],
      eventHandlers: [],
      ajaxCalls: [],
      timers: [],
      globalRefs: []
    };
  }

  parse() {
    return parser.parse(this.sourceCode, {
      sourceType: 'module',
      plugins: ['jsx']
    });
  }

  analyze() {
    const ast = this.parse();

    traverse(ast, {
      CallExpression: (path) => {
        const callee = path.node.callee;

        // $('selector')
        if (
          callee.type === 'Identifier' &&
          callee.name === '$' &&
          path.node.arguments.length > 0
        ) {
          this.features.domOperations.push({
            type: 'jquery_selector',
            selector: this.getLiteralValue(path.node.arguments[0]),
            loc: path.node.loc
          });
        }

        // $('.btn').on('click', handler)
        if (
          callee.type === 'MemberExpression' &&
          callee.property.type === 'Identifier' &&
          callee.property.name === 'on'
        ) {
          const objectCode = this.sourceCode.slice(
            path.node.start,
            path.node.end
          );

          this.features.eventHandlers.push({
            eventName: this.getLiteralValue(path.node.arguments[0]),
            handlerType: path.node.arguments[1]?.type || 'unknown',
            raw: objectCode,
            loc: path.node.loc
          });
        }

        // $.ajax(...)
        if (
          callee.type === 'MemberExpression' &&
          callee.object.type === 'Identifier' &&
          callee.object.name === '$' &&
          callee.property.type === 'Identifier' &&
          callee.property.name === 'ajax'
        ) {
          this.features.ajaxCalls.push({
            raw: this.sourceCode.slice(path.node.start, path.node.end),
            loc: path.node.loc
          });
        }

        // setTimeout / setInterval
        if (
          callee.type === 'Identifier' &&
          ['setTimeout', 'setInterval'].includes(callee.name)
        ) {
          this.features.timers.push({
            type: callee.name,
            loc: path.node.loc
          });
        }
      },

      Identifier: (path) => {
        if (
          !path.scope.hasBinding(path.node.name) &&
          !['window', 'document', '$', 'console'].includes(path.node.name)
        ) {
          this.features.globalRefs.push({
            name: path.node.name,
            loc: path.node.loc
          });
        }
      }
    });

    return this.features;
  }

  getLiteralValue(node) {
    if (!node) return null;
    if (node.type === 'StringLiteral') return node.value;
    if (node.type === 'NumericLiteral') return node.value;
    return '[dynamic]';
  }
}

module.exports = { JQueryAnalyzer };

这段代码的意义,不是“完美理解代码”,而是把老代码里最关键的迁移信号抽出来。
你会发现,一旦有了这层结构化分析,AI 需要处理的信息复杂度会瞬间下降很多。

它不再面对一坨原始代码,而是面对“这个文件里有哪些选择器、哪些事件、哪些请求、哪些定时器、哪些全局依赖”。

这就是从“自然语言猜测”转向“结构化推理”的第一步。


3.2 AI 决策引擎:让 GPT 输出 JSON,而不是直接吐 React

接下来是最关键的一层:Prompt 设计。

这里的目标不是问模型“帮我改成 React”,而是要求它输出明确的重构计划。
建议至少包含下面几个字段:

  • strategy:整体迁移方向
  • instructions:逐条指令
  • risks:潜在风险
  • manualReviewRequired:是否需要人工复核

下面给一个可直接用的示例:

const OpenAI = require('openai');

class RefactorPlanner {
  constructor(apiKey) {
    this.client = new OpenAI({ apiKey });
  }

  buildPrompt(features, sourceCode) {
    return `
你是资深前端架构师,请根据下面的 jQuery 代码特征,输出 React 重构计划。

要求:
1. 只能输出 JSON,不要输出 markdown
2. 不要直接给最终 React 代码
3. strategy 必须是以下之一:
   - function_component
   - class_component
   - custom_hook
4. instructions 是数组,每项包含:
   - type
   - target
   - description
5. risks 是数组,列出潜在风险
6. manualReviewRequired 为布尔值

代码特征:
${JSON.stringify(features, null, 2)}

原始代码:
${sourceCode}

输出格式示例:
{
  "strategy": "function_component",
  "instructions": [
    {
      "type": "state",
      "target": "counter",
      "description": "将 counter 转为 useState"
    }
  ],
  "risks": [
    "可能存在未清理的事件监听"
  ],
  "manualReviewRequired": true
}
`;
  }

  async plan(features, sourceCode) {
    const prompt = this.buildPrompt(features, sourceCode);

    const response = await this.client.responses.create({
      model: 'gpt-4.1',
      temperature: 0.2,
      input: prompt
    });

    const text = response.output_text?.trim() || '{}';

    try {
      return JSON.parse(text);
    } catch (err) {
      throw new Error(`AI 输出不是合法 JSON: ${text}`);
    }
  }
}

module.exports = { RefactorPlanner };

这里有几个特别关键的点。

第一,不让模型直接生成代码

这是整个系统最重要的护栏。
模型一旦直接输出最终代码,你就很难保证格式、依赖、注释、副作用边界。

第二,输出结构化指令而不是自然语言建议

自然语言虽然好懂,但难以自动执行。
JSON 指令则天然适合进入下一阶段流水线。

第三,把温度压低

重构不是创作,越稳定越重要。
temperature 建议尽量低一点,0.2 左右比较合适。

模型在这里不是来“发挥想象力”的,而是来“做保守判断”的。


3.3 AST 执行引擎:把策略变成真正的代码修改

有了重构策略后,接下来就轮到 AST 发挥作用了。

这里我们实现一个 RefactorExecutor,做三类典型转换:

  • 普通变量 → useState
  • DOM 副作用 → useEffect
  • 重复逻辑 → 自定义 Hook

为了演示重点,我这里先写一个精简但可扩展的版本。

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generate = require('@babel/generator').default;

class RefactorExecutor {
  constructor(sourceCode, plan) {
    this.sourceCode = sourceCode;
    this.plan = plan;
    this.ast = parser.parse(sourceCode, {
      sourceType: 'module',
      plugins: ['jsx']
    });
    this.usedHooks = new Set();
    this.report = [];
  }

  execute() {
    for (const instruction of this.plan.instructions || []) {
      switch (instruction.type) {
        case 'state':
          this.transformToUseState(instruction);
          break;
        case 'effect':
          this.transformToUseEffect(instruction);
          break;
        case 'custom_hook':
          this.extractToCustomHook(instruction);
          break;
        default:
          this.report.push(`跳过未知指令类型: ${instruction.type}`);
      }
    }

    this.injectReactHooksImport();

    const output = generate(this.ast, {
      retainLines: true,
      comments: true
    });

    return {
      code: output.code,
      report: this.report
    };
  }

  transformToUseState(instruction) {
    const target = instruction.target;

    traverse(this.ast, {
      VariableDeclarator: (path) => {
        if (
          path.node.id.type === 'Identifier' &&
          path.node.id.name === target &&
          path.node.init
        ) {
          const initValue = path.node.init;
          const setterName =
            'set' + target.charAt(0).toUpperCase() + target.slice(1);

          path.parentPath.replaceWith(
            t.variableDeclaration('const', [
              t.variableDeclarator(
                t.arrayPattern([
                  t.identifier(target),
                  t.identifier(setterName)
                ]),
                t.callExpression(t.identifier('useState'), [initValue])
              )
            ])
          );

          this.usedHooks.add('useState');
          this.report.push(`已将变量 ${target} 转为 useState`);
        }
      }
    });
  }

  transformToUseEffect(instruction) {
    const description = instruction.description || '迁移副作用逻辑';

    const effectBody = t.blockStatement([
      t.expressionStatement(
        t.stringLiteral(`TODO: ${description}`)
      ),
      t.returnStatement(
        t.arrowFunctionExpression([], t.blockStatement([]))
      )
    ]);

    const effectStatement = t.expressionStatement(
      t.callExpression(t.identifier('useEffect'), [
        t.arrowFunctionExpression([], effectBody),
        t.arrayExpression([])
      ])
    );

    this.ast.program.body.push(effectStatement);
    this.usedHooks.add('useEffect');
    this.report.push(`已插入 useEffect 模板: ${description}`);
  }

  extractToCustomHook(instruction) {
    const hookName = instruction.target || 'useLegacyLogic';

    const hookFn = t.functionDeclaration(
      t.identifier(hookName),
      [],
      t.blockStatement([
        t.expressionStatement(
          t.stringLiteral('TODO: 提取重复逻辑到自定义 Hook')
        ),
        t.returnStatement(t.objectExpression([]))
      ])
    );

    this.ast.program.body.unshift(hookFn);
    this.report.push(`已生成自定义 Hook 模板: ${hookName}`);
  }

  injectReactHooksImport() {
    if (this.usedHooks.size === 0) return;

    const hookIdentifiers = Array.from(this.usedHooks).map((name) =>
      t.importSpecifier(t.identifier(name), t.identifier(name))
    );

    const importDecl = t.importDeclaration(
      hookIdentifiers,
      t.stringLiteral('react')
    );

    this.ast.program.body.unshift(importDecl);
    this.report.push(`已注入 React Hooks 导入: ${Array.from(this.usedHooks).join(', ')}`);
  }
}

module.exports = { RefactorExecutor };

这段代码当然还不是一个生产级迁移器,但它已经把最核心的骨架搭起来了:

  • AI 只管给策略
  • AST 负责真正改代码
  • 所有修改都有报告
  • 输出尽量保留注释和结构

真实项目里,你可以继续把执行器扩成更多规则,比如:

  • $.ajaxrequest.get/post
  • .on() → 组件级事件回调
  • DOM 查询 → useRef
  • 定时器 → useEffect + cleanup
  • 重复逻辑 → 自定义 Hook 文件输出

一旦这些规则沉淀下来,后面就可以复用到大量旧模块。


3.4 完整调用流程:分析、规划、执行、报告四步闭环

最后我们把整条链路串起来,形成一个入口函数 refactorLegacyCode

const fs = require('fs');
const path = require('path');
const { JQueryAnalyzer } = require('./JQueryAnalyzer');
const { RefactorPlanner } = require('./RefactorPlanner');
const { RefactorExecutor } = require('./RefactorExecutor');

async function refactorLegacyCode(filePath, apiKey) {
  const sourceCode = fs.readFileSync(filePath, 'utf-8');

  console.log('Step 1: 分析旧代码特征...');
  const analyzer = new JQueryAnalyzer(sourceCode);
  const features = analyzer.analyze();

  console.log('Step 2: 生成 AI 重构策略...');
  const planner = new RefactorPlanner(apiKey);
  const plan = await planner.plan(features, sourceCode);

  console.log('AI Strategy:', JSON.stringify(plan, null, 2));

  console.log('Step 3: 执行 AST 重构...');
  const executor = new RefactorExecutor(sourceCode, plan);
  const result = executor.execute();

  const outputFilePath = filePath.replace(/.js$/, '.refactored.js');
  fs.writeFileSync(outputFilePath, result.code, 'utf-8');

  console.log('Step 4: 输出变更报告...');
  const reportFilePath = filePath.replace(/.js$/, '.report.json');
  fs.writeFileSync(
    reportFilePath,
    JSON.stringify(
      {
        strategy: plan.strategy,
        risks: plan.risks || [],
        manualReviewRequired: plan.manualReviewRequired,
        changes: result.report
      },
      null,
      2
    ),
    'utf-8'
  );

  return {
    outputFilePath,
    reportFilePath,
    plan,
    changes: result.report
  };
}

// demo
(async () => {
  try {
    const result = await refactorLegacyCode(
      path.resolve(__dirname, './legacy.js'),
      process.env.OPENAI_API_KEY
    );

    console.log('重构完成:', result);
  } catch (error) {
    console.error('重构失败:', error);
  }
})();

这时候,整个系统就已经形成了清晰闭环:

  1. 分析器负责提取结构化信号
  2. AI 负责做迁移策略判断
  3. AST 负责执行具体改动
  4. 报告模块负责留痕与人工复核

这套模式最大的优点,是你可以逐步增强,而不是一开始就追求“大而全”。

比如第一版只处理:

  • 简单变量状态化
  • 简单事件迁移
  • 简单 AJAX 替换

先把准确率做出来,再逐渐扩展复杂规则。
工程上,永远是可控的小闭环比“大而全但不稳定”更值钱。


四、避坑指南:这类系统最容易翻车的 3 个地方

实际做下来,这种“AI + AST”的方案确实比纯 AI 稳很多,但也不是完全没坑。
下面这 3 个地方,是最容易出问题的。

坑 1:AI 幻觉导致变量名冲突

比如旧代码里已经有一个 setCount,而 AI 又建议把 count 转成 useState,执行器再机械生成 setCount,就会直接撞名。

这个问题的解决办法不是“相信模型会注意”,而是在 AST 层做 Scope 分析

也就是说,在真正插入标识符之前,先通过 Babel Scope 判断当前作用域是否已有同名绑定。如果有,就自动重命名,比如:

  • setCountsetCountState
  • datadataState
  • 或者根据作用域生成唯一后缀

结论很简单:
命名安全,不能交给模型兜底,必须交给编译器能力兜底。

坑 2:TypeScript 类型信息容易丢

如果你的旧项目本身带 TypeScript,或者迁移目标是 TS React,那么另一个常见问题就是:
AI 很容易给出“逻辑上差不多对,但类型并不完整”的策略。

比如:

  • 原始对象缺少接口声明
  • state 初始化导致推导成错误联合类型
  • DOM 引用缺少泛型
  • 异步请求返回值没有约束

比较稳的做法有两个:

第一,在 Prompt 中显式要求模型标注类型迁移风险。
第二,在 Parser 和 Generator 层启用 TS 插件,并在执行阶段补类型模板。

也就是说,不要把 TS 看成“重构完成后再慢慢补”。
在真实工程里,类型本身就是迁移质量的一部分。

坑 3:副作用清理最容易遗漏

这是最危险也最隐蔽的一类问题。

jQuery 时代很多逻辑是这样散落的:

  • 手动绑事件
  • 手动开定时器
  • 手动请求后再操作 DOM

迁到 React 后,如果只是把逻辑塞进 useEffect,但没有正确插入 cleanup,就很容易留下:

  • 重复绑定事件
  • 定时器泄漏
  • 页面切换后副作用残留
  • 内存泄漏或状态错乱

这一块建议别指望模型“自动想全”,而是直接在执行器里内建模板:

  • 发现事件绑定,就生成清理函数占位
  • 发现定时器,就自动插入 clearTimeout/clearInterval 模板
  • 发现外部订阅,就强制标记人工 Review

副作用问题,本质上不是“代码生成不优雅”,而是“运行时会出事故”。
这里一定要偏保守。


五、效果评估:这类工具到底值不值

最后聊点更实际的。

这套方案值不值,不取决于它能不能 100% 自动完成迁移,而取决于它能不能把最贵的那部分人工成本压下来。

在一个中等复杂的旧模块里,如果完全人工处理,比较常见的节奏是:

  • 读代码 + 拆逻辑:半天到一天
  • 手动改写:一天
  • 自测 + 修边角:半天到一天

算下来,一个模块 2 到 3 天很正常

而如果用“分析 + AI 策略 + AST 执行”的方式,比较现实的数据通常会变成:

  • 工具自动处理:约 30 分钟
  • 工程师 Review + 修边角:约 2 小时

当然,这并不意味着“一键完美迁移”。
更真实的说法是:

  • 大约 75% 的简单模块可以直接进入 Review
  • 剩下的 25% 需要人工重点介入

但对工程团队来说,这已经非常值钱了。
因为你节省掉的,不只是“敲代码时间”,更是“重复理解和机械改写时间”。

它最适合什么场景

最适合的是这些:

  • 简单 DOM 操作
  • 规则清晰的事件绑定
  • 常规 AJAX 请求
  • 重复结构很多的老模块
  • 迁移目标比较统一的项目

它不太适合什么场景

不太适合的是这些:

  • 复杂状态机
  • 强时序依赖逻辑
  • 大量运行时动态拼接行为
  • 严重依赖全局环境的代码
  • 业务语义极重、抽象很深的模块

所以别把它当成“万能重写器”。
它更适合做的是:

把 70 分的机械迁移工作自动完成,再把 30 分的关键判断留给资深工程师。

这才是现实世界里最有价值的分工。


六、结语

很多人一提到 AI 重构,就会下意识地问一句:

“能不能让大模型直接把老代码全改了?”

我的答案是:
理论上你可以试,工程上最好别这么干。

因为真正可落地的系统,不是让 AI 一把梭,而是让它在自己擅长的位置上发挥价值:

  • 它负责理解和规划
  • AST 负责精准和可控
  • 工程师负责判断边界和最终验收

所以这套方案真正的核心,不是“GPT 很强”,也不是“AST 很强”,而是你把两者放到了正确的位置:

AI 做大脑,AST 做手术刀。

这时候,祖传代码自动重构才不再是一个 Demo,而是一个能进流水线、能跑在仓库里、能服务团队的工程工具。