GitHub 用 Copilot 把自己重写成 Rust:80 万行迁移实录与智能体工作流拆解

一句话结论
GitHub 把 Copilot 智能体运行时从 43 万行 TypeScript 完整迁到 80 万行生产 Rust,全程主要靠 Copilot 自己写代码:128 个 PR 增量上线、一名工程师主导、几个月完成,性能提升数量级。这是一次「用智能体做超大规模重构」的完整实证——编排方式、审查机制、回归分类、token 花销全部公开,任何想用 AI 编程做大型改造的团队都能直接对照。
背景:一套被全家共用的运行时
Copilot CLI、Copilot 应用和 Copilot SDK 的底层都是同一个「Copilot 智能体运行时」——一个可以嵌进应用和服务的智能体框架。它最初用 TypeScript 写在 Node.js 和 V8 上,为 GitHub Copilot 云智能体(CCA)服务,之后随着能力扩张一直留在这套技术栈上。
现在它变了。用 Copilot 应用和 Copilot CLI,团队把运行时完全重写成超过 80 万行生产 Rust。AI 智能体写了其中大部分代码,横跨 128 个合入主干并增量发布的 PR,而不是等最后一次大切换。少量不可避免的回归在过程中被快速发现和修复,运行时性能提升了几个数量级。一个以前要整队工程师一到两年的项目,现在主要由一名工程师在几个月内完成,同时团队其他人在持续扩展运行时的能力边界。
这个运行时不只是 Copilot CLI 的引擎。它支撑着一长串微软和 GitHub 产品:VS Code、Visual Studio、CCA、Copilot Code Review(CCR)、Copilot Cowork、Copilot Studio,以及 Excel、Outlook、PowerPoint、Word 等。这些产品架构上都是「同一运行时加各自定制」的外壳。
这些产品形态差异极大,谁都不该自己去实现一个生产级智能体框架的全部细节。它们要的是智能、安全、可靠、性能,而且要共享——一处修复处处生效。上面多数产品最初各自实现过自己的智能体循环,后来都换成了 GitHub Copilot SDK(即 Copilot 运行时的入口),把精力留给核心业务。在行业节奏极快的背景下,这一点尤其重要:被采用的智能体循环必须始终是最好的。
为什么必须重写:TypeScript 嵌入的代价
CLI 在逻辑上是「智能体循环之上套一个终端 UI」。整套栈都是 TypeScript:Node.js 做框架、V8 做执行引擎、Ink 和 React 做 UI。对一个控制台应用来说,这是体面的选择——TypeScript 和 Node.js 上手门槛低、开发速度快,启动、响应、吞吐、内存等性能指标对控制台也够用。
不幸的是,把这套实现放进别的环境、面对别的约束时,就不合理了:那些环境要求快速启动和低内存开销带来的高服务密度。
CLI 和运行时的架构也加剧了问题。行业跑得极快,聪明人为了交付速度做决策。Copilot CLI 起初写得快发得快,TUI 和运行时相当纠缠,没有分层。等需要 SDK 做编程访问时,在没有清晰分层的情况下,务实的选择是把 SDK 架在 CLI 之上——虽然逻辑上应该是反过来。CLI 加了一个无头模式:从 stdin 读命令、往 stdout 写响应,用 JSON-RPC 把函数调用跨进程转发。SDK 嵌进宿主程序后,会拉起一个 CLI 子进程来承载智能体循环。
这样做的后果是:从 SDK 创建一个 CopilotClient 就要生成一个子进程,这个进程要启动 Node 和 V8,要解析 CLI 编译出来的大量 JavaScript、生成字节码、在更高 JIT 层优化热点代码,还要背上 V8 的全部内存开销。它继承了 Node 的线程模型,默认把所有 CPU 密集工作串行化。它强迫所有函数调用跨进程通信。每个 SDK 使用方——不管什么语言——都得随包附带 Node.js 或内嵌 V8 的二进制。C#、Python、Go、Java、Rust 的 SDK 每个客户端都要为一个自己用不上的运行时多付约 100 MB 常驻内存起步。每次事件、每条消息、每个抽象的会话文件读写都被推过进程边界。Node 一崩整个会话跟着崩。部署者至少要监督、监控、调试两个进程。
为什么选 Rust
团队要的是一个能通过 C ABI 嵌入、启动和平稳期开销低、资源占用可预测的运行时。加上团队经验和行业方向这些软性因素,Rust 让这些目标成为可能,代价是另一些复杂度——比如必须显式表达生命周期和共享状态(后文的生命周期回归就是教训)。
作者明确强调:这绝不是说所有大型 TypeScript 程序都该变 Rust。目标语言因应用而异,这是工程判断,不是信仰。
迁移怎么算量:13 万行的估算错得多离谱
2026 年 5 月初的初始计划把运行时估为约 13 万行 TypeScript。作为范围估算这个数还算准,但事后看严重误导,原因有二:移植在推进的同时,团队还在持续给运行时加新能力;新代码早期以 TypeScript 为主,后期以 Rust 为主。
全部算上,作者估计约 43 万行生产 TypeScript 经过了移植。同样这些因素让进度很难看清:直到接近尾声,生产 TypeScript 的量看起来一直稳定甚至微涨,因为移植速度一直跟得上新工作涌入。而移植期间另有约 120 万行生产 Rust 进来、36.5 万行离开——表面稳定的 TypeScript 曲线背后是大量暗流。
原地替换,而不是大爆炸
这种规模的重写有两条主路:大爆炸——新 Rust 运行时作为完整替代品整体换入;或者增量——逐步替换。大爆炸又分两个变体:先冻结功能只做移植,或边移植边开发。
团队选了增量原地替换,理由是一串:保持新能力持续交付、避免长期分支合并地狱、让每一步都能被验证、回归能被快速发现而不是在最后集中爆发。
验证也靠增量发布完成。每个 PR 合入后走真实发布管线,出问题立刻回滚或修复。先靠两个 PR 建立信任并验证路子,再全面铺开。早期移植的小叶子组件走得快;大子系统不是一步到位,MCP 支持这样的核心模块是分多次 PR 逐步搬过去的。

互操作两层设计:.node 与 JSON-RPC
移植期间有互操作的两层。其一,发布的 runtime.node 是一个普通的平台共享库(.node 是 Node.js 原生插件的扩展名,底层是标准共享库)。这让 Node 侧代码能直接调用 Rust 实现,进程内、零序列化。等全部移植完成,这个临时接缝就拆掉了——运行时的生产实现现在是纯 Rust。
其二,对真正远程的运行时——跨子进程或 TCP——仍然需要 JSON-RPC。在进程内保持同一套协议,意味着只维护一套双向 API,调试工具和测试两边通用。
智能体工作流:人会话、子会话与并行移植
这次移植的智能体编排方式是最值得借鉴的部分。
用户的交互被分成几类。前三类占了交互的 63%:只有约 40 次是可识别的会话发起——那大多发生在作者先开一个对话探索方向的时候。大量工作是「会话生成会话」:一个移植会话发现需要先移植某个依赖,就派生一个子会话去处理,完成后父会话继续。

最难的移植之一是 session.ts。这个文件有机长到了约 3 万行 TypeScript,代表会话的核心抽象。移植它时会话日志显示,父会话和它的多数子会话在时间线上并行推进。作者为入口点会话写的启动提示词里明确告诉它「会话移植和六个组件移植正在并行进行」,让它知道自己边界的所在、不要碰别人正在搬的东西。
有个子会话原计划 42 小时,实际跑了 88 小时,结构也大不一样——这是智能体自主规划的常态。
审查:机器初审 + 人审关键处
作者自己的审查只是智能体审查的一部分。除了 CCR 在每次提交上运行,团队还有多个专门的代码审查机器人,各有自己的审查重点和风格。Agent Merge 处理了每一个移植 PR:修复全部 CI 失败、回应全部评论,只是大多数情况下停在真正合并那一步之前,留给人类最后确认。
智能体用得有多深:shell 命令与诊断分布
看智能体的 shell 工具流量,最常见的命令族印证了「定向与验证并重」的模式:找文件、看结构、跑构建、跑测试。
在编码诊断里,所有权、借用和生命周期错误合计只占 1.7%。借用检查器——那个在所有关于 Rust 难学的对话里霸榜的东西——在这里是安静的背景板。智能体拿着编译器当导航,机械错误被静态类型消灭了一大类。
一包一 crate:依赖对映
大量例子是一个 npm 包变成一个做同样事的 crate。js-tiktoken 变成 tiktoken-rs,同样用 o200k_base 编码;ignore 变成同名 crate。这种机械对映正是智能体擅长的领域。
unsafe 边界:哪里没用比哪里用了更重要
unsafe 用在哪的细节有意思,但作者更关心哪里没用:模型客户端、MCP 层、智能体层、提示词层都没有 unsafe。它集中在 FFI 边界和少数性能热点上。
回归分类:错在哪、为什么
绝对数量不重要,「为什么」才重要,这样才能学到东西、避免重复。几乎所有回归都能归进三类:
闪烁窗口。Windows 上生成子进程不加 CREATE_NO_WINDOW 会短暂弹出控制台窗口。Node 运行时当年给进程生成打了猴子补丁加了这个标志,掩盖了必要性;Rust 侧不知道,于是窗口闪了。
还有慢的。最后一类功能正确但变慢:有的回归丢了已有的优化,比如记忆化、完全异步等待、有界日志流;有的引入了新的同步点。
这绝不构成对 Rust 编译器的否定。跟任何静态类型语言一样,编译器消灭了一大类机械错误,给智能体提供了一个异常有用的反馈通道。回归集中在「隐式行为没被显式表达」的地方——这正是移植类工作的本质风险,也是人审该聚焦的地方。
性能:1000 个单轮会话生命周期的压力测试
「1000 个单轮会话生命周期」压力测试代表可构建能力的阶跃变化。它用单个共享客户端,测 100 个并发管线、每个创建并销毁会话的生命周期。这在以前的架构下不可行,现在跑通了。
token 账单:1363 亿总量怎么构成
整个移植工作的 token 花销:总量约 1363 亿,其中约 1306 亿是缓存输入读、42 亿缓存输入写、9 亿新鲜输入。缓存命中占比之高,是这种长周期智能体工作流能负担得起的关键——没有提示词缓存,这个账单结构完全不一样。
结果与剩余工作
成功了。5 月还全是 TypeScript 的执行运行时,8 月已全是 Rust,而且全程持续发给真实用户,而不是一次恐怖的大切换。移植本身已完成:运行时的生产实现 100% Rust,临时的 TypeScript/N-API 接缝已拆除。当然还有想继续做的事:进一步性能优化、更多平台覆盖。
原文信息
- 作者:Stephen Toub(微软杰出工程师,.NET 与性能领域知名工程师)
- 来源:The GitHub Blog(GitHub 官方博客)
- 原文发布日期:2026-09-16
原文地址:
暂无评论,快来抢沙发~