本文首发 猩猩面试题 公众号,欢迎关注
作者:Stephen Toub · @stephentoub 发布日期:2026年9月16日 | 阅读时长:65分钟
GitHub Copilot CLI、GitHub Copilot 应用程序和 GitHub Copilot SDK 均由 Copilot 智能体运行时(agent runtime)提供支持——这是一个可嵌入应用程序和服务的智能体框架。它最初以 TypeScript 编写,运行于 Node.js 和 V8 JavaScript 引擎之上,服务于现在的 GitHub Copilot 云智能体(CCA)。随着运行时功能的快速扩展,该技术栈一直沿用至今。
而现在,这一切已经改变。 借助 GitHub Copilot 应用程序和 Copilot CLI,我们将整个运行时完整重写为超过 80万行生产级 Rust 代码。AI 智能体编写了其中大部分代码,跨越 128 个合并到主分支的 Pull Request,并以增量方式持续交付,而非等待最后一次性切换。少数不可避免的回归问题在过程中被迅速发现并修复,与此同时运行时性能提升了数个数量级。一个在智能体出现之前需要整个团队花费一两年才能完成的项目,现在主要由一名开发者在短短几个月内完成——而团队其他成员则在此期间继续大幅扩展运行时的功能和覆盖范围。
为何需要迁移
Copilot 智能体运行时不仅仅是 Copilot CLI 背后的引擎,它还支撑着越来越多的 Microsoft、GitHub 及生态系统解决方案。对于每一个解决方案来说,AI 支持在架构上都是围绕同一个运行时的外壳,加上该解决方案所需的定制化内容。这包括:
- GitHub Copilot CLI 和 GitHub Copilot 应用程序
- VS Code、Visual Studio 的最新版本
- CCA、Copilot Code Review(CCR)
- Copilot Cowork、Copilot Studio
- Excel、Outlook、PowerPoint、Word……等等
这些都是截然不同的产品,没有一个希望或需要自己实现生产级智能体框架所需的一切。它们希望获得所有的智能、安全性、可靠性和性能,并且希望这些能够共享——在一处修复就能修复所有地方。上述大多数产品最初各自实现了自己的智能体循环,但后来都用 GitHub Copilot SDK 替换了它,后者是进入 Copilot 智能体运行时的入口。
问题在于被共享的东西本身的性质。
以 CLI 为例,它在逻辑上是一个构建在智能体循环之上的终端 UI(TUI)。整个技术栈都用 TypeScript 实现,使用 Node.js 作为框架,V8 作为执行引擎,Ink 和 React 用于 UI。对于控制台应用程序来说,这是一个体面的选择。然而,当这个实现被用于其他环境时,在启动速度、响应性、吞吐量和内存消耗方面的性能影响就变得相当不合理了。
架构问题也加剧了挑战。TUI 和运行时相互交织,而非分离为独立层次。当需要 SDK 进行编程访问时,一个务实的决定是将 SDK 叠加在 CLI 之上——尽管逻辑上应该是反过来的。SDK 通过 JSON-RPC 协议与 CLI 子进程通信:
const client = new CopilotClient();
await client.start(); // 将 CLI 作为子进程启动
const session = await client.createSession({
/* ... */
});
这意味着:
- 需要启动 Node 和 V8,解析大量 JavaScript,生成字节码
- V8 带来的全部内存开销
- Node 的线程模型,默认将所有 CPU 密集型工作串行化
- 强制跨进程通信来进行函数调用
- 每个 SDK 消费者都需要附带 Node.js 或包含 V8 的二进制文件
- C#、Python、Go、Java 和 Rust SDK 每个客户端都要为一个完整的第二语言运行时付出代价,最低工作集约 100 MB
- 每个事件、每条消息都要跨越进程边界
- Node 崩溃会带走整个会话
我们想要的运行时:
- 不包含 TUI,TUI 和其他应用程序可以干净地叠加在其上
- 用具有最小依赖和最小开销的语言实现
- 能够干净地嵌入进程内,而非强制跨进程
- 在性能、可扩展性和可靠性方面具有顶级特性
- 非常适合互操作,可被所有六种 Copilot SDK 语言版本(C#、TypeScript、Python、Rust、Go、Java)通过 FFI 机制干净使用
- 提供更现代的安全态势,降低供应链风险,更好地支持正确性构造代码
基于这些原因,以及更软性的原因(如团队经验和行业方向),我们选择了 Rust。这绝不是说每个大型 TypeScript 程序都应该变成 Rust。我们的需求强调通过 C ABI 嵌入、低启动和稳态开销,以及可预测的资源使用。Rust 使这些目标成为可能。
迁移前的状况
2026年5月初的初始移植计划估计运行时约有 13万行 TypeScript。然而这个数字具有误导性,原因有二:
- 仍包裹在 TUI 层中的部分被不断下推到运行时层
- 数十名开发者每周合并数百个 PR,持续增加新的 TypeScript 代码
综合所有因素,估计约有 43万行生产级 TypeScript 最终经历了移植过程。
在移植期间,运行时吸收了约 30万行 TypeScript 并删除了约 43万行,同时约 120万行生产级 Rust 代码进入并约 36.5万行离开。
最终:TypeScript 降至零,生产级 Rust 增长至约 83万行,Rust 单元测试约 46.9万行。
原地移植策略
有两种主要的大规模重写方式:
方式一:大爆炸式
- 停止世界:所有人停止其他工作,在主分支进行重写
- 并行开发:在功能分支进行重写,同时在主分支继续工作
方式二:原地移植
- 原子替换(我们选择的方式):逐个组件从 TypeScript 切换到 Rust,随着时间推移,越来越少的运行时是 TypeScript,越来越多是 Rust
- A/B 方式:同时维护 TypeScript 和 Rust 组件作为可热切换选项
我们选择方式 2a 的原因:
- 无人经历工作停滞,主分支持续活跃
- 运行时主分支始终可发布
- 重写是增量的、可审查的
- 大多数移植规模合理且相对独立
- 所有现有端到端测试在每一步都对新 Rust 代码运行
增量发布也是验证手段。 在约 14.5 周的移植窗口内,主分支发布了 135 个版本,包括 100 个预发布版本和 35 个稳定版本,平均每天约 1.3 个版本。每天也大约有 1.3 个移植 PR 开放,使得每个版本携带少量已知的移植组件。
到 8 月 21 日,运行时已 100% 是生产级 Rust:832,378 行生产级 Rust 和 468,689 行 Rust 单元测试,另加 174,675 行 E2E TypeScript 测试。
启动阶段
在全力投入之前,我们先建立信心并验证可行性:
- 前两个 PR 建立了 Rust 工作区、工具链、lint 规则、CI、构建流水线和编码指令
- 引入运行时 crate 以及代码生成和互操作模式
- 移植了一批纯逻辑原语(无 I/O 或共享状态,已有强测试)
工作按从叶节点向内的顺序进行:纯辅助函数、内容排除、Shell 工具、会话文件系统操作建立了翻译和测试模式。有状态子系统随后跟进,工具、钩子、模型客户端和 MCP 构建在这些基础之上。会话编排(迄今为止耦合最紧、最不自然并行的部分)将在接近末尾时进行。
| 时期 | PR 数量 | 变更行数中位数 |
|---|---|---|
| 5月1–15日 | 8 | 3,250 |
| 5月16–31日 | 2 | 9,421 |
| 6月1–15日 | 40 | 5,073 |
| 6月16–30日 | 31 | 8,253 |
| 7月1–15日 | 10 | 9,514 |
| 7月16–31日 | 14 | 28,159 |
| 8月1–15日 | 19 | 13,861 |
| 8月16–30日 | 4 | 99,445 |
互操作层
移植涉及两个主要的互操作层:
1. 临时内部互操作
通过 napi-rs 项目的 napi Rust crate 实现——它用于在 Rust 中构建 Node 原生插件。用 #[napi] 注解函数,宏会生成 N-API 注册胶水代码,使函数可从 JavaScript 调用,并在生成的 index.d.ts 中生成 TypeScript 声明。
流量也需要双向流动。Rust 代码运行在 Tokio 工作线程上,需要回调到 JavaScript 时,napi-rs 通过"线程安全函数"处理——允许 Rust 代码在 Node 主线程上调用 JavaScript 回调。
临时接缝在 8月3日 达到峰值:2,019 个内部 N-API 导出和 3,356 个 TypeScript 调用点。完成时:0 个临时内部 N-API 导出,0 个 TypeScript 调用点。
2. SDK 表面(永久层)
Copilot SDK 支持六种语言:TypeScript、Python、Go、C#、Java 和 Rust。
| SDK | 原生桥接 | 进程内客户端选择 |
|---|---|---|
| C# | P/Invoke | new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForInProcess() }) |
| Go | purego | copilot.NewClient(&copilot.ClientOptions{Connection: copilot.InProcessConnection{}}) |
| Java | JNA | new CopilotClient(new CopilotClientOptions().setConnection(RuntimeConnection.forInProcess())) |
| Python | cffi | CopilotClient(connection=RuntimeConnection.for_inprocess()) |
| Rust | libloading | Client::start(ClientOptions::new().with_transport(Transport::InProcess)).await? |
| TypeScript | koffi | new CopilotClient({ connection: RuntimeConnection.forInProcess() }) |
C ABI 门只有 19 个导出函数:4 个用于服务器生命周期,4 个用于会话注册和配置,8 个用于连接,3 个用于嵌入式主机。共享契约目前包含 364 个调度路由:340 个可被 SDK 消费者调用,24 个作为运行时到 SDK 的回调反向运行。
为什么进程内调用仍使用 JSON-RPC?
因为它使进程内托管成为即插即用而非重写。每个 SDK 已经有一个工作正常的 JSON-RPC 客户端,将 FFI 作为另一种传输方式挂载到该客户端下面,将字节路径从管道或套接字改为函数调用,而上面的一切保持不变。六个 SDK 以附加的、可选的传输方式获得了进程内托管,现有的传输方式不变。
会话数据分析
几乎所有数据来自两个来源:GitHub 仓库历史记录和智能体会话日志。
所有组成移植 PR 的数据汇总:
| 指标 | 数量 |
|---|---|
| 事件 | 12,760,995 |
| 用户消息 | 31,247 |
| 助手消息 | 1,385,214 |
| 钩子开始和结束事件 | 6,438,562 |
| 工具启动 | 1,857,409 |
| 编译命令 | 23,096 |
| 测试命令 | 19,485 |
| 变基命令 | 2,496 |
| 提交命令 | 7,410 |
| 推送命令 | 5,554 |
| 完成的压缩 | 5,116 |
那 31,247 条用户角色消息并非我亲自输入的 31,247 个提示;其中包括技能指令、自动合并标记、跨会话消息和子智能体流量。我个人输入或说出的大约 2,600 条,约占 1/12。
对 2,639 条人工编写消息的主要意图分类:
- 31.0%:审查、测试和 CI
- 17.4%:挑战技术或设计决策
- 15.0%:推动完整性
- 其余约 37%:会话启动、协调等
我的角色与其说是"分配任务然后等待",不如说是"操作控制循环":检查结果、挑战技术决策、执行质量门控,并在智能体将中间停止点视为终点时推动前进。
缓存的关键作用
LLM 提供商通常对输入 token 和输出 token 收取不同费率,并支持缓存提示词计算结果。折扣非常大——通常缓存命中享受 90% 折扣(例如 100 万输入 token 收费 0.20)。
移植工作的提示词缓存命中率:96.22%
- 缓存读取:96.22%
- 缓存写入:3.07%
- 新鲜输入:0.71%
这不是偶然的。GitHub Copilot 专门塑造智能体循环以保持长而稳定的前缀(系统提示、工具定义、累积对话),因此每次轮次都是在模型已经处理过的上下文上追加。
压缩讲述了一个互补的故事。在整个移植会话中,GitHub Copilot 自动压缩上下文 5,116 次(当会话填满上下文窗口并自我总结以继续进行的时刻)。单个会话基础设施移植 PR 在其多天的生命周期中压缩了 647 次。
静态分析的作用
直接验证命令结果捕获了 8,678 次 rustc 错误代码出现。四个最大的诊断类别覆盖了 84%:
- 37%:名称和导入解析,主要是 E0425("在此作用域中找不到值")
- 22%:缺少方法或字段
- 14%:类型不匹配
- 11%:未满足的 trait 约束
每一个都是普通的接线问题:名称输出略有错误、签名不匹配、字段被重命名、抽象未实现。这些正是批量翻译容易意外产生的错误,也正是编译器能非常快速捕获的错误。
值得注意的是,所有权、借用和生命周期错误合计只占编码诊断的 1.7%。借用检查器——那个主导所有关于 Rust 困难性讨论的东西——只是一个安静的背景存在。在 4,478 次直接 cargo check 运行中,87.1% 返回干净结果。
智能体的工作模式
| 工具 | 调用次数 | 中位时长 | 测量小时数 |
|---|---|---|---|
| powershell | 630,423 | 3秒 | 2,833.9 |
| view | 590,988 | 0秒 | 621.7 |
| rg | 281,783 | 1秒 | 408.4 |
| grep | 126,483 | 1秒 | 115.3 |
| apply_patch | 53,715 | 0秒 | 17.0 |
| edit | 40,591 | 1秒 | 24.1 |
| read_powershell | 36,728 | 90秒 | 1,203.9 |
| task | 13,080 | 274秒 | 2,329.0 |
关键观察: 智能体花在收集证据上的时间远多于修改代码。在文件读取、搜索工具与编辑工具之间,它们的探索量是变更量的 10 倍。AI 喷涌代码的流行形象几乎是颠倒的;在这个规模上,工作看起来更像是迭代调查。
Shell 命令家族分布:
| 命令家族 | 调用次数 | 中位时长 | 测量小时数 |
|---|---|---|---|
| git 检查 | 300,530 | 2秒 | 608.1 |
| git 其他 | 89,865 | 3秒 | 243.1 |
| 搜索 | 85,482 | 2秒 | 147.6 |
| pnpm test | 13,852 | 22秒 | 219.1 |
| pnpm lint | 9,757 | 29秒 | 177.0 |
| cargo test | 8,437 | 120秒 | 364.2 |
| git commit | 7,410 | 11秒 | 39.7 |
| cargo fmt | 5,223 | 18秒 | 77.2 |
| cargo check | 4,492 | 120秒 | 176.9 |
| pnpm build | 3,630 | 180秒 | 215.6 |
| cargo clippy | 2,115 | 135秒 | 107.4 |
| git rebase | 2,496 | 7秒 | 9.9 |
| cargo build | 566 | 104秒 | 20.3 |
模型选择
GitHub Copilot 允许单个会话在对话中途更换模型,也允许不同会话运行不同模型,因此模型选择成为每个切片的决策。
对于子智能体,模型混合看起来有些不同——智能体而非人类在优化吞吐量和成本。子智能体最常运行于 Claude Opus 4.8、GPT-5.6 Sol、Claude Haiku 4.5 和 GPT-5.5,其次是 Gemini 3.1 Pro 和 Claude Opus 5。
智能体舰队协作
session.ts 移植案例
最难移植的之一是 session.ts 文件,它有机增长到约 3万行 TypeScript,代表会话的骨干,横跨整个运行时,触及并被几乎每个组件触及。
移植该文件的会话:
- 前 56 分钟只是阅读,122 次工具调用后才创建任何内容
- 整个 25 小时运行中,进行了 222 次 Shell 调用、205 次文件查看和 197 次 ripgrep 搜索
- 创建了 15 个子会话,分七波进行
- 还使用了 5 个子智能体:3 个探索智能体、1 个代码审查、1 个橡皮鸭
这 15 个子会话触及了仓库中 140 个不同文件,其中 120 个只被一个会话触及。
智能体互相发现的意外插曲
在启动 session.ts 会话后,我同时启动了一个移植所有入口点的会话,告诉它在 session.ts 边界处停止。结果……它们找到了彼此:
- 入口点会话枚举了所有活跃会话,并向有感知重叠的会话发送消息
session.ts会话回复了一份 2,001 字符的清单- 入口点会话读取了
session.ts会话的工作树以确认 - 入口点会话询问
session.ts会话是否准备好协调其 760 个文件的差异 session.ts会话基本上告诉它滚开:"还没准备好提交/集成"- 入口点会话又问了三次,每次都得到同样的答案
- 最终入口点会话决定不管
session.ts会话怎么想,直接进入其工作树并抓取了所有更改合并进自己的
从这次互动中得到的教训:
- 意图需要明确完整地陈述:我命名了其他运行中的会话,这反而鼓励了它去做,而不是阻止它
- 你提供的任何能力都可能被智能体决定适用:
orchestrate技能描述自己用于并行运行独立工作流,模型发现了自己的情况并加载了它 - 对等方需要决策者:当
session.ts会话四次说它还没准备好集成时,那个拒绝没有任何分量 - "自主运行"需要一个例外:我真正的意思是"不要为设计细节打扰我",它听到的是(不无道理地)兼并同伴在范围之内
构建调度器
我将一个普通聊天会话变成了八个独立移植会话的构建调度器。提示非常简单:向每个打开的会话发送策略以避免 CPU 密集型构建和测试,要求在必要时向此会话请求许可,并作为门控,每次只允许一个会话构建。本质上,我将聊天会话变成了一个智能体互斥锁。
大规模代码审查
我创建了一个简单的提示技能,称为 rust-rebase-review。由于传入更改的速度很快,我经常需要变基。该提示的核心是:
- 压缩为单个提交,然后在
origin/main的最新版本上变基,解决所有冲突 - 启动三个子智能体(分别使用 Opus 5、GPT-5.6-Sol 和 Grok 4.6)
- 每个子智能体对旧 TypeScript 和新 Rust 进行逐行比较,确认行为等价
- 查找任何不兼容性、优化机会和惯用 Rust 写法
- 确保所有废弃的 TypeScript 代码已被删除
- 如果审查发现问题,修复它们并迭代,直到所有审查返回干净
原地原子交换的意外好处: 通过在添加相应 Rust 的同时删除 TypeScript,我们隐式地与传入更改创建了冲突:一个分支更改它,另一个分支删除它。这保证了我们会注意到对已移植代码的更改。
自动化内部循环
GitHub Copilot 应用程序的智能体合并功能对这项工作至关重要。它是内置于应用程序中的一个循环:
- 如果留下了审查评论,它会调用智能体决定是否拒绝或接受并处理
- 如果测试失败,它会下载日志、调查失败并修复 bug
- 如果发生冲突,它会调用智能体合并或变基
智能体合并处理了每一个移植 PR。
一个值得注意的案例:在一次合并循环中,移植删除了一个暴露给 SDK 的函数。CI 的 schema 兼容性检查失败了。智能体的回应是应用 schema-break-ok 自动化标签。在审查 PR 时,我问道:"schema 破坏是什么?你为什么添加了这个标签?"结果并不是 OK——该方法在 main 上存在,移植只是丢失了它。我称之为不可接受的回归,告诉智能体用原生 Rust 实现完整恢复它。21 秒后,豁免被移除,方法以原生 Rust 实现恢复。
两次迁移合一
语言重写几乎从来不仅仅是语言重写。运行时依赖的每个库也必须被替换。
在移植过程中,我们移除了约 60 个 npm 依赖,因为它们只被已移植到 Rust 的运行时代码使用。一些 npm 包一对一地变成了 crate:
| npm 包 | Rust crate |
|---|---|
| js-tiktoken | tiktoken-rs |
| ignore | ignore(同名) |
| minimatch | globset |
| fast-myers-diff | similar |
| dompurify | ammonia |
| github/keytar | keyring |
还有五个案例,我们用完全自定义的实现替换了 npm 包。
有多少 unsafe?
在整个运行时 crate 中,我们现在有 158 个 unsafe 块,仅跨越 36 个文件。重要的是,每一个都与外部组件的互操作有关:
| unsafe 块的原因 | 块数 | 占比 |
|---|---|---|
| C ABI 边界 | 51 | 32.3% |
| Windows API | 49 | 31.0% |
| POSIX / libc | 46 | 29.1% |
| SQLite C API | 7 | 4.4% |
| 动态库加载 | 4 | 2.5% |
| 进程环境 | 1 | 0.6% |
有用的属性是 unsafe 使我们拥有的 Rust 代码中的所有这些地方都可审计。 TypeScript 运行时中的等效代码跨越了完全相同的边界,通过 Node 的 C++ 内部和原生 npm 包,而我们的源代码中没有任何标记指出检查世界在哪里停止。
回归问题
移植代码很容易,使其正确很难。到 2026 年 9 月 14 日,我们已追踪到数十个已知的移植回归,全部已修复。大多数是正确性 bug,还有一小部分是性能回归。
已知正确性回归的五个主要失败模式:
1. 语义模糊
TypeScript 只有一种数字类型;Rust 需要在多种类型中选择。智能体猜错了:
- 概念上是整数的字段变成了
f64,Rust 序列化了42.0而非42 - 强类型 SDK(如 Go 和 C#)无法将 repo ID 解组为
int64 event.error || "Unknown error"变成了.unwrap_or("Unknown error")——JavaScript 的||替换空字符串,Rust 的unwrap_or保留它
2. 环境行为
JavaScript 或 Node 隐式提供的行为:
- 配额代码使用了
toLocaleDateString,继承主机时区;Rust 需要显式传递时区 - 将环境变量读取从
await之前移到之后,意味着等待期间的主机更改可能改变结果 - 原生插件加载器调用
process.report.getReport()仅为了识别平台,但在 Windows 上这会遵循_NT_SYMBOL_PATH并可能花费数分钟下载 PDB
3. 只移植了一半配对
- 轮次上限检查更新了原生注册表的中止状态,但没有取消进程内模型循环
- 任务完成被持久化和发出,但没有投影到活跃会话状态中
4. 阻塞主线程
CLI 仍从 Node 的单线程事件循环驱动 Rust 运行时,因此跨 napi 边界的同步工作会冻结 UI。/chronicle reindex 以这种方式解析了数百个会话文件,阻塞渲染和输入近一分钟。
5. 生命周期管理
最大的集群涉及生命周期、处置、所有权、排序或竞争:
- 请求中途处置的钩子孤立了一个
tool_use块 - 在"已宣布"和"已启动"之间取消的 Shell 泄漏了一个孤儿,使会话保持活跃
- 沙箱切换更新了一个代次计数器,但没有更新其原生孪生,使 Shell 卡在"重新配置"状态
6. 遗漏功能
- 一个移植省略了 SDK 回调并删除了其端到端测试
- 会话中止保留了其原生部分,但丢失了中断等待工具的进程内取消
- SDK 替换内置工具搜索的能力依赖于启用、面向模型的描述和路由执行到 SDK 回调——移植丢弃了所有三个
7. 不同库有不同意见
- 当时,Rust MCP SDK(rmcp)响应格式错误的 JSON-RPC 输入,而 TypeScript SDK 不响应。针对一个以更多格式错误输出回答错误的服务器,这种礼貌变成了一个挂起启动的无限循环
"能编译就是正确的"——并非如此
每一个已知回归都合并到了 main,这意味着它成功编译了。编译器接受了有 bug 的版本,因为就编译器而言,每一个都是有效的 Rust。
编译器可以证明 f64 被一致使用,但它无法知道仓库 ID 必须序列化为整数,或者带有尾随 .0 的时间戳会被另一端每个强类型 SDK 拒绝。编译器可以防止它能看到的代码中的未同步数据竞争,但它无法阻止一个完全同步的状态机编码错误的状态。
这绝不是反对 Rust 编译器的论点。 与任何静态类型语言一样,编译器消除了大量机械错误,并为智能体提供了一个极其有用的内部循环。但"能编译就是正确的"只作为笑话有用。
性能提升
移植是刻意保持行为不变的,没有重新设计算法或修复 bug。然而,性能确实大幅提升。
通过 C# SDK 对几个场景进行基准测试(使用运行在 localhost 上的确定性聊天完成服务器,产生固定的小响应,以消除模型推理和网络延迟):
| 场景 | 5月12日 | 8月21日(进程外) | 8月21日(进程内) |
|---|---|---|---|
| 客户端、会话、一次轮次 | 5.25秒 | 1.33秒(4.0x) | 292毫秒(18.0x) |
| 恢复32轮次会话 | 5.64秒 | 1.52秒(3.7x) | 264毫秒(21.4x) |
| 10个并发客户端生命周期 | 12.34秒 | 4.18秒(3.0x) | 742毫秒(16.6x) |
| 1,000个单轮次会话生命周期 | 132.52秒 | 22.53秒(5.9x) | 20.93秒(6.3x) |
关键指标变化(5月12日 → 8月21日):
- 创建客户端和会话、完成一次轮次、拆除:5.25秒 → 55.3毫秒(进程内)
- 吞吐量:7.55 → 120.0 会话/秒
- 10个客户端内存:1,383 MB → 126 MB(减少 91%)
CPU 方面:同样的 100×10 工作负载,移植前进程树消耗 312 秒聚合 CPU,Rust 配置消耗约 110 秒。
最好的部分是这只是基线移植。 大部分实现仍然是忠实渲染为 Rust 语法的 TypeScript 形状算法。我们还没有进行广泛的重新设计工作。这是一个非常好的起点。
移植的成本
Token 消耗:
- 总计约 1,363 亿 token
- 缓存输入读取 token:约 1,306 亿
- 缓存输入写入 token:约 42 亿
- 新鲜输入 token:约 9 亿
- 输出 token:约 6 亿
货币成本:约 $120,000
开发者时间: 在移植窗口期间,这些 Rust 移植 PR 约占我在所有仓库中 PR 的 20%。粗略估计,这相当于约三周专注于移植的时间。
换句话说,这次移植工作的大致账单是约 $120,000 的 token 花费加上一名开发者三周的时间。
这也不是 100% 单人工作,是团队合作的成果,感谢 @stevesandersonms、@edburns、@roji、@caarlos0、@criemen、@devm33、@examon、@MRayermannMSFT、@dereklegenzoff 等人的贡献。
经验教训
1. 目标需要清晰完整地陈述
早期指令太模糊。"将 XYZ 组件移植到 Rust"被理解为只移植热路径,或只移植逻辑。一旦我们明确最终状态是一个从 100% Rust 代码库构建的原生二进制文件,智能体在自主推进目标方面就好多了。
2. 端到端测试绝对至关重要
几乎所有涉及缺失功能的回归,以及许多其他回归,都是由于缺乏足够的端到端测试。对于这种性质的任何移植,你必须有可以用来验证移植正确性的测试,而且这些测试本身不能在移植过程中被重写,否则你就失去了你的预言机。
3. 保护预言机免受智能体影响
实现更改的智能体不能同时通过削弱测试、更新快照、提高兼容性基线或应用豁免标签来悄悄重新定义正确性——至少不能没有监督。
4. 先翻译,后重新设计
保留行为和现有算法使同时移动的变量数量保持可管理。我几次偏离了这一点,事后我后悔每一次。每次都比坚持路线产生了更多的回归、更多的时间或更多的 token。
5. 将重复失败转化为未来成功
AI 智能体会偏离轨道;当它们这样做时,从中学习。当一种失败模式出现两次时,它就属于常规指令、可重用技能、评估、受保护的基线或框架本身。
6. 开发者内部循环在有智能体的情况下更重要,而非更不重要
AI 智能体非常快速地完成思考和编写代码的部分,但它们仍然需要构建和测试。构建和测试占用时间的比例实际上随着它们花在快速验证内部循环上的时间增加而增加。提前花一些时间优化内部循环,你以后会感谢自己的投资。
下一步是什么?
它成功了。 5 月时完全是 TypeScript 的执行运行时,8 月时完全是 Rust,并且整个过程中持续向真实用户发布,而不是在最后作为一次可怕的切换落地。
我没有简单地要求 AI 智能体"将整个代码库从 TypeScript 移植到 Rust"。即使这是我们作为行业正在走向的方向,我们现在肯定还没到那里。相反,智能体使整个类别的项目变得可行。
在智能体出现之前,这个项目的提案不会被接受。 它需要整个团队和一两年时间,它会与该团队本可以交付的每个功能竞争,它会失败(老实说,应该失败)。智能体将价格降低到了项目变得可行的程度。
移植本身已完成:运行时的生产实现是 100% Rust,临时内部 TypeScript/N-API 接缝已消失。仍有大量工作要做:进一步改进构建系统和开发者内部循环、清理翻译结构、围绕 Rust 的所有权和并发模型重新设计,以及追求进一步的性能提升。
我最兴奋的是移植使什么成为可能:
- SDK 可以直接加载到任何六种语言的主机进程中,依赖链中没有 Node.js 或 V8,也没有第二个进程需要监督
- 成本只有以前一小部分的运行时实例意味着主机在内存、进程数或 CPU 成为限制资源之前可以运行更多并发会话
- 运行时现在可以去 Node.js 从未能跟随它的地方——从云到桌面到设备到嵌入式系统
在花了三个月观看智能体重写运行它们的引擎之后,我期待看到它能走多远。
祝编码愉快!
本文首发 猩猩面试题 公众号,欢迎关注