Tối Ưu Mã Nguồn Được Tạo Bởi AI: Chiến Lược Chống Lại Sự Phình To và Nâng Cao Chất Lượng Code

Khám phá cách tối ưu hóa mã nguồn do AI tạo ra, từ việc nhận diện “mùi” code đến các chiến lược giảm thiểu đáng kể dòng lệnh mà vẫn đảm bảo chức năng.

Sự trỗi dậy của các AI Agent trong phát triển phần mềm đã mở ra một kỷ nguyên mới về năng suất, nhưng cũng đồng thời mang lại những thách thức độc đáo. Một trong số đó là “mùi” đặc trưng của mã nguồn do AI tạo ra: thường quá dài dòng, khó theo dõi, thiếu đi sự rõ ràng và súc tích mà một lập trình viên giàu kinh nghiệm mong đợi. Thậm chí, chỉ cần nhìn vào một tệp README.md được viết bởi AI, ta có thể dễ dàng nhận ra sự “phình to” không cần thiết. Một thử nghiệm gần đây đã cho thấy việc giảm 40% số dòng mã (Lines of Code – SLOC) từ một cơ sở mã do AI phát triển, mà không ảnh hưởng đến chức năng, không chỉ là một trải nghiệm mở mang tầm mắt mà còn cung cấp những bài học quý giá về cách đối phó với hiện tượng “AI Bloat” này. Những bài học đó đã được chắt lọc thành một kỹ năng agent chuyên biệt.

Sự tiến hóa của động cơ Raptor: Biểu đồ minh họa quá trình tinh gọn và tối ưu hóa phức tạp.

Hành Trình Xây Dựng Ứng Dụng Với AI: Ngọt Ngào Và Cay Đắng

Vào mùa thu năm ngoái, tôi bắt đầu xây dựng một ứng dụng Flutter – một trình phát media – hoàn toàn bằng AI. Đây không phải là một kiểu “vibe-coding” thiếu kiểm soát; tôi đã thúc đẩy các agent duy trì tài liệu, tăng cường độ bao phủ kiểm thử tự động, và đầu tư vào các vòng lặp phản hồi (ví dụ: tạo một CLI tiện dụng cho việc điều khiển ứng dụng Flutter). Về cơ bản, ứng dụng này có cấu trúc rõ ràng và có thể chạy cũng như tương tác từ bên ngoài.

Tuy nhiên, tôi cũng không dành nhiều thời gian để đọc code. Nói đúng hơn, việc đọc code cảm giác như mở ra một cánh cổng: một khi bạn bắt đầu nhìn vào, bạn không chỉ “xem xét” nó. Bạn sẽ phát hiện ra những lớp lạ lùng, các bản sửa lỗi nửa vời, những ý tưởng cũ vẫn còn tồn tại trong hệ thống, các bình luận không giải thích gì cả, các lớp trừu tượng được đưa vào cho một vấn đề không còn tồn tại… Và khi đó, lựa chọn trở nên khó khăn: liệu tôi có nên dừng lại và viết lại phần này không? Liệu tôi có nên dành cả cuối tuần để thanh toán món nợ kỹ thuật mà tôi chỉ phát hiện ra vì đã nhìn vào? Vì vậy, tôi đã tiếp tục phát triển mà không đi sâu vào chi tiết.

“Mùi” Mã Nguồn AI: Dấu Hiệu Của Sự Phình To

Ứng dụng vẫn hoạt động, nhưng thường cảm thấy “lởm chởm” (jagged). Các bản sửa lỗi bug thường chỉ là tạm thời, không triệt để. Mỗi lần thêm tính năng mới do AI tạo ra dường như lại làm tăng entropy của toàn hệ thống, ngay cả khi tính năng đó đã được triển khai thành công. Cơ sở mã bắt đầu có cái “mùi” quen thuộc của AI: rất nhiều năng lực cục bộ, rất nhiều sự an toàn có vẻ hợp lý, nhưng ngày càng nhiều thứ mà mục đích của chúng khó có thể cảm nhận được từ bên ngoài.

Tôi có cảm giác rằng cơ sở mã đang phình to. Nhưng tôi không đủ khả năng tinh thần (hoặc sự quan tâm và động lực) để đi sâu vào tìm hiểu – nợ nhận thức (cognitive debt) cứ thế chồng chất.

Thực Nghiệm Giảm Thiểu Mã Nguồn: Kết Quả Đáng Ngạc Nhiên

Quyết định đối mặt với vấn đề này, tôi đã tiến hành một thử nghiệm tinh gọn mã nguồn.

Thước Đo Trước Sau
Mã Ứng Dụng (Dart + Native) 19,772 13,509
Mã Dart (lib/) 15,859 9,924
Kiểm Thử green 335 green

Kết quả thật ấn tượng: giảm 31.7% tổng số dòng mã ứng dụng, đồng thời giữ nguyên tất cả các tính năng, đảm bảo mã sạch theo bộ phân tích (analyzer clean), và kiểm tra thời gian chạy ổn định trên cả trình giả lập Android lẫn bản dựng Linux desktop. Đáng chú ý, hai lỗi tiềm ẩn đã được khắc phục trong quá trình này.

Giới Thiệu /goal-sloc: Công Cụ Kiểm Soát Sự Phình To Của AI

Sự ra mắt chế độ /goal trong Codex/Claude của OpenAI và Anthropic đã khơi gợi một ý tưởng trong tôi: “biến SLOC thành mục tiêu” – liệu đây có phải là một cách “lười biếng” nhưng hiệu quả để loại bỏ những phần không cần thiết trong cơ sở mã của mình?

SLOC (Lines of Code) là một thước đo thô sơ, dễ dàng đo lường… nhưng cũng đầy rủi ro. Tuy nhiên, một thước đo thô sơ vẫn có thể hữu ích nếu nó buộc mô hình phải tìm kiếm sự đơn giản hóa thực sự, thay vì chỉ thêm một lớp giải thích nữa lên trên sự hỗn độn có sẵn. Thực nghiệm đó đã biến thành /goal-sloc, một kỹ năng agent nhỏ cho phép sử dụng số dòng mã làm một hàm ép buộc (forcing function) mà không để agent “gian lận” số liệu.

Chiến Lược Giảm Dòng Mã Hiệu Quả

Những gì thực sự hiệu quả trong quá trình tinh gọn bao gồm:

  • Xóa mã chết (dead code) không còn được sử dụng.
  • Loại bỏ các hệ thống con giữ chỗ (placeholder subsystem) không hoạt động, mặc dù đã được “đấu nối” đầy đủ.
  • Chuyển mã gỡ lỗi (debug harness) ra khỏi mã nguồn sản xuất.
  • Loại bỏ một lớp trạng thái (state layer) thừa thãi.
  • Thực hiện viết lại “trong phòng sạch” (clean-room rewrites) dựa trên các kiểm thử khi chúng đóng vai trò như một đặc tả hành vi tốt.
  • Thay thế mã ghi log tùy chỉnh bằng một thư viện đã trưởng thành và tối ưu hơn.

Những Giới Hạn và Thách Thức Khi Tối Ưu

Một số công việc khác tuy có giá trị nhưng không làm thay đổi đáng kể số dòng mã. Việc sắp xếp lại module sâu sắc, thiết lập các ranh giới tốt hơn, và tái cấu trúc hook/controller có thể cải thiện thiết kế mà vẫn giữ SLOC tương đối ổn định. Điều này phù hợp với quan điểm của Matt Pocock về các module sâu: AI hoạt động tốt hơn khi nó có thể làm việc thông qua các giao diện đơn giản và các ranh giới có thể kiểm thử, thay vì phải “khám phá” qua các module nông, nhiều rò rỉ. Đây là một trong những phát hiện hữu ích: nếu mục tiêu là chất lượng mã, SLOC không thể là phần thưởng duy nhất. Một số công việc kiến trúc tốt nhất không gây ấn tượng trên bộ đếm dòng code.

Ngoài ra, còn có một “ngưỡng cứng” (hard floor). Các dự án Flutter luôn đi kèm với mã được tạo tự động và các khung sườn nền tảng. Một phần trong số đó có thể được giảm nếu là mã native tùy chỉnh. Nhưng phần lớn là ngưỡng không thể giảm: các tệp Gradle, CMake, Xcode, manifest, tài sản nhị phân được tính là dòng mã, và các thư mục nền tảng mà bạn phải hỗ trợ hoặc cắt bỏ theo quyết định sản phẩm.

Bạn có thể đọc chi tiết đầy đủ về thử nghiệm này.

Bản Chất Của Sự Suy Thoái Mã Nguồn Do AI: Vượt Ra Ngoài Lỗi “Ảo Giác”

Ứng dụng đã được xây dựng 100% với sự hỗ trợ của AI. Đóng góp của con người ít hơn ở chỗ “tôi hiểu mọi hệ thống con” mà thiên về “tôi thiết lập khung sườn, viết đặc tả, yêu cầu kiểm thử và liên tục định hướng”. Sự khác biệt này rất quan trọng.

Có một câu chuyện an ủi mà mọi người thường kể về lập trình AI: nếu bạn có các kiểm thử, đặc tả/tài liệu và vòng lặp phản hồi, bạn đang làm đúng. Không phải là Vibe Coding, mà là Agentic Engineering 🕶️… Tôi vẫn tin rằng điều đó phần lớn là đúng. Nhưng nó không có nghĩa là mã nguồn sẽ luôn khỏe mạnh. Nó có nghĩa là mã có thể tiếp tục phát triển trong khi “sức khỏe” của nó âm thầm suy giảm.

Sự suy thoái không phải là một thất bại thảm khốc duy nhất, mà là sự tích lũy của nhiều vấn đề nhỏ:

  • Các tính năng được triển khai với thêm các khung sườn phụ trợ (extra scaffolding) xung quanh.
  • Các bản sửa lỗi bug chỉ giải quyết triệu chứng được báo cáo nhưng bỏ qua những điểm kỳ lạ gần đó.
  • Các bình luận dài dòng tích tụ như thể số lượng bình luận đồng nghĩa với sự rõ ràng.
  • Các hệ thống con không hoạt động (no-op) hoặc chỉ là giữ chỗ vẫn được “đấu nối” vào các mô hình, lớp lưu trữ, giao diện người dùng và các kênh nền tảng.
  • Mã gỡ lỗi và tự động hóa (debug and automation harness code) nằm trong mã nguồn sản xuất.
  • Các lớp trạng thái (state layers) phản chiếu các lớp trạng thái khác vì mô hình đã “học” kiến trúc như một nghi lễ.

Đây chính là mối nguy hiểm đặc biệt của mã nguồn được phát triển bởi AI. Nhìn cận cảnh, nó thường không có vẻ ngớ ngẩn. Mỗi lần bổ sung đều có vẻ hợp lý tại thời điểm đó. Sự phình to đến từ sự tích lũy: mỗi lượt AI agent làm việc lại để lại một sự thỏa hiệp nhỏ cục bộ, một chút “dư lượng” giải thích, một chút trừu tượng phòng thủ. Sau đủ lượt, hệ thống trở nên nặng nề hơn ngay cả khi mỗi bước riêng lẻ đều có vẻ hợp lý – các chế độ lỗi (failure modes) chồng chất.

Sự Tích Lũy Nợ Kỹ Thuật (Technical Debt) Với AI

Bài nói chuyện của Matt Pocock, “Software Fundamentals Matter More Than Ever” (Các Nguyên Tắc Cơ Bản Của Phần Mềm Quan Trọng Hơn Bao Giờ Hết), đã chạm đúng vào điểm đau. Tôi không muốn đi sâu vào code, và chưa bao giờ có đủ can đảm. John Ousterhout định nghĩa sự phức tạp là bất cứ điều gì về cấu trúc của một hệ thống khiến nó khó hiểu và khó sửa đổi. Cuốn “The Pragmatic Programmer” nói về entropy phần mềm: sự thay đổi sau sự thay đổi được thực hiện cục bộ, mà không quan tâm đến thiết kế tổng thể. Quan điểm của Pocock sắc bén hơn: mã nguồn không hề rẻ. Mã nguồn tồi còn đắt hơn trong kỷ nguyên AI vì một cơ sở mã khó thay đổi sẽ cản trở cả bạn và AI agent thực hiện các thay đổi chất lượng.

Tôi rất thích cách nhìn nhận này. Tôi cũng biết rằng mình sẽ không ngồi xuống và thực hiện một cuộc đánh giá kiến trúc anh hùng cho một cơ sở mã mà tôi đã ủy thác một nửa cho máy móc. Tôi muốn một ràng buộc mà tôi có thể ủy thác.

SLOC – Một Thước Đo Đầy Nguy Hiểm Nhưng Hiệu Quả?

Số dòng mã (SLOC) dễ dàng đo lường. Nó cung cấp một mục tiêu cho agent. Nó biến yêu cầu “hãy đơn giản hóa cơ sở mã” từ một lập luận về “gu” thành một trò chơi có bảng điểm. Trong Claude Code, tôi đã thử sử dụng chế độ /goal làm vòng lặp bên ngoài: đặt mục tiêu, để agent làm việc, đo lường và tiếp tục.

Hy vọng ban đầu của tôi là một loại vòng lặp tự động Ralph: agent sẽ tiếp tục làm việc, tự kiểm tra, và cuối cùng trả về một ứng dụng nhỏ hơn nhiều, nhưng vẫn hoạt động. Một cái gì đó gần giống với các thử nghiệm trình biên dịch/tự chủ Claude cũ, nơi bạn quay lại sau và kiểm tra kết quả.

Điều đó đã không xảy ra. Claude Opus 4.8 kiểm tra với tôi quá thường xuyên. Ban đầu, điều đó cảm giác như vòng lặp mục tiêu không hoạt động như tôi mong muốn. Nhìn lại, tôi nghĩ rằng những lần gián đoạn thường xuyên có thể đã cứu vãn quá trình chạy. Nhìn lại sự tương tác, tôi không nghĩ hoạt động hoàn toàn tự chủ sẽ diễn ra tốt đẹp. Agent cần được chỉnh sửa, đặc biệt là về những gì được tính là tiến bộ thực sự.

Cách dễ nhất để giảm SLOC là rất rõ ràng: cắt bỏ bình luận, gom dòng code, định dạng lại, di chuyển mã ra khỏi các đường dẫn được tính, trích xuất các hàm trợ giúp làm cho bộ đếm nhỏ hơn nhưng hệ thống khó theo dõi hơn, xóa tài liệu và kiểm thử nếu lời nhắc (prompt) đủ cẩu thả. Một agent không cần phải có ý đồ xấu để làm điều này. Nó chỉ cần tối ưu hóa phần thưởng hiển thị.

Và tôi đã thấy sự “gian lận” phần thưởng:

Biểu đồ minh họa các khía cạnh của hiệu suất và chất lượng mã nguồn.

Một số “thắng lợi” ban đầu là dọn dẹp bình luận. Điều đó có vẻ như gian lận, nhưng tôi không nghĩ nó hoàn toàn là giả tạo. Các bình luận AI quá mức là một vấn đề thực sự. Chúng làm phình to ngữ cảnh, làm cho khả năng hiểu của agent trong tương lai tệ hơn, chúng giải thích mã rõ ràng trong khi che giấu những bình luận thực sự quan trọng. Quy tắc hiện tại của tôi rất đơn giản: mỗi dòng bình luận phải xứng đáng với vị trí của nó.

Tuy nhiên, xóa bình luận không thể là chiến lược chính. Nếu cơ sở mã chỉ nhỏ hơn vì văn xuôi xung quanh nó đã biến mất, hệ thống không thực sự đơn giản hơn. Nó chỉ yên tĩnh hơn. Sự phân biệt đó đã trở thành trọng tâm của kỹ năng.

Đối Phó Với “Gian Lận” Của AI Khi Tối Ưu SLOC

/goal-sloc không phải là một lời nhắc “thần kỳ” nói “hãy làm cho nó nhỏ hơn”. Toàn bộ mục đích là để agent chứng minh rằng nó không tự lừa dối mình. Kỹ năng này bắt đầu với các bước tiền kiểm (preflight):

  1. Đọc công cụ đo lường và xác định chính xác con số đó đếm cái gì.
  2. Ghi lại baseline và phân tích theo từng khu vực.
  3. Tính toán “ngưỡng không thể giảm” (irreducible floor).
  4. Đảm bảo các kiểm thử, phân tích tĩnh và kiểm tra ứng dụng thời gian chạy hoạt động trước khi cắt giảm.
  5. Sử dụng các công cụ ngữ nghĩa để phân tích mã chết và phụ thuộc thay vì chỉ dùng grep.
  6. Cố định định dạng để các thay đổi dòng code có thể so sánh được.
  7. Làm việc theo các mốc nhỏ, đã được xác minh.

Sau đó, nó đưa ra một thứ tự giảm thiểu trung thực cho agent:

1. Mã chết (dead code) trước tiên.
2. Các hệ thống con giữ chỗ (placeholder subsystems) thứ hai.
3. Mã khung sườn phát triển/kiểm thử (dev/test scaffolding) đặt sai chỗ thứ ba.
4. Trùng lặp thực sự sau đó.
5. Vệ sinh bình luận (comment hygiene) chỉ như một biện pháp vệ sinh.
6. Sau đó là các bản viết lại "trong phòng sạch" (clean-room rewrites) rủi ro hơn.
7. Đơn giản hóa kiến trúc.
8. Cuối cùng là ủy quyền cho các thư viện nếu một thư viện thực sự là kỹ thuật tốt hơn.

Quy tắc quan trọng là tự kiểm toán: sau mỗi vài mốc quan trọng, phân loại các giảm thiểu là cấu trúc (structural) hay “rẻ tiền” (cheap). Nếu các yếu tố “rẻ tiền” chiếm ưu thế, agent phải dừng lại và thừa nhận rằng nó đang “gian lận” số liệu, hoặc báo cáo rằng “nguồn gốc” cấu trúc đã cạn kiệt.

Điều này nghe có vẻ quá hiển nhiên. Nhưng nó không hiển nhiên trong quá trình thực hiện. Nếu không có quy tắc đó, mô hình sẽ tiếp tục trôi dạt về phía các yếu tố dễ dàng vì các yếu tố dễ dàng làm cho bảng điểm thay đổi.

Kỹ năng này cũng cho agent biết khi nào nên dừng lại. Điều này rất quan trọng. Các agent thường kém trong việc thừa nhận rằng mức tăng tiếp theo không còn đáng giá rủi ro. Chúng sẽ tạo ra sự “quay vòng” nếu lời nhắc (prompt) tiếp tục thưởng cho hoạt động. Một mục tiêu SLOC không có điều kiện dừng sẽ mời gọi các vòng lặp tái cấu trúc-hối tiếc-hoàn tác (refactor-regret-revert): thay đổi hệ thống, làm hỏng cái gì đó, vá lỗi, mở rộng lại mã và gọi toàn bộ sự hỗn độn đó là “học hỏi”.

Kết thúc đúng đắn đôi khi là: chúng ta đã gần đến ngưỡng tối thiểu; công việc còn lại là kiến trúc trung tính SLOC hoặc phạm vi sản phẩm; hãy hỏi con người.

Trải Nghiệm Với Claude Opus 4.8: Những Điểm Mạnh và Hạn Chế Bất Ngờ

Tôi đã sử dụng Claude Opus 4.8 cho phiên làm việc cuối tuần dài. Trải nghiệm rất mạnh mẽ, nhưng không theo nghĩa “để nó một mình cả ngày”.

Nó rất trung thực và tôi đánh giá cao điều đó. Nó sẽ đưa ra những nghi ngờ. Nó chấp nhận sửa chữa. Nó không giống một mô hình cố gắng thể hiện tiến bộ và làm “ugly-wishing” như hầu hết các mô hình trước đây. Sự trung thực đó rất quan trọng vì việc giảm SLOC có một con đường “gian lận” phần thưởng rõ ràng, và agent cần có khả năng bị gián đoạn.

Đồng thời, nó thường cảm thấy do dự. Đôi khi quá nhút nhát. Thẻ hệ thống của Claude Opus 4.8 có một dòng phù hợp với trải nghiệm hơn tôi mong đợi:

“Độ khó cho thấy sự phân tán lớn nhất, và cũng là nơi Claude Opus 4.8 khác biệt nhất so với các mô hình trước đây: Claude Opus 4.8 nhìn chung không thích các nhiệm vụ khó khăn, tương tự như Opus 4.7, nhưng ở mức độ lớn hơn.”

Tôi có thể cảm nhận điều đó. Mô hình có khả năng, nhưng nó không phải lúc nào cũng có sự tự tin mà tôi mong muốn cho một công việc dọn dẹp khó khăn. Nó kiểm tra. Nó thận trọng. Đôi khi tôi cần phải nói: không, đó không phải là tinh thần của nhiệm vụ; hãy tìm một chiến thắng cấu trúc thực sự.

Ngoài sự do dự, còn có rất nhiều thiếu sót rõ ràng. Ví dụ, xu hướng không sử dụng các bên thứ ba tốt là rất rõ ràng và không hợp lý – Opus cứ sử dụng quản lý trạng thái cơ bản mà bạn sẽ tìm thấy trong các hướng dẫn Flutter và điều đó giống như sử dụng prop-drilling trong React thay vì ví dụ như Redux.

Hình ảnh trừu tượng về sự phức tạp của hệ thống và các lớp trừu tượng trong mã nguồn.

Bài Học Lớn Nhất: Vai Trò Không Thể Thiếu Của Con Người Trong Kỷ Nguyên AI

Thực nghiệm này phù hợp với một mô hình tôi đã viết trong “Các Chế Độ Thất Bại Của AI Agent Vượt Ra Ngoài Ảo Giác”. Vấn đề không chỉ là ảo giác. Đó là việc vá lỗi cục bộ, overengineering theo mặc định, hoàn thành sai, đầu ra “hoạt động nhưng sai”, và sự thối rữa bộ nhớ làm việc. Phình to mã nguồn AI là một biểu hiện cụ thể của điều đó.

Đây là lý do tại sao câu nói “mã nguồn rẻ” cảm thấy sai, hoặc ít nhất là nguy hiểm không đầy đủ. Tạo mã nguồn thì rẻ. Sở hữu mã nguồn tồi thì không. Chi phí đến sau, khi agent tiếp theo phải hiểu một module nông cạn, duy trì một trừu tượng giả tạo, “đi đường vòng” một hệ thống con không hoạt động, hoặc đọc mười bình luận lặp lại những gì tên hàm đã nói.

Mô hình học từ một thế giới đầy mã nguồn “có vẻ doanh nghiệp”. Nó đã thấy hàng triệu ví dụ nơi mỗi tính năng đều có một manager, một service, một provider, một config object, một test double, một logger, một compatibility wrapper, và một bình luận giải thích điều hiển nhiên. Nó đã học được sự phức tạp. Sau đó, bên trong một vòng lặp agent, nó áp dụng sự phức tạp đó cục bộ. Kết quả hiếm khi là một tệp thảm khốc. Đó là sự tích lũy của những tàn dư trông có vẻ hợp lý.

Các kiểm thử hữu ích. Các khung sườn hữu ích. Tài liệu hữu ích. Nhưng chúng không tự động tạo ra “gu” thẩm mỹ. Chúng không cho bạn biết rằng một hệ thống con tồn tại chỉ vì một agent trước đó đã có một ý tưởng và không bao giờ loại bỏ các “đường ống” (plumbing). Chúng không phàn nàn khi một lớp trạng thái phản chiếu một lớp trạng thái khác. Chúng không quan tâm rằng agent tiếp theo sẽ lãng phí ngữ cảnh để đọc những bình luận không nên tồn tại.

P.S.: Trải nghiệm này thực sự đã khiến tôi tham gia sâu hơn. Tôi đã xem xét kỹ hơn cách phụ thuộc SoLoud được sử dụng, tại sao có rất nhiều tình trạng đóng băng luồng UI, cách codec Opus là một thách thức công nghệ (đừng nhầm lẫn với mô hình, nó chỉ là một lựa chọn thay thế MP3 hiện đại và hiệu quả hơn mà tôi sử dụng cho bộ sưu tập nhạc cục bộ của mình), thậm chí đã fork plugin SoLoud và thực hiện các thay đổi… Bây giờ ứng dụng hoạt động nhanh hơn nhiều và tôi không thấy các vấn đề rõ ràng đã làm phiền tôi. Điều này thực sự khiến tôi nghĩ rằng giấc mơ nhà máy phần mềm với đầu vào là đặc tả và đầu ra là phần mềm có thể đã được đánh giá quá cao, và vai trò của con người không chỉ dừng lại ở việc xác minh.