代码迁移是软件工程中最繁琐的任务之一。从 React 类组件升级到 hooks。将代码库从 JavaScript 迁移到 TypeScript。将 API 从 REST 迁移到 gRPC。这些项目定义明确、重复性高且耗时 — 这使它们成为 AI 辅助的理想候选。
过去一年,我使用 AI 工具协助了四个主要迁移项目。两个进展顺利。一个是部分成功。一个是灾难性的,比手动迁移花费了更多时间。本文将分析哪些方法有效,哪些无效,以及如何评估 AI 辅助迁移是否适合您的项目。
2026 年的迁移格局
AI 驱动的代码迁移分为三类:
- LLM 辅助迁移: 使用 Claude、GPT-4 或类似模型逐个转换代码文件,并进行人工审核。这是最常见的方法。
- 专用迁移工具: 专门构建的工具,如 OpenRewrite(用于 Java)、ts-morph(用于 TypeScript)和 jscodeshift(用于 JavaScript),它们使用 AST 转换。
- 混合方法: 使用 AI 生成 AST 转换规则,然后在整个代码库中确定性地应用这些规则。
我学到的关键见解:AI 非常擅长理解意图和生成初始转换,但在处理大型代码库中的一致性方面存在困难。混合方法 — 使用 AI 编写转换规则,然后机械地应用它们 — 始终能产生最佳结果。
案例研究 1:JavaScript 到 TypeScript(成功)
项目:一个 45,000 行的 Express.js API,包含 180 个文件,零 TypeScript。目标是在严格模式下全面采用 TypeScript。
有效的方法
我将迁移分为几个阶段,使用 Claude 协助每个阶段:
阶段 1:基础设施(手动,2 小时)
// tsconfig.json - 从宽松设置开始,稍后收紧
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": false, // 开始时宽松
"allowJs": true, // 与 JS 文件共存
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
阶段 2:类型提取(AI 辅助,4 小时)
我将数据库模式、API 路由和示例请求/响应载荷提供给 Claude。它生成了全面的类型定义:
// types/api.ts - 由 AI 生成,然后经过审查和优化
export interface User {
id: string;
email: string;
displayName: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
lastLoginAt: Date | null;
preferences: UserPreferences;
}
export interface UserPreferences {
theme: "light" | "dark" | "system";
emailNotifications: boolean;
timezone: string;
}
export interface CreateUserRequest {
email: string;
displayName: string;
role?: User["role"]; // 默认为 "viewer"
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
}
仅从模式来看,AI 生成的类型约有 85% 是正确的。其余 15% 需要手动优化,主要集中在 JavaScript 代码中隐含的可空字段和联合类型上。
第三阶段:逐文件转换(AI 辅助,12 小时)
这是方法至关重要的阶段。我没有要求 AI 独立转换每个文件,而是提供了一个包含类型定义和风格指南的转换提示:
将此 JavaScript 文件转换为 TypeScript。规则:
1. 使用 types/api.ts 中提供的类型
2. 所有导出的函数都优先使用显式返回类型
3. 在类型确实未知的情况下使用 unknown 而不是 any
4. 保留所有现有注释
5. 不要更改任何业务逻辑
6. 在无法从上下文确定类型的地方添加 TODO 注释
要转换的文件:
[粘贴文件内容]
提示的一致性极为重要。如果没有规则 #5,AI 偶尔会”改进”业务逻辑,引入微妙的错误。如果没有规则 #6,它会使用 any 来掩盖类型模糊性,而不是标记出来供人工审查。
结果
| 指标 | 值 |
|---|---|
| 总时间 | 18 小时(手动估计需要 60 小时) |
| 转换的文件数 | 180 |
| AI 准确率(无需手动编辑) | 72% |
| AI 引入的错误 | 3 个(在审查中发现) |
| 剩余的 TypeScript 严格模式错误 | 0 |
案例研究 2:React 类组件到 Hooks(部分成功)
项目:一个包含 120 个组件的 React 应用程序。大约 60 个组件使用了带有生命周期方法、ref 和复杂状态管理的类语法。
有效的方法
简单的组件转换完美。AI 可以将这个:
class UserCard extends React.Component {
constructor(props) {
super(props);
this.state = { expanded: false };
}
toggleExpand = () => {
this.setState(prev => ({ expanded: !prev.expanded }));
}
render() {
return (
<div className="user-card">
<h3>{this.props.user.name}</h3>
{this.state.expanded && <UserDetails user={this.props.user} />}
<button onClick={this.toggleExpand}>
{this.state.expanded ? "Collapse" : "Expand"}
</button>
</div>
);
}
}
每次都正确地转换为这个:
function UserCard({ user }: { user: User }) {
const [expanded, setExpanded] = useState(false);
const toggleExpand = useCallback(() => {
setExpanded(prev => !prev);
}, []);
return (
<div className="user-card">
<h3>{user.name}</h3>
{expanded && <UserDetails user={user} />}
<button onClick={toggleExpand}>
{expanded ? "Collapse" : "Expand"}
</button>
</div>
);
}
失败的部分
复杂的组件,包含相互作用的 componentDidMount、componentDidUpdate 和 componentWillUnmount,存在问题。AI 生成的 useEffect 钩子看起来正确,但存在微妙的依赖问题:
// AI 生成的 — 看起来正确,但有陈旧闭包错误
useEffect(() => {
const interval = setInterval(() => {
if (isActive) { // 这里捕获的是 isActive 的初始值
fetchNewData();
}
}, 5000);
return () => clearInterval(interval);
}, []); // 依赖数组中缺少 isActive
这些错误很隐蔽,因为它们能通过表面审查。组件在大多数测试场景中都能正确渲染。陈旧闭包只在用户在初始渲染后切换 isActive 时才会显现,而这可能不在现有测试的覆盖范围内。
解决方案:基于 AST 的规则
对于复杂组件,我转而编写 jscodeshift 转换器,并利用 AI 帮助编写转换器本身:
// jscodeshift 转换器:将 componentDidMount 转换为 useEffect
export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
root.find(j.MethodDefinition, {
key: { name: "componentDidMount" }
}).forEach(path => {
const body = path.value.value.body;
// 生成带有空依赖数组的 useEffect
const useEffect = j.expressionStatement(
j.callExpression(j.identifier("useEffect"), [
j.arrowFunctionExpression([], body),
j.arrayExpression([]) // 空依赖 = 仅挂载
])
);
// 在函数组件主体中替换
j(path).replaceWith(useEffect);
});
return root.toSource();
}
该转换器是确定性的——它对每个匹配的组件应用相同的转换。AI 帮助我编写了转换器,但执行过程是机械的。这消除了一致性问题。
案例研究 3:REST 到 gRPC(灾难)
项目:将一个 40 个端点的 REST API 迁移到 gRPC 以实现内部服务通信,同时为外部客户端维护 REST 网关。
失败原因
迁移需要同时更改多个层:Protocol Buffer 定义、服务器实现、客户端代码和 REST 网关。AI 可以单独处理每一层,但不能跨层保持一致性。
具体问题:
- AI 生成的 .proto 文件看起来正确,但在服务之间使用了不一致的命名约定
- 由于上下文窗口限制,生成的服务器实现与 .proto 定义不匹配
- gRPC 状态码和 HTTP 状态码之间的错误映射不一致
- 流式端点生成了不正确的流控制
经过三天的 AI 辅助迁移和调试后,我放弃了 AI 生成的代码,并在五天内完成了手动迁移。手动方法虽然较慢,但产生了正确、一致的代码。
经验教训
当转换是局部的时,AI 辅助迁移有效——一次转换一个文件、一个组件或一个函数,具有明确的输入和输出类型。当转换是系统性的时,它会失败——需要跨多个文件进行协调更改,这些文件必须保持相互一致性。
评估 AI 迁移的框架
基于这些经验,这里有一个决策框架:
| 因素 | AI 表现良好 | AI 面临挑战 |
|---|---|---|
| 范围 | 逐文件转换 | 跨文件协调 |
| 验证 | 编译器捕获错误 | 仅运行时验证 |
| 模式 | 机械性、重复性 | 需要领域知识 |
| 类型 | 明确定义的输入/输出 | 隐式契约 |
| 测试 | 现有测试验证 | 无现有测试覆盖 |
理想的 AI 迁移候选具有三个特性:
- 每个文件可以独立转换
- 类型检查器或编译器验证输出
- 现有测试确认行为正确性
2026年值得使用的工具
对于 JavaScript/TypeScript 迁移:
- ts-morph: 程序化 TypeScript AST 操作。非常适合添加类型、重命名、重构。
- jscodeshift: Facebook 的代码修改工具包。已在数百万行代码上经过实战检验。
- Claude/GPT-4 与结构化输出: 用于生成转换规则本身。
对于 Java 迁移:
- OpenRewrite: 黄金标准。处理 Spring Boot 升级、Java 版本迁移和依赖更新。其配方系统是确定性的和可测试的。
- Error Prone: Google 的静态分析工具,包含可作为微迁移使用的自动修复建议。
对于多语言项目:
- Semgrep: 基于模式的代码转换,跨语言工作。用于安全相关迁移(修复易受攻击的模式)和 API 更改。
- ast-grep: 一个较新的工具,结合了 AST 匹配和简单的模式语法。对于大型代码库比 Semgrep 更快。
混合工作流
最有效的方法:
- 使用 AI 分析代码库 并识别所有需要迁移的实例。AI 擅长分类。
- 使用 AI 生成转换规则(jscodeshift 转换、OpenRewrite 配方、Semgrep 规则)。让它编写规则,而不是应用规则。
- 机械性地应用规则 到整个代码库。这确保了一致性。
- 使用 AI 处理长尾问题 — 不符合机械规则的 10-15% 的案例。这些需要个别关注,AI 可以为人工审核草拟初始转换。
- 运行现有测试。 如果测试失败,手动调试。AI 生成的测试失败修复往往会掩盖错误而不是修复错误。
这种工作流程利用了 AI 的优势(理解模式、生成代码),同时避免了其弱点(保持文件间的一致性、处理复杂的相互依赖关系)。
接下来会发生什么
“AI 可以转换这个文件”和”AI 可以迁移这个系统”之间的差距正在缩小。更长的上下文窗口有所帮助。工具使用能力(AI 可以运行编译器并迭代修复错误)提供了更多帮助。但就目前而言,混合方法——AI 生成的规则,机械式应用——仍然是生产环境迁移最可靠的路径。
如果您正在计划一次迁移,先从 10 个文件的试点开始。测量 AI 的准确率。如果准确率在 80% 以上且只需少量审核,就可以扩大规模。如果低于 60%,则应该投入编写确定性转换规则。最糟糕的结果是一个不一致的代码库,其中一半的文件由 AI 使用略有不同的模式进行了迁移。
