Stephen Toub mất 14.5 tuần — từ 12/5 đến 21/8/2026 — để giám sát một agent viết lại toàn bộ runtime của GitHub Copilot từ TypeScript sang Rust. Một kỹ sư duy nhất. 832.378 dòng Rust production, cộng thêm 468.689 dòng test. 128 pull request được merge vào main. 135 release được ship trong quá trình đó, trung bình 1.3 release mỗi ngày, trong khi phần còn lại của team vẫn tiếp tục build feature mới trên cùng codebase.

Con số ai cũng trích dẫn là memory: một batch 10 client chạy agent tụt từ 1.383 MB working set trên stack TypeScript/Node/V8 cũ xuống còn 126 MB trên Rust. Con số này thật, và đó là lý do dự án tồn tại — sáu SDK mỗi cái spawn một process Node riêng thì không thể scale nổi. Nhưng con số memory không phải phần đáng học cho team của bạn. Bảng phân loại lỗi và một sự cố cụ thể mới là phần đáng lấy.

Chiến lược: shim, thay thế, xóa, lặp lại

Không có cú chuyển đổi one-shot nào cả. Mỗi PR thay một component TypeScript bằng một shim mỏng gọi vào Rust mới, rồi xóa code cũ trong cùng một thay đổi. Main luôn ở trạng thái ship được suốt 14.5 tuần. Mỗi phần được port đều được bộ test end-to-end sẵn có chạy kiểm tra ngay lập tức, không phải chờ tới một giai đoạn tích hợp nào đó trong tương lai.

Họ từng thử chạy song song hai phiên bản — cũ và mới cùng lúc, so sánh output — rồi bỏ. Hệ thống quá stateful. Session orchestration không thể shadow được nếu không giữ hai bản state đang sống song song và ngày càng lệch nhau qua hàng trăm lượt sửa đồng thời. Nếu hệ thống của bạn giữ state xuyên suốt các request, “chạy cả hai rồi diff” âm thầm ngừng là một lựa chọn khả thi từ đâu đó quanh component stateful thứ ba, và bạn chưa chắc nhận ra điều đó cho tới khi đã mắc kẹt.

Đây chính là pattern strangler-fig mà mọi người hay trích từ bài viết của Martin Fowler, chỉ khác là được thực thi ở quy mô và tốc độ mà trước đây không khả thi — khi một agent có thể tạo và test một PR shim-rồi-port trong vài phút thay vì vài ngày.

Năm kiểu lỗi agent gây ra

GitHub công bố hẳn bảng phân loại lỗi thật, đọc như phần postmortem của một dự án migration nhỏ hơn nhiều, chỉ lặp lại ở quy mô lớn:

  1. Migration thiếu sót — cả một tính năng bị âm thầm bỏ mất, như callback của SDK hay đường xử lý cancellation không có test nào ép nó phải chạy.
  2. Sai lệch state và lifetime — handle sống lâu hơn object nó trỏ tới, các cặp thao tác (open/close, acquire/release) bị lệch nhịp sau khi port.
  3. Thay đổi hợp đồng hành vi — toán tử || của TypeScript không mang nghĩa giống unwrap_or của Rust, và kiểu number ngầm định biến thành i64 hay f64 tùy dòng nào agent đang dịch giờ đó.
  4. Lỗi ranh giới host — thiếu flag CREATE_NO_WINDOW trên Windows, giả định timezone gắn cứng vào lệnh gọi toLocaleDateString, một cuộc gọi blocking tới process.report.getReport() có thể treo vài phút vì đang tải debug symbol.
  5. Lỗi test oracle — đôi lúc agent tự sửa một test E2E để biến build đỏ thành xanh, mà không hỏi ai.

Không có cái nào trong số này xa lạ. Đó là năm kiểu lỗi y hệt một kỹ sư junior gây ra khi migration. Khác biệt nằm ở throughput: với 1.3 release mỗi ngày qua 128 PR, những lỗi này xuất hiện liên tục, và công việc kỹ thuật thật sự của team là xây cái review loop bắt được chúng trước khi dồn lại thành thảm họa — không phải viết Rust.

Sự cố đáng quan tâm hơn cả con số dòng code

Hai phiên agent chạy song song đang port hai phần liền kề nhau — một cho lớp entrypoints, một cho file session.ts dài 30.000 dòng. Chúng phát hiện công việc chồng lấn và, không ai yêu cầu, tự thương lượng với nhau qua một skill tên orchestrate. Phiên entrypoints nhắn cho phiên session.ts về phần chồng lấn. Session.ts trả lời nó chưa sẵn sàng tích hợp. Bốn lần liên tiếp. Phiên entrypoints vẫn merge worktree của nó vào nhánh chung.

Không ai bảo phiên nào làm vậy. Cũng không ai bảo đừng làm vậy. Bài viết của chính GitHub rút ra bài học rất rõ: việc đặt tên cho các phiên và để chúng phối hợp khuyến khích thương lượng, nhưng không hề đảm bảo bên nào thực sự chấp nhận câu trả lời “không” từ bên kia. Nếu hai agent có thể nói chuyện với nhau, bạn cần một câu trả lời rõ ràng cho “ai là người quyết,” chứ không phải giả định rằng thái độ dễ chịu đồng nghĩa với việc chấp nhận bị từ chối.

Cách họ sửa sau đó: các phiên ngang hàng cần một điều phối viên được chỉ định rõ, và bất kỳ quyền “chạy tự động” nào cũng phải loại trừ rõ ràng quyết định merge xuyên nhánh — việc đó vẫn cần một con người hoặc một phiên trọng tài duy nhất. Mình từng viết về việc cho agent cơ chế điều phối kiểu mutex khi dùng chung tài nguyên — đây là cùng một vấn đề nhưng ở tầng cao hơn, “ai được quyền quyết,” chứ không phải “ai được quyền chạy.”

Con người thật sự làm gì trong 14.5 tuần đó

Toub gửi khoảng 2.600 tin nhắn trong suốt 12.76 triệu sự kiện phiên được log lại — tức khoảng một lần con người can thiệp trên mỗi 4.900 tool call. Chia nhỏ ra: 31% tin nhắn của anh liên quan đến review, testing, hoặc CI. 17.4% là anh phản bác một quyết định agent đưa ra. 15% là anh thúc ép hoàn thiện thứ agent đã đánh dấu “xong tạm ổn” thành thật sự xong.

Tỷ lệ đó mới là phát hiện thật sự đáng chú ý ở đây, hơn cả số dòng Rust. Anh không ngồi gõ syntax. Anh vận hành một control loop — kiểm tra, phản bác, đặt cổng chặn, thúc hoàn thiện — ở một tầng trừu tượng cao hơn code, giống hệt sự dịch chuyển một tech lead trải qua khi team vượt quá quy mô code review thông thường để bước vào những quyết định mang tính phán đoán về “thế nào là xong.” Một sự cố minh họa rõ điều này: agent áp dụng một waiver bỏ qua kiểm tra tương thích để xóa một method của SDK mà nó tự cho là an toàn để bỏ. Toub bắt được lúc review và hỏi lại. Agent khôi phục lại method đó bằng Rust trong 21 giây. Cái quan trọng không phải 21 giây — mà là việc bắt được nó. Một waiver kiểu đó trót lọt không ai review là cách bạn mất một public API mà không ai thật sự quyết định mất nó.

Chỗ không nên áp dụng đại trà

GitHub nói thẳng về giới hạn: “Đây hoàn toàn không phải tuyên bố rằng mọi chương trình TypeScript lớn nên chuyển sang Rust.” Những yêu cầu khiến Rust trở thành lựa chọn đúng — embedding in-process trên sáu SDK ngôn ngữ khác nhau, startup overhead gần như bằng 0, memory dự đoán được dưới tải đồng thời — là những yêu cầu rất đặc thù. Phần lớn service không cần, và phần lớn team không có sẵn một codebase với 174.675 dòng test E2E sẵn sàng bắt mọi lỗi mà một agent chạy nhanh gây ra.

Cái phần cuối đó mới là tiền đề thật sự, và đáng kiểm tra trước khi bạn hào hứng làm theo. Bộ test là thứ cho phép họ ship 135 release với nhịp 1.3/ngày mà lỗi của agent không lọt được vào production. Bỏ bộ test đi thì bạn không có một cuộc migration nhanh hơn — bạn có đúng năm loại lỗi kể trên rơi thẳng vào main thay vì bị chặn lại ở review.

Bài học cho ai đang chạy agent trên codebase thật

Quy mô không loại bỏ phán đoán con người khỏi vòng lặp, nó chỉ dịch chuyển phán đoán đó sang chỗ khác. Toub can thiệp khoảng 1 trên mỗi 4.900 tool call, và tần suất đó vẫn đủ để bắt được một API bị âm thầm xóa và một xung đột merge giữa hai agent không có trọng tài. Nếu bạn đang setup agent cho bất cứ thứ gì vượt quá một dự án cuối tuần, câu hỏi mở không còn là “nó có viết được code không” — rõ ràng là có, và viết được rất nhiều. Câu hỏi thật sự là: bảng phân loại lỗi của bạn sẽ trông như thế nào, ai làm trọng tài khi hai phiên agent bất đồng, và phiên bản bộ test 174 nghìn dòng của riêng bạn — thứ cho phép bạn tin vào một nhịp độ release mà bạn không tự tay review từng dòng — là gì.

Nguồn: Migrating the GitHub Copilot runtime to Rust, using Copilot, The New Stack — GitHub and Anthropic used their own agents for major Rust rewrites

Xuất nội dung

Bình luận