Thủ thuật
Tối ưu hóa mã nguồn AI: Bí quyết giảm chi phí token hiệu quả từ CTO Thoughtworks
(giờ Việt Nam)
Tóm tắt AI
CTO của Thoughtworks chứng minh rằng việc tái cấu trúc mã nguồn giúp giảm đáng kể lượng token tiêu thụ. Thử nghiệm trên ứng dụng Rust cho thấy việc tinh gọn tệp tin giúp giảm hơn 80% chi phí đầu vào cho AI.
Bản dịch AI

Để làm quen với thế giới mới của kỹ thuật tác tử (agentic engineering), tôi đã xây dựng một ứng dụng để hỗ trợ công việc của mình. Đây là một ứng dụng phức tạp: giao diện web chất lượng cao với khả năng làm mới và tra cứu động, các modal và tính năng tự động lưu, tích hợp với các hệ thống bên ngoài, học máy và phân tích văn bản, các tác vụ chạy ngầm, cùng một thiết lập môi trường hoàn chỉnh với quy trình triển khai tự động hóa hoàn toàn. Ứng dụng này có khoảng 150.000 dòng mã, chủ yếu viết bằng Rust (~120 kLoC), phần còn lại là TypeScript và Terraform.
Ứng dụng này được viết hoàn toàn bởi các tác tử (agents). Chủ yếu là Claude Code, và có sử dụng thêm Cursor. Tôi không đọc hay xem xét bất kỳ đoạn mã nào, ngoại trừ đôi khi xem qua vì tò mò.
Trong quá trình xây dựng ứng dụng, tôi nhận thấy một vài thứ đang đi chệch hướng. Sau khi theo dõi một chỉnh sửa ở dòng 4.000 của một tệp tin lướt qua trên terminal, tôi đã kiểm tra kỹ hơn. Lớp truy cập dữ liệu (data access layer) đã phình to lên hơn 6.000 dòng. Khi có thêm nhiều tính năng mới, con số này tiếp tục tăng. Mọi truy vấn, đọc hoặc ghi, đều lặp lại cùng một thiết lập yêu cầu HTTP, cùng một cách mã hóa và giải mã JSON. Cuối cùng, nó đạt tới 17.155 dòng. Trong một tệp Rust duy nhất.
Một thử nghiệm về tái cấu trúc (refactoring)
Tệp tin 17.155 dòng đó chính là toàn bộ lớp truy cập dữ liệu. Một module đơn lẻ, khép kín. Khi xem xét mã nguồn, không hề có sự khử trùng lặp (de-duplication), không có ngôn ngữ nội bộ, việc trích xuất hàm bị hạn chế và rất ít khi trích xuất lớp (class). Tuy nhiên, nó có một ranh giới rõ ràng với một giao diện cần được bảo toàn. Đây là một mục tiêu tuyệt vời để tái cấu trúc.
Mục tiêu của việc tái cấu trúc một cơ sở mã (code base) do tác tử tạo ra là chi tiêu token ngay bây giờ để giảm mức tiêu thụ token cho các công việc trong tương lai. Một thử nghiệm cần phải chứng minh được rằng khi tệp tin này được tái cấu trúc, chi phí token để thực hiện các tính năng riêng biệt trong cơ sở mã này sẽ giảm xuống.
Chính vì các tác tử không bao giờ học hỏi, nên việc chạy thử nghiệm này là khả thi. Tôi có thể yêu cầu một tác tử mới thực hiện chính xác cùng một thay đổi sau mỗi giai đoạn tái cấu trúc. Không giống như một kỹ sư con người, thử nghiệm sẽ không bị ảnh hưởng bởi việc học hỏi từ các bước trước đó.
Lời nhắc (prompt) được sử dụng cho thay đổi đại diện và các bước tái cấu trúc được áp dụng được hiển thị trong các phụ lục bên dưới.
Một lưu ý nhỏ: Claude không cung cấp các phương pháp đáng tin cậy để đếm token trực tiếp mặc dù có hiển thị số lượng token, báo cáo token tiêu thụ mỗi phiên và tính phí theo token. Tôi cho rằng đây là vấn đề tạm thời và sẽ được cải thiện theo thời gian. Thay vào đó, tác tử phụ đã báo cáo số lượng ký tự được gửi và nhận, đồng thời sử dụng tiktoken để ước tính số lượng token bằng cách chia số ký tự cho bốn.
Kết quả
Các chỉ số thú vị ở đây là tổng số dòng mã trong lớp truy cập dữ liệu, tổng số dòng mã trong tệp đơn lẻ lớn nhất trong lớp truy cập dữ liệu và số lượng token đầu vào tiêu thụ khi thực hiện thay đổi.
Biểu đồ này cho thấy bốn yếu tố. Điểm đầu tiên là đường cơ sở (baseline), bước 0, và sau đó các chỉ số tương tự được lặp lại sau mỗi bước tái cấu trúc được áp dụng.
Tái cấu trúc giúp giảm tiêu thụ token
Kết quả rất rõ ràng. Token đầu vào duy trì ở mức khá ổn định cho đến khi tệp lớn nhất bắt đầu giảm kích thước, và sau đó chúng giảm xuống, theo cách nói của Claude là "rơi xuống vực thẳm".
Giữa đường cơ sở và lần tái cấu trúc cuối cùng, token đầu vào cho cùng một tác vụ đã giảm từ 159.564 xuống còn 27.360. Tiết kiệm được 132.204 token, tương đương 83%. Và khoản tiết kiệm đó không phải là một lần duy nhất. Mọi thay đổi chạm đến lớp truy cập dữ liệu từ thời điểm này trở đi đều tốn ít chi phí hơn đáng kể.
Tiết kiệm được bao nhiêu? Giả sử giá của Sonnet 5 tại thời điểm viết bài là $3/MTok, thì là 39,7 cent. Không nhiều lắm. Liệu nó có nhân lên không? Điều này sẽ diễn ra như thế nào trong quá trình gỡ lỗi? Các tính năng phức tạp hơn thì sao? Đây chỉ là tái cấu trúc một phần của cơ sở mã, liệu toàn bộ cơ sở mã có thể được tái cấu trúc mạnh mẽ để tìm kiếm sự tiết kiệm ở mọi nơi không? Những lần tái cấu trúc đó sẽ tốn bao nhiêu chi phí?
Khoản tiết kiệm này có được là do tác tử phải đọc ít mã hơn. Nhưng không phải vì có ít mã để đọc hơn. Tổng thể mã trong lớp truy cập dữ liệu vẫn khá ổn định. Do đó, để có thể đạt được khoản tiết kiệm này, tác tử phải có khả năng xác định chính xác tập hợp con nhỏ nhất các tệp cần đọc. Kết quả cho thấy điều này đã xảy ra. Việc đọc đầu ra suy nghĩ (thinking output) và tóm tắt tệp tin được đọc của Claude Code khi thay đổi được áp dụng cũng cho thấy tác tử phụ đã đọc thành công các phần mã ngày càng nhỏ hơn qua mỗi lần.
Nói cách khác, việc cắt tệp tin một cách ngẫu nhiên thành các tệp nhỏ hơn khó có thể mang lại hiệu quả tương tự: ngay cả khi mỗi tệp nhỏ hơn, tác tử vẫn buộc phải đọc qua nhiều tệp để tìm kiếm mã liên quan. Mặc dù bước có hiệu quả lớn nhất xảy ra ở cuối, nhưng các bước trước đó là những bước tái cấu trúc để tạo tiền đề cho khoản tiết kiệm này. Điều này không được lên kế hoạch trước. Nó chỉ đơn giản là kết quả của cách thức tái cấu trúc thường diễn ra: thay đổi tệp cục bộ để khử trùng lặp, trước khi chia nhỏ thành các tệp nhỏ hơn một khi lõi lặp lại xuất hiện.
Việc tái cấu trúc không làm cho thay đổi đại diện nhỏ hơn. Số lượng token được tạo ra khi viết mã hầu như không bị ảnh hưởng: token đầu ra không thay đổi nhiều. Những token đó có giá gấp năm lần token đầu vào. Tuy nhiên, số lượng của chúng ít hơn nhiều. Liệu có những cách tái cấu trúc nào có thể được áp dụng để giảm việc sản xuất token đầu ra không? Tôi cần một thay đổi mẫu phức tạp hơn để khám phá những câu hỏi này. Nhiễu từ quá trình tạo mã không xác định (non-deterministic) đang che khuất bất kỳ sự khác biệt nào gây ra bởi những thay đổi trong cách cấu trúc mã.
Ghi chú về quy trình
Claude không giỏi trong việc tái cấu trúc. Nếu bạn đọc lời nhắc và các bước tái cấu trúc bên dưới, rõ ràng là các bước tái cấu trúc được tạo ra là phản ứng trực tiếp với lời nhắc. Claude không thể nhìn vào mã, nhìn vào các phương pháp tái cấu trúc nói chung và tự tìm ra phương pháp nào phù hợp để áp dụng: con người cần phải chủ động hướng dẫn nó. Điều này phù hợp với kinh nghiệm rộng hơn trong ứng dụng này. Bộ công cụ phát triển bao gồm một bước tái cấu trúc rõ ràng. Bước tái cấu trúc đó đã không thúc đẩy Claude cải thiện tệp này. Một cách không chính thức, Claude.ai tốt hơn Claude Code. Tôi đã sử dụng cả hai giao diện để tạo kế hoạch tái cấu trúc. Claude Code đã phát hiện ra việc trích xuất hàm là bước đầu tiên. Claude.ai đã đi xa hơn và thấy cả một lớp client cần được trích xuất.
Nó cũng làm không tốt việc áp dụng các bước đó. Hành động tái cấu trúc cơ học được thực hiện bằng cách viết các tập lệnh Python sử dụng grep và sed. Các tập lệnh này thường bị nhầm lẫn bởi thụt lề. Ồ, thật trớ trêu. Ngoài ra, bước tái cấu trúc có giá trị nhất đã bị bỏ lỡ trong lần đầu tiên và phải được áp dụng lại như một bước tiếp theo. Đây là lý do tại sao số lượng các bước trong hình không khớp với các bước tái cấu trúc trong phụ lục.
Mất khoảng tám giờ để hoàn thành toàn bộ thử nghiệm. Quá trình này hầu như không cần giám sát. Sự can thiệp duy nhất là sau 6 giờ 40 phút khi nó có vẻ đã hoàn thành, nhưng lại bỏ qua bước đó và cần được điều hướng lại. Thử nghiệm này chạy trên WiFi khách sạn chậm. Tôi tự hỏi liệu điều đó có góp phần vào thời gian thực hiện hay không. Nhưng khi phân tích sâu hơn về cơ sở mã, bộ nhớ đệm xây dựng tạm thời (cargo temporary build cache) đã trở nên rất lớn. Việc thực thi kiểm thử đã bị ảnh hưởng đáng kể.
Công việc tiếp theo và các tác động rộng hơn
Thật không may, tôi đã không nghĩ đến việc đếm số lượng token cần thiết để tạo và thực hiện kế hoạch tái cấu trúc cho đến khi nó đã hoàn thành. Tôi đã xem xét mức tiêu thụ tổng hợp của mình trong khoảng thời gian tôi thực hiện công việc này, bao gồm cả việc thiết kế và chạy thử nghiệm. Tôi không thể nói chính xác cần bao nhiêu token để thực hiện việc tái cấu trúc. Tuy nhiên, giới hạn trên là năm triệu. Con số này bao gồm việc tạo kế hoạch tái cấu trúc hai lần, công việc thiết kế thử nghiệm bao gồm cả thay đổi đại diện và các tác vụ khác. Công việc trong tương lai nên bao gồm việc đếm chính xác hơn số lượng token tiêu thụ để tái cấu trúc.
Đây chỉ là một thử nghiệm trên một ứng dụng quan trọng vẫn còn ở giai đoạn sơ khai (greenfield) và được xây dựng, duy trì bởi một nhà phát triển duy nhất. Tuy nhiên, tôi tin rằng đây là bước đầu tiên thú vị. Nỗ lực này cho thấy giá trị về thời gian và tiền bạc của việc tái cấu trúc. Cũng như việc đo lường mức độ đắt đỏ của việc tái cấu trúc. Sẽ rất thú vị khi xem xét các thay đổi phức tạp hơn, tái cấu trúc trên phạm vi rộng hơn, tái cấu trúc liên tục và thậm chí là giá trị tương đối của các phương pháp tái cấu trúc khác nhau.
Đây mới chỉ là sự khởi đầu.
Phụ lục
Lưu ý: Các phụ lục này bao gồm các lời nhắc mà tôi đã sử dụng và đầu ra được trả về. Việc chỉnh sửa duy nhất được áp dụng là xóa các thay đổi mã cụ thể cần thực hiện. Chúng được bao gồm mà không cần chỉnh sửa để cho thấy cách các tác tử được chỉ đạo. Không có thủ thuật ẩn nào. Do đó, có một số ngôn ngữ ở đây có thể gây nhầm lẫn. Lỗi nằm ở bản gốc.
Đây là lời nhắc đã ghi lại được gửi đến từng tác tử phụ, không có ngữ cảnh nào khác được cung cấp ngoài cơ sở mã và tài liệu kiến trúc đi kèm. Mọi tác tử phụ đều bắt đầu với cùng một thông tin.
Bạn đang làm việc trong dự án Rust tại ~/dev/your-project-name.
Thêm một trait công khai async mới là ItemWatchStore vào lớp Firestore, tuân thủ chính xác các mẫu hiện có. Trait phải có ba phương thức:
Các "Watch" được lưu trữ trong bộ sưu tập Firestore có tên là item_watches. Mỗi tài liệu có các trường: itemId (chuỗi), userId (chuỗi), createdAt (dấu thời gian). Không có cấu trúc Rust nào cho bản ghi watch — các phương thức trả về Vec<String> (các id mục).
Triển khai trait cho cả FakeStore (sử dụng trường Vec<(String, String)> trong bộ nhớ được thêm vào FakeStoreInner) và FirestoreStore (sử dụng các mẫu HTTP tương tự được sử dụng cho các triển khai lưu trữ khác trong tệp này).
Ở cuối phản hồi của bạn, hãy xuất chính xác khối JSON này (điền các giá trị thực):
KHÔNG commit thay đổi. Dừng lại sau khi viết mã.
Đây là lời nhắc được sử dụng để tạo kế hoạch tái cấu trúc.
Theo định nghĩa nghiêm ngặt rằng tái cấu trúc là một chuỗi các chỉnh sửa mã có thể chứng minh được là bảo toàn tính đúng đắn, và sử dụng cuốn "Refactoring" ấn bản thứ 2 của Martin Fowler làm nguồn, hãy kiểm tra @src/firestore.rs. Đây là tệp Rust dài 17K dòng. Không tệp nào nên dài như vậy. Gần như chắc chắn nó không sử dụng ngôn ngữ nội bộ để xây dựng và quản lý các truy vấn. Hãy tạo và mô tả, nhưng đừng thực thi, một chuỗi các bước tái cấu trúc sẽ làm giảm đáng kể số dòng của tệp đó mà không làm thay đổi giao diện.
Sau đây là mô tả về các bước tái cấu trúc đã áp dụng, được trích xuất từ kế hoạch do Claude xây dựng và tuân theo. Kế hoạch thực tế bao gồm các thay đổi mã được dự đoán. Đối với mỗi lần tái cấu trúc, các bước riêng lẻ cần thực hiện đã được liệt kê. Mỗi bước đó đều có thể kiểm thử riêng lẻ và đã được kiểm thử riêng lẻ. Đây là một quy trình tái cấu trúc nghiêm ngặt hơn hầu hết các kỹ sư con người sẽ thực hiện.
Bài viết được AI dịch và tổng hợp tự động từ Hacker News Nổi bật (buzzing.cc bản dịch tiếng Trung). Liên kết bài gốc ở phía trên. Dữ liệu đồng bộ qua API công khai được ghi nguồn tại AI HOT (canonical) ↗. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.