标签:前端工程化、人工智能、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 模块,通常要做这些事:
- 先读懂原逻辑
- 分清楚状态、事件、DOM、副作用
- 再决定哪些转成 state,哪些放进 effect
- 最后再补上组件结构和生命周期清理
如果项目里这种模块有几十上百个,迁移周期很容易拉到按月计算。
很多团队最后不是“不会迁”,而是“没时间迁”。
方案二:直接让 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 负责真正改代码
- 所有修改都有报告
- 输出尽量保留注释和结构
真实项目里,你可以继续把执行器扩成更多规则,比如:
$.ajax→request.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);
}
})();
这时候,整个系统就已经形成了清晰闭环:
- 分析器负责提取结构化信号
- AI 负责做迁移策略判断
- AST 负责执行具体改动
- 报告模块负责留痕与人工复核
这套模式最大的优点,是你可以逐步增强,而不是一开始就追求“大而全”。
比如第一版只处理:
- 简单变量状态化
- 简单事件迁移
- 简单 AJAX 替换
先把准确率做出来,再逐渐扩展复杂规则。
工程上,永远是可控的小闭环比“大而全但不稳定”更值钱。
四、避坑指南:这类系统最容易翻车的 3 个地方
实际做下来,这种“AI + AST”的方案确实比纯 AI 稳很多,但也不是完全没坑。
下面这 3 个地方,是最容易出问题的。
坑 1:AI 幻觉导致变量名冲突
比如旧代码里已经有一个 setCount,而 AI 又建议把 count 转成 useState,执行器再机械生成 setCount,就会直接撞名。
这个问题的解决办法不是“相信模型会注意”,而是在 AST 层做 Scope 分析。
也就是说,在真正插入标识符之前,先通过 Babel Scope 判断当前作用域是否已有同名绑定。如果有,就自动重命名,比如:
setCount→setCountStatedata→dataState- 或者根据作用域生成唯一后缀
结论很简单:
命名安全,不能交给模型兜底,必须交给编译器能力兜底。
坑 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,而是一个能进流水线、能跑在仓库里、能服务团队的工程工具。