1. 这件事到底是怎么发生的:三周、128个PR、83万行代码
第一次看到"三周时间,128个PR,83万行代码"这组数字的时候,我的第一反应是:这要么是一次大规模重构,要么就是一次"AI主导的代码迁移"。后来仔细看下来,确实是后者——GitHub 用 AI 辅助的方式,把自家平台里相当一部分核心代码从一种技术栈迁移到了另一种技术栈,而整个过程只用了三周。
先把这组数字拆开看,你就能感受到它的量级:
- 三周:大约 15 个工作日,如果按每天 8 小时算,也就 120 个小时左右。
- 128 个 PR:平均每天要合并 8 到 9 个 Pull Request,而且这些 PR 不是改改文案、修修样式,而是涉及核心逻辑的迁移。
- 83 万行代码:这个体量,如果靠人工一行行改,一个熟练工程师一天能高质量迁移 500 到 1000 行就已经很不错了,83 万行意味着至少需要 830 到 1660 个工作日,也就是 3 到 6 年。
所以这件事的核心不是"AI 写了多少代码",而是AI 把一件原本需要几年、几十人团队才能完成的事情,压缩到了三周。这才是真正值得聊的地方。
我自己也做过类似的代码迁移项目,规模比这个小得多,大概几万行的 TypeScript 迁移到 Rust 的部分模块,当时用了将近两个月,还踩了一堆坑。所以看到这个案例的时候,我特别能理解里面那些"看起来简单、做起来要命"的细节。
这篇文章我会从几个角度来拆:
- 为什么 GitHub 要做这次迁移,背后的技术选型逻辑是什么;
- AI 在其中到底扮演了什么角色,是"写代码"还是"审代码";
- 128 个 PR 是怎么拆的,为什么这样拆;
- 实操过程中会遇到哪些坑,怎么排查;
- 如果你也想在自己的项目里复现这套流程,应该从哪一步开始。
适合谁看?如果你正在做技术栈迁移、正在尝试把 AI 引入研发流程、或者单纯好奇"AI 重写大型项目"到底靠不靠谱,这篇应该都能给你一些可以直接抄作业的东西。
2. 为什么是 Rust 和 TypeScript:技术选型背后的真实考量
2.1 从 TypeScript 到 Rust 的迁移动机
GitHub 平台早期大量使用 Ruby on Rails,后来逐步引入 TypeScript 做前端和部分服务端逻辑。TypeScript 的优势很明显:开发速度快、生态成熟、类型系统够用。但它的短板也很明显:
- 运行时性能:TypeScript 编译成 JavaScript 后,在 Node.js 上跑,遇到 CPU 密集型任务(比如大规模文本处理、代码解析、diff 计算)就会成为瓶颈。
- 内存占用:Node.js 的内存管理在大规模并发场景下不够精细,GC 停顿会影响响应时间。
- 并发模型:JavaScript 的单线程事件循环在处理多核并行计算时,需要靠 Worker Threads 绕来绕去,写起来复杂,调试也麻烦。
Rust 恰好补上了这些短板:零成本抽象、无 GC、所有权模型保证内存安全、原生支持多线程。对于 GitHub 这种需要处理海量代码仓库、diff、搜索索引的场景,Rust 的吸引力非常大。
但迁移不是"重写一遍"那么简单。GitHub 的代码库不是一个小项目,里面有大量的业务逻辑、边界条件、历史遗留的兼容性处理。如果全靠人工迁移,成本高到不现实。所以这次迁移的核心思路是:用 AI 做"批量翻译",用人做"关键审核"。
2.2 为什么不是"全部重写",而是"逐步迁移"
这里有一个很重要的工程判断:不要试图一次性重写整个系统。
我见过太多团队一上来就说"我们要用 Rust 重写整个后端",结果做了半年,新系统还没上线,旧系统已经改得面目全非,两边对不齐,最后项目黄了。
GitHub 的做法更务实:按模块拆分,逐个迁移,每个模块迁移完立刻跑测试、对比行为、合并上线。这样即使某个模块出问题,影响范围也可控。
具体来说,他们的迁移策略大概是这样的:
| 策略 | 做法 | 优点 | 风险 |
|---|---|---|---|
| 全量重写 | 一次性用 Rust 重写所有逻辑 | 架构干净 | 周期长、风险高、容易烂尾 |
| 逐步迁移 | 按模块拆分,逐个迁移 | 风险可控、可回滚 | 需要维护两套代码一段时间 |
| 并行运行 | 新旧逻辑同时跑,对比结果 | 验证充分 | 资源消耗翻倍 |
| AI 辅助翻译 | AI 生成初版,人工审核 | 速度快 | 需要严格的审核流程 |
GitHub 选择的是"逐步迁移 + AI 辅助翻译 + 并行验证"的组合。这也是我认为目前最靠谱的方案。
2.3 AI 在迁移中的真实角色
很多人看到"AI 重写代码"就会想象成:AI 自己读代码、自己改、自己提交。实际上完全不是这样。
在这次迁移里,AI 的角色更像是一个高级代码翻译器 + 初稿生成器。具体流程是:
- 人工确定迁移范围和接口边界;
- AI 根据 TypeScript 源码生成对应的 Rust 初版;
- 人工审核 AI 生成的代码,重点看类型映射、错误处理、边界条件;
- 跑测试,对比新旧行为;
- 修复差异,合并 PR。
AI 负责的是"把 80% 的机械翻译工作做掉",人负责的是"剩下 20% 需要判断力的部分"。但恰恰是这 20%,决定了项目能不能成。
提示:不要指望 AI 一次性生成完全正确的迁移代码。它的价值在于把"从零写"变成"改一改就能用",这个效率提升是巨大的,但审核环节绝对不能省。
3. 128 个 PR 是怎么拆出来的:迁移的工程化拆解
3.1 PR 拆分的原则
128 个 PR 听起来很多,但如果按模块拆,其实很合理。假设每个 PR 对应一个相对独立的模块或功能点,128 个 PR 大概覆盖了 128 个迁移单元。
拆 PR 的核心原则是:每个 PR 必须可以独立审核、独立测试、独立回滚。
具体来说,一个好的迁移 PR 应该满足:
- 边界清晰:只改一个模块或一个功能,不跨模块;
- 可测试:有对应的单元测试或集成测试;
- 可对比:新旧逻辑的行为可以逐条对比;
- 可回滚:出问题能单独 revert,不影响其他 PR。
我自己的经验是,如果一个 PR 超过 500 行有效改动,审核质量就会明显下降。所以 83 万行除以 128 个 PR,平均每个 PR 大概 6500 行——这个数字看起来很大,但考虑到其中很多是 AI 生成的机械翻译代码,实际需要人工仔细看的部分可能只有几百行。
3.2 迁移单元的选择
不是所有代码都值得迁移。GitHub 在选迁移单元的时候,大概率遵循了这几个标准:
- 性能瓶颈明显:比如 diff 计算、语法解析、搜索索引构建;
- 逻辑相对独立:不依赖太多外部状态,接口清晰;
- 测试覆盖充分:有现成的测试用例,迁移后能快速验证;
- 业务风险可控:即使出问题,也不会导致核心功能不可用。
反过来,那些"逻辑复杂、依赖多、测试少"的模块,大概率被排在了后面,或者干脆不迁移。
3.3 接口边界的处理
迁移中最容易出问题的地方就是接口边界。TypeScript 和 Rust 的类型系统差异很大:
- TypeScript 有
any、undefined、null,Rust 没有; - TypeScript 的对象是动态的,Rust 的 struct 是静态的;
- TypeScript 的异步是 Promise,Rust 的异步是 Future + async/await;
- TypeScript 的错误是异常,Rust 的错误是
Result<T, E>。
所以迁移的时候,必须先把接口边界定义清楚。常见的做法是:
- 用 Rust 定义一套与 TypeScript 对应的类型;
- 写一层 FFI(外部函数接口)或者用 WASM 做桥接;
- 在边界处做严格的类型转换和错误处理。
这一步如果做不好,后面会无穷无尽地修 bug。
4. AI 辅助迁移的实操流程:从 TypeScript 到 Rust
4.1 环境准备与工具链
如果你也想复现这套流程,先把工具链搭好。以下是我实测下来比较稳的组合:
- Rust 工具链:
rustup+cargo,建议用 stable 版本,nightly 只在必要时用; - TypeScript 环境:Node.js 20+,
tsc或者swc做编译; - AI 辅助工具:Copilot 或者类似的代码生成工具,配置好 API base;
- 测试框架:Rust 用
cargo test,TypeScript 用vitest或jest; - 对比工具:自己写脚本,跑新旧逻辑,对比输出。
安装 Rust 的命令很简单:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh装完之后验证一下:
rustc --version cargo --versionTypeScript 这边,如果你用的是较新的版本,注意moduleResolution和baseUrl这些选项在新版本里有变化,迁移前先把tsconfig.json理清楚。
4.2 第一步:用 AI 生成 Rust 初版
假设你有一段 TypeScript 代码需要迁移,比如一个简单的字符串处理函数:
function normalizePath(path: string): string { return path.replace(/\\/g, '/').replace(/\/+/g, '/'); }你可以让 AI 生成对应的 Rust 版本:
pub fn normalize_path(path: &str) -> String { let mut result = path.replace('\\', "/"); while result.contains("//") { result = result.replace("//", "/"); } result }AI 生成的初版通常能用,但有几个地方需要人工检查:
- 性能:上面的 Rust 版本用了循环替换,效率不高,更好的写法是用正则或者一次遍历;
- 边界条件:空字符串、只有斜杠的字符串、超长字符串,行为是否一致;
- 错误处理:TypeScript 版本不会抛异常,Rust 版本也不应该 panic。
我一般会让 AI 生成 2 到 3 个版本,然后对比选最好的。实测下来,AI 在"机械翻译"上准确率很高,但在"优化写法"上经常给出平庸的方案。
4.3 第二步:人工审核的重点
AI 生成的代码,审核的时候重点看这几个地方:
| 审核项 | 常见问题 | 检查方法 |
|---|---|---|
| 类型映射 | any被映射成serde_json::Value,丢失类型信息 | 检查所有any的来源 |
| 错误处理 | TypeScript 的 try/catch 被忽略 | 检查所有可能 panic 的地方 |
| 异步逻辑 | Promise 链被错误地转成阻塞调用 | 检查 async/await 的对应关系 |
| 内存管理 | 不必要的 clone,或者生命周期错误 | 用 clippy 检查 |
| 边界条件 | 空值、越界、溢出 | 补充单元测试 |
Rust 的clippy是一个非常好的工具,能帮你发现很多潜在问题:
cargo clippy -- -D warnings4.4 第三步:并行验证
迁移完一个模块后,不要急着删掉旧代码。正确的做法是让新旧逻辑并行跑一段时间,对比输出。
具体做法:
- 写一个测试 harness,输入同一组数据;
- 分别调用 TypeScript 版本和 Rust 版本;
- 对比输出,记录差异;
- 分析差异,判断是 bug 还是预期行为变化。
这一步非常关键。我在自己的项目里就遇到过:Rust 版本在某个边界条件下返回了不同的结果,查了半天发现是 TypeScript 的浮点数精度问题和 Rust 不一致。这种问题如果不做并行验证,上线后就是线上事故。
4.5 第四步:合并与回滚预案
每个 PR 合并前,必须准备好回滚预案。常见的做法是:
- 用 feature flag 控制新旧逻辑的切换;
- 保留旧代码至少一个发布周期;
- 监控关键指标,发现异常立刻切回。
GitHub 的 128 个 PR 能三周内合并完,说明他们的 CI/CD 流程非常成熟,每个 PR 的测试和验证都是自动化的。这一点如果没有,AI 生成再多代码也没用。
5. 常见问题与排查技巧实录
5.1 AI 生成的代码编译不过怎么办
这是最常见的问题。AI 生成的 Rust 代码,大概有 20% 到 30% 第一次编译会报错。常见原因和解决方法:
- 生命周期错误:AI 经常忽略生命周期标注。解决方法是手动补上
<'a>,或者改用 owned 类型; - 借用检查失败:AI 会写出同时可变借用和不可变借用的代码。解决方法是重构逻辑,或者用
RefCell临时绕过; - trait 未实现:AI 假设某个类型实现了某个 trait,实际没有。解决方法是手动实现,或者换一个类型;
- 依赖缺失:AI 用了某个 crate 但没加到
Cargo.toml。解决方法是补上依赖。
我的经验是,不要试图一次修完所有错误。先让代码编译通过,再跑测试,再优化。分阶段处理,效率更高。
5.2 行为不一致怎么排查
新旧逻辑行为不一致,是最难查的问题。我的排查思路是:
- 缩小范围:找到最小的输入,能复现差异;
- 打印中间状态:在关键步骤打印变量值,对比两边;
- 二分查找:注释掉一半逻辑,看差异是否还在;
- 查文档:确认两边对同一个操作的定义是否一致。
常见的不一致来源:
- 字符串编码(UTF-8 vs UTF-16);
- 浮点数精度;
- 正则表达式的方言差异;
- 排序算法的稳定性;
- 时间处理的时区问题。
5.3 性能反而变慢了怎么办
Rust 不一定比 TypeScript 快。如果迁移后性能变慢,检查这几个地方:
- 不必要的 clone:Rust 的所有权模型下,clone 是显式开销;
- 锁竞争:多线程下锁用多了,性能反而下降;
- 内存分配:频繁的
String和Vec分配; - 编译优化:release 模式下有没有开
--release。
用perf或者flamegraph做性能分析,找到真正的瓶颈。
5.4 常见问题速查表
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 编译报生命周期错误 | AI 忽略生命周期 | 手动补标注或改 owned |
| 测试通过但线上出错 | 边界条件未覆盖 | 补充边界测试 |
| 性能下降 | clone 过多或锁竞争 | 用 perf 分析,优化热点 |
| 内存泄漏 | 循环引用或未释放 | 用 valgrind 或 heaptrack |
| 异步逻辑死锁 | Future 未正确 await | 检查 async 调用链 |
注意:AI 生成的代码,永远不要直接合并到主分支。必须经过人工审核、测试、并行验证三个环节。
6. 如果你想复现这套流程:从哪开始
6.1 小规模试点
不要一上来就迁移整个项目。先选一个 500 到 1000 行的小模块,走一遍完整流程:
- 用 AI 生成 Rust 初版;
- 人工审核并修复编译错误;
- 写测试,对比新旧行为;
- 合并,观察一段时间。
这个过程大概需要 1 到 2 周。走通之后,再逐步扩大范围。
6.2 建立审核规范
AI 生成的代码,审核必须有规范。我自己的规范是:
- 所有
unsafe代码必须人工逐行审核; - 所有涉及内存分配的地方必须检查;
- 所有错误处理必须明确;
- 所有公共接口必须有文档注释。
6.3 自动化测试是基础
没有测试,AI 迁移就是灾难。迁移前先确保:
- 单元测试覆盖率 80% 以上;
- 有集成测试覆盖核心流程;
- 有性能基准测试。
6.4 团队协作的注意事项
- 每个 PR 指定一个审核人,不要多人同时审;
- 审核人必须懂 Rust,不能只看逻辑不看实现;
- 建立共享的"迁移踩坑文档",记录常见问题和解决方法;
- 定期同步进度,避免多个 PR 冲突。
我在实际项目里踩过最大的坑,就是低估了"接口边界"的复杂度。TypeScript 的动态类型和 Rust 的静态类型之间,有一层看不见的鸿沟。AI 能帮你填平大部分,但剩下的那部分,必须靠人对业务的理解来补。三周 128 个 PR 听起来很猛,但背后是成熟的工程体系在支撑。如果你也想试,先把测试和 CI 搭好,再让 AI 上场。