微软工程师做了一次颇具象征意义的迁移:让 AI agents 重写支撑 GitHub Copilot 产品族的运行时。公开摘要给出的数字很醒目——约 43 万行 TypeScript 变成约 80 万行 Rust,消耗约 12 万美元的 token,并由一名工程师投入三周时间,报告性能提升达到 15.9 倍。
真正值得工程团队关注的,不只是“AI 能写多少代码”,而是这种迁移如何改变软件重写的成本结构:人类不再逐文件翻译,而是定义边界、建立验证系统、审查风险,让 agents 在约束内批量推进。
这不是普通的语言迁移
被迁移的是 Copilot 运行时,而不是一个边缘工具。根据摘要,它支撑 GitHub Copilot CLI、Copilot App 和 SDK 等产品形态。这意味着迁移必须同时面对几类问题:
- 行为兼容性:相同输入是否产生相同输出、错误和退出码;
- 协议兼容性:CLI、应用和 SDK 依赖的接口不能随意变化;
- 并发与资源管理:TypeScript 的异步模型与 Rust 的所有权、线程和异步运行时并非一一对应;
- 发布安全性:新实现必须能够灰度、回退,并与旧实现并行验证;
- 可观测性:性能提升不能以丢失日志、指标或错误上下文为代价。
因此,“三周完成”不应理解为三周内凭空生成 80 万行可用代码。更合理的工程解读是:原实现已经提供了相对明确的行为规范,而 AI agents 承担了大量机械迁移、编译修复和测试迭代工作,人类工程师则负责设计验证闭环与处理高风险决策。
80 万行 Rust 并不自动代表更复杂
迁移后代码量从 43 万行增加到 80 万行,很容易被解读成 Rust 版本更臃肿,但仅靠行数无法得出这个结论。
TypeScript 可以依赖运行时动态行为、npm 包以及隐式类型转换;Rust 往往会把类型、错误、协议结构和平台适配显式写进代码。生成式迁移还可能产生更多包装层、重复实现和展开后的样板代码。反过来,更多代码也不等于更安全:如果重复逻辑、无效抽象和未使用分支没有被清理,维护成本仍会快速上升。
报告中的 15.9 倍性能提升同样需要结合测试边界理解。摘要没有给出具体工作负载,因此不能直接推断每个 Copilot 请求、每个平台或所有端到端场景都会快 15.9 倍。评估类似结果时,至少要追问:
- 测量的是启动时间、吞吐量、延迟,还是 CPU 密集型任务?
- 是否包含网络请求和模型响应时间?
- 比较的是开发构建还是优化后的 release 构建?
- p50、p95 和 p99 延迟是否都改善?
- 内存峰值、二进制体积与崩溃率有没有变化?
在 AI 产品中,模型调用可能占据端到端延迟的大头。运行时快 15.9 倍,并不必然让用户感知到同样幅度的提升,但它仍可能显著改善 CLI 启动、请求编排、本地处理和资源占用。
可复制的迁移方法:先做差分测试,再让 Agent 改代码
大规模重写最危险的做法,是让 agent 一次性生成新仓库,然后依靠人工阅读判断是否正确。更稳妥的方式是把旧程序当作“行为预言机”:对同一批输入分别运行 TypeScript 和 Rust 实现,再逐字节或按结构比较结果。
下面是一个可以直接运行的最小示例。它不是 Copilot 的真实代码,而是演示迁移流水线的简化项目。运行前需要安装 Node.js 和 Rust 工具链。
set -euo pipefail
rm -rf runtime-migration-demo
mkdir runtime-migration-demo
cd runtime-migration-demo
cat > legacy.mjs <<'EOF'
const input = Number.parseInt(process.argv[2] ?? "0", 10);
if (!Number.isFinite(input)) {
console.error("invalid integer");
process.exit(2);
}
console.log(JSON.stringify({ input, result: input * 2 + 1 }));
EOF
cargo new --quiet rust-runtime
cat > rust-runtime/Cargo.toml <<'EOF'
[package]
name = "rust-runtime"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
EOF
cat > rust-runtime/src/main.rs <<'EOF'
use serde::Serialize;
use std::process::ExitCode;
#[derive(Serialize)]
struct Output {
input: i64,
result: i64,
}
fn main() -> ExitCode {
let raw = std::env::args().nth(1).unwrap_or_else(|| "0".into());
let input: i64 = match raw.parse() {
Ok(value) => value,
Err(_) => {
eprintln!("invalid integer");
return ExitCode::from(2);
}
};
println!("{}", serde_json::to_string(&Output {
input,
result: input * 2 + 1,
}).unwrap());
ExitCode::SUCCESS
}
EOF
cargo build --quiet --release --manifest-path rust-runtime/Cargo.toml
for value in -10 0 1 42 9999; do
old_output="$(node legacy.mjs "$value")"
new_output="$(./rust-runtime/target/release/rust-runtime "$value")"
test "$old_output" = "$new_output" || {
echo "Mismatch for input $value"
echo "TypeScript: $old_output"
echo "Rust: $new_output"
exit 1
}
done
echo "All differential tests passed"
真实项目需要把这个模式扩展到以下对象:
- 正常输入、边界值和畸形输入;
- stdout、stderr、退出码与错误类型;
- 文件系统变更和网络请求序列;
- 超时、取消、重试与并发行为;
- 固定数据集上的性能和内存基线。
如果输出中含有时间戳、随机 ID 或字段顺序,比较前应先做规范化,而不是简单比较字符串。对于网络服务,可以录制请求与响应,或让新旧实现同时处理镜像流量,但只允许旧版本产生真实副作用。
Agent 应该接收任务单,而不是一句“全部改成 Rust”
可以把迁移切成可审查的小单元,并给 agent 明确的完成条件。下面这类提示词比开放式要求更适合进入工程流水线:
将 packages/protocol/session.ts 迁移到 crates/protocol/src/session.rs。
约束:
1. 不修改公开 JSON 字段名、默认值和错误码。
2. 不新增网络请求或文件系统副作用。
3. 禁止使用 unsafe。
4. 为每个导出函数添加正常、边界和错误输入测试。
5. 运行旧实现与新实现的差分测试。
6. 提交中附上未能证明等价的行为清单。
完成条件:
- cargo fmt --check 通过;
- cargo clippy -- -D warnings 通过;
- cargo test 通过;
- 差分测试无差异;
- 单次提交只迁移这个模块及其必要依赖。
这里最重要的一条不是“测试通过”,而是要求 agent 列出无法证明等价的部分。AI 很擅长生成看起来完整的实现,也可能在不确定时补出一个合理但错误的行为。显式暴露不确定性,能把人类审查集中到协议、并发、权限和数据处理等高风险区域。
如何判断这类重写是否值得做
12 万美元 token 成本和一名工程师三周投入展示了惊人的生成效率,但它们不应被直接当作完整项目成本。团队还需要计算测试基础设施、代码审查、安全评估、灰度发布、回归处理,以及未来维护 80 万行 Rust 的成本。
准备采用类似方法时,可以用下面的清单做决策:
- 旧系统是否拥有足够完整的自动化测试和可重放样本?
- 公共协议能否冻结,并用机器验证兼容性?
- 新语言是否确实解决性能、内存、安全或部署问题?
- 团队是否具备维护 Rust 的能力,而不只是生成 Rust 的能力?
- 是否可以按模块双轨运行,并在异常时快速回退?
- 性能数字是否来自代表真实流量的 release 构建?
- 是否对依赖许可证、供应链和 AI 生成代码做了审查?
这次迁移最有价值的信号,不是“AI 可以替代几十万行手写代码”,而是软件重写正在变成一种由验证系统驱动的工作。Agents 可以快速搬运和改写实现,但行为规范、验收标准、风险边界与最终责任仍然必须由工程团队掌握。