- k10s là một TUI Kubernetes có nhận biết GPU, được tạo rất nhanh bằng vibe-coding với Claude, nhưng sau khi thêm fleet view thì trạng thái của nhiều màn hình bị hỏng
model.go phình ra thành một Model đơn lẻ dài 1690 dòng cùng Update() dài 500 dòng, phải gánh toàn bộ trạng thái UI, client, cache, navigation và view
- AI thêm tính năng rất nhanh nhưng cũng làm god object và global key handler ngày càng lớn, khiến mỗi view mới lại phải thêm branch vào handler sẵn có
- Dữ liệu
[]string dựa trên vị trí và việc mutation trực tiếp trong background tea.Cmd có thể gây ra lỗi cột và data race rõ ràng
- Phiên bản k10s mới sẽ được viết lại bằng Rust, và trước prompt đầu tiên sẽ cố định interface, message type, ownership rule và scope trong CLAUDE.md
Bối cảnh phải viết lại k10s
- k10s khởi đầu là một dashboard Kubernetes có nhận biết GPU, một công cụ TUI được tạo ra để operator của các cụm NVIDIA có thể xem ngay mức sử dụng GPU, metric DCGM, node nhàn rỗi và chi phí
$32/hr
- Dự án được viết bằng Go và Bubble Tea, được tạo ra trong khoảng 7 tháng, 234 commit và khoảng 30 cuối tuần thông qua các phiên vibe-coding với Claude
- Giai đoạn đầu, các chức năng nền tảng kiểu bản sao k9s như pods, nodes, deployments, services, command palette, live updates dựa trên watch và Vim keybindings đã chạy được chỉ sau khoảng 3 cuối tuần
- Tính năng cốt lõi là GPU fleet view, màn hình hiển thị GPU allocation, mức sử dụng, chỉ số dựa trên DCGM, nhiệt độ, điện năng, bộ nhớ và trạng thái theo màu của từng node; Claude đã tạo một lần cả struct
FleetView, bộ lọc tab GPU/CPU/All và phần render allocation bars
- Sau khi thêm fleet view, khi quay lại pods view bằng
:rs pods, bảng bị trống, live updates dừng lại, nodes view hiển thị stale data từ bộ lọc fleet view và cả số đếm fleet tab cũng sai
- Trong lúc lần theo vấn đề, tác giả lần đầu tiên đọc toàn bộ
model.go dài 1690 dòng do Claude tạo ra và thấy một struct Model duy nhất đang ôm hết UI widget, Kubernetes client, trạng thái logs/describe/fleet, navigation history, cache và xử lý chuột
- Phương thức
Update() là một hàm dispatch msg.(type) dài 500 dòng với 110 nhánh switch/case
- AI có thể tạo tính năng rất nhanh, nhưng nếu cứ giao tiếp tục mà không có ràng buộc, kiến trúc sẽ sụp đổ; cảm giác tốc độ sẽ trông như thành công cho đến lúc toàn bộ hệ thống vỡ cùng lúc
Năm nguyên tắc rút ra từ đống đổ nát
-
Nguyên tắc 1: AI tạo tính năng nhưng không tạo kiến trúc
- Claude làm tốt các tính năng riêng lẻ như fleet view, log streaming, mouse support, nhưng mỗi tính năng đều được triển khai trong ngữ cảnh “làm cho nó chạy ngay bây giờ” và không tính đến quan hệ với các tính năng khác cùng chia sẻ trạng thái
- Trong handler
resourcesLoadedMsg xuất hiện điều kiện như msg.gvr.Resource == k8s.ResourceNodes && m.fleetView != nil, khiến logic riêng cho fleet view bị trộn vào generic resource loading path
- Mỗi khi view mới cần custom behavior, cùng một handler lại có thêm branch, và nhiều field phải được xóa thủ công để dữ liệu từ view trước không rò sang view mới
- Trong
model.go, các đoạn cleanup thủ công như m.logLines = nil, m.allResources = nil, m.resources = nil xuất hiện rải rác 9 lần; chỉ cần sót một chỗ là ghost data từ view cũ sẽ còn lại
- Cách thay thế là tự viết trước các interface cụ thể, message type và ownership rule trước khi viết code, rồi đưa chúng vào
CLAUDE.md như các architecture invariant
- Ví dụ quy tắc có thể là mỗi view phải triển khai trait/interface
View, view không được truy cập state của view khác, dữ liệu async chỉ đi vào qua các biến thể AppMsg, còn struct App chỉ phụ trách navigation và message dispatch
-
Nguyên tắc 2: god object là sản phẩm mặc định AI rất dễ tạo ra
- AI có xu hướng chọn cấu trúc một struct ôm tất cả để đáp ứng prompt trước mắt với ít ceremony nhất
- Ngay cả key handling cũng không được tách theo từng view; một phím
s trong logs view là autoscroll, trong pods view là shell, còn trong containers view là container shell
- Yêu cầu “thêm shell support cho pods” được triển khai bằng cách chèn thêm một branch cạnh global key handler cũ
- Phím
Enter cũng phân nhánh giữa contexts view, namespaces view, logs view và logic generic drill-down trong cùng một flat dispatch bằng cách so sánh chuỗi m.currentGVR.Resource
- Trong riêng file
model.go, biểu thức m.currentGVR.Resource == được dùng hơn 20 lần như một type discriminator, khiến mỗi lần thêm view mới lại phải chạm vào nhiều handler
- Cách thay thế là không thêm field state riêng theo view vào
App/Model, tạo mỗi view thành một struct riêng và để key binding trong keymap của view đang active; các quy tắc này nên được đưa vào CLAUDE.md
- Cần có guardrail như “thêm view phải là thêm file; nếu phải sửa view hiện có thì dừng lại và hỏi” để AI không chọn con đường ngắn nhất là chèn thêm branch
-
Nguyên tắc 3: Ảo giác tốc độ làm scope phình ra
- Ban đầu k10s là công cụ cho một nhóm người dùng hẹp, những người vận hành GPU training cluster, nhưng vibe-coding khiến các tính năng như pods, deployments, services, command palette, mouse support, contexts, namespaces trông như “miễn phí”
- Kết quả là sản phẩm mở rộng khỏi một công cụ tập trung vào GPU để thành TUI general-purpose cho mọi người dùng Kubernetes, thực chất là đang làm lại k9s
keyMap phẳng chứa lẫn lộn nhiều binding dành riêng cho các view khác nhau như Fullscreen, Autoscroll, ToggleTime, WrapText, CopyLogs, ToggleLineNums, Describe, YamlView, Edit, Shell, FilterLogs, FleetTabNext, FleetTabPrev
Autoscroll và Shell đều là s; do dispatch kiểm tra resource hiện tại nên nó vẫn “chạy”, nhưng keybinding không còn thể được hiểu trong ngữ cảnh cục bộ
- Tốc độ viết code trông giống như đang “shipping”, nhưng mỗi feature thực ra đang cộng thêm một branch vào god object
- Cách thay thế là ghi rõ scope boundary trong
CLAUDE.md: k10s là công cụ cho GPU cluster operator, các view được hỗ trợ chỉ gồm fleet, node-detail, gpu-detail, workload, và không thêm generic resource views hay các tính năng trùng với k9s
- AI có thể cho ngân sách dòng code gần như vô hạn, nhưng ngân sách độ phức tạp vẫn hữu hạn, nên cần từ chối scope ngay từ đầu
-
Nguyên tắc 4: Dữ liệu dựa trên vị trí là bom nổ chậm
- k10s làm phẳng resource lấy từ Kubernetes API ngay thành
type OrderedResourceFields []string
- Hàm sort của fleet view xử lý
ra[3] như Alloc, ra[2] như Compute, ra[0] như Name; danh tính cột chỉ dựa vào comment và thứ tự cột trong resource.views.json
- Nếu thêm một cột vào giữa Instance và Compute trong
resource.views.json, thì các đoạn sort, conditional render và drill target tham chiếu ra[2], ra[3] có thể âm thầm sai đi
- Compiler không thể biết ý nghĩa của
[]string, và JSON config cũng không diễn đạt được sort behavior, conditional rendering hay custom drill target, nên Go code phải hardcode các giả định theo vị trí
- AI rất dễ chọn
[]string hoặc Vec<String> vì chúng thuận tiện để đổ thẳng vào table widget, còn typed struct đòi hỏi ceremony ban đầu nhiều hơn nên hay bị bỏ qua trong con đường nhanh
- Cách thay thế là giữ dữ liệu có cấu trúc dưới dạng typed struct như
FleetNode, PodInfo cho đến sát lúc render, và sort trên named field thay vì positional access như row[3]
- Ví dụ struct như
FleetNode { name, instance_type, compute_class, alloc } biểu diễn danh tính cột bằng type, từ đó ngăn không cho xuất hiện các trạng thái bất khả như sort nhầm cột
- “Making impossible states impossible” là cách nói phổ biến trong cộng đồng Elm/Rust, nghĩa là thiết kế type để invalid state không thể được tạo ra thay vì chờ runtime check
-
Nguyên tắc 5: AI không nên sở hữu state transition
- Cốt lõi trong mô hình của Bubble Tea là state chỉ thay đổi trong
Update() chạy theo message, nhưng k10s đã phá vỡ nguyên tắc này
- Handler
updateTableMsg trả về một closure tea.Cmd, và bên trong closure đó có các lệnh như m.updateColumns(m.viewWidth), m.updateTableData(), m.table.SetCursor(savedCursor) làm thay đổi field của Model
- Bubble Tea chạy
tea.Cmd trong goroutine riêng, nên trong lúc closure đọc/ghi m.resources, m.table, m.viewWidth, thì View() trên goroutine chính vẫn có thể đọc cùng các field đó
- Không có lock hay mutex, và
<-m.updateTableChan chỉ chờ tín hiệu update chứ không ngăn View() đọc phải trạng thái đang được ghi dở
- Đây là một data race rõ ràng; phần lớn thời gian vẫn chạy được nhưng thỉnh thoảng màn hình sẽ hỏng
- Cách thay thế là background worker không được mutate UI state trực tiếp, mà gửi typed message qua channel, để main event loop nhận message rồi mới áp dụng mutation state
- Quy tắc concurrency là background task không được đổi UI state trực tiếp, phải gửi kết quả bằng typed message, và
render()/view() phải là pure function không có side effect, I/O hay channel operation
Các quy tắc bảo vệ nên đưa vào CLAUDE.md và agents.md
-
Các điều kiện bất biến của kiến trúc
- Mỗi view phải triển khai trait/interface
View và không được truy cập state của view khác
- Mọi dữ liệu async phải đi vào qua các biến thể
AppMsg, background task không được mutate field trực tiếp
- Việc thêm view mới không được yêu cầu sửa các view hiện có
- Struct
App phải là một router mỏng chỉ phụ trách navigation và message dispatch
-
Quy tắc ownership của state
- Không được thêm field state riêng cho từng view vào struct
App/Model
- Mỗi view phải tồn tại dưới dạng struct riêng và tự khai báo key binding của mình
- Ứng dụng phải dispatch phím cho active view, và keybinding mới phải được thêm vào keymap của view đó thay vì global handler
- Nếu việc thêm view đòi hỏi sửa các view cũ thì phải dừng lại và xác nhận
-
Phạm vi
- k10s phải là công cụ cho GPU cluster operator chứ không phải cho mọi người dùng Kubernetes
- Các view được hỗ trợ phải giới hạn ở fleet, node-detail, gpu-detail, workload
- Không được thêm generic resource view như pods, deployments, services
- Không được thêm các tính năng sao chép chức năng của k9s
- Những feature request không giúp ích cho operator vận hành GPU training jobs phải bị từ chối
-
Biểu diễn dữ liệu
- Không được làm phẳng structured data thành
[]string, Vec<String> hay positional array
- Dữ liệu phải được giữ dưới dạng typed struct cho đến ngay trước lệnh render
- Danh tính cột phải đến từ tên field của struct thay vì array index
- Hàm sort phải chạy trên typed field thay vì positional access như
row[3]
- Việc tạo string chỉ để hiển thị chỉ được diễn ra bên trong hàm
render()/view()
-
Quy tắc đồng thời
- Background task như watcher, scraper, API call không được mutate UI state trực tiếp
- Background task phải gửi kết quả qua channel dưới dạng typed message
- Chỉ main event loop mới được áp dụng state mutation từ message đã nhận
render()/view() phải là pure function không có side effect, I/O hay channel operation
- Nếu cần đổi state từ kết quả của async work, phải định nghĩa một biến thể
AppMsg mới
Cách làm lại
- k10s sẽ được viết lại bằng Rust, không phải vì Rust tốt hơn, mà vì đây là ngôn ngữ mà tác giả cảm thấy mình có thể trực tiếp lèo lái
- Với một ngôn ngữ đã dùng đủ lâu, có thể cảm nhận điều gì đó sai trước cả khi diễn đạt được nó thành lời, và cảm giác này không thể bị thay thế bởi vibe-coding
- Khi AI tạo ra đoạn code trông có vẻ hợp lý, cần có khả năng nhận ra liệu đó có phải rác hay không
- Ở phiên bản mới, con người sẽ tự làm phần design work như concrete interface, message type và ownership rule trước khi viết code
- Thay vì để AI tự đưa ra các architecture decision sai như trước, lần này các quyết định đó sẽ được chốt thành tài liệu ngay trước prompt đầu tiên
- Liên kết tới TUI hiện tại và dự án có tại k10s Github và K10S.DEV
Phụ lục
- Bubble Tea là framework Go TUI dựa trên The Elm Architecture, và các vấn đề kiến trúc của k10s không đến từ Bubble Tea mà từ cách k10s được triển khai
- “Making impossible states impossible” là cách nói trong cộng đồng Elm/Rust về việc dùng thiết kế type để ngăn invalid state được tạo ra, thay vì kiểm tra ở runtime
- Giống như “em-dash” trong văn viết AI, coding bằng AI cũng có thể để lại mùi “god-object”; vibe-coding khiến chi phí triển khai có vẻ rẻ, từ đó dẫn đến mất tập trung và bloat
1 bình luận
Ý kiến Hacker News
Những người nói rằng mã được sinh ra là ổn nhìn chung chỉ là những người không đọc đoạn mã đó
Các biện pháp giảm nhẹ được đề xuất trong bài cũng khó mà trụ lâu. Khi thiết kế hệ thống hay component, sẽ xuất hiện các bất biến như “view không truy cập trạng thái của view khác”, và đến lúc nào đó bạn sẽ phải thêm một tính năng xung đột với bất biến đó
Khi ấy thường chỉ còn chọn bỏ tính năng, chồng nó lên bất biến một cách gượng ép và kém hiệu quả, hoặc thay đổi chính bất biến đó. Đây không đơn thuần là vấn đề ngữ cảnh mà là vấn đề phán đoán, và các mô hình hiện tại sai quá thường xuyên ở chỗ này
Nếu ghi rõ các ràng buộc kiến trúc, agent sẽ cố uốn mình theo các ràng buộc đó ngay cả khi lẽ ra cần thay đổi, tạo ra mã phức tạp và không thể bảo trì. Nếu bạn không đọc kỹ hơn cả khi đọc mã do người viết, cuối cùng sẽ sinh ra thứ “mã tự ăn thịt chính nó”, và đến khi nhận ra thì đã quá muộn
Cốt lõi là xác định những điểm AI gặp khó rồi làm chúng trở nên dễ hơn. Ví dụ cần ngữ cảnh cực nhỏ, mô-đun hóa với ranh giới rõ ràng, các mô-đun thuần tách khỏi input/output, ẩn sau interface, 100 bài test chạy trong dưới 1 giây, benchmark, v.v.
AI hoạt động tốt khi có ranh giới và ngữ cảnh nhỏ. Nếu không cung cấp điều đó thì hiệu năng sẽ giảm, và trách nhiệm thuộc về người dùng công cụ
Không có đặc tả nào chịu nổi thực tế mãi mãi, và dù nghiên cứu thiết kế kỹ đến đâu thì một số bất biến trong đặc tả cuối cùng cũng sẽ lộ ra là sai
Khi con người gặp tình huống này trong lúc phát triển, họ có thể lùi lại một bước để nghĩ lại xem bất biến có sai không, nếu thay đổi thì ảnh hưởng là gì. Trong khi đó AI thường cố nặn ra một giải pháp chắp vá dưới các giả định hay thiết kế sai, và thiếu sự sáng suốt để đánh giá lại toàn cục
Có thể cải thiện bằng quy trình làm việc và xác minh tốt hơn, nhưng đây không phải lĩnh vực mà các công cụ như Claude Code xử lý tốt theo mặc định, và rõ ràng có giới hạn
Ban đầu chúng tôi đặt ra các nguyên tắc mạnh mẽ và tự tay chuyển vài chỗ để lấy niềm tin. Toàn bộ đợt chuyển đổi lớn và tốn kém đến mức bị hoãn gần 10 năm, nên để giảm chi phí chúng tôi muốn tăng tốc bằng AI
AI làm khá ổn với 80% trường hợp mang tính cơ học và đơn giản. 20% còn lại cần thay đổi framework, đa phần chỉ là thay đổi nhỏ như thêm field API, nhưng một hai trường hợp cần tái thiết kế khái niệm
Backend của một hệ thống có thể tạo dữ liệu cụ thể trong 99% trường hợp, nhưng với vài trường hợp quan trọng thì về mặt logic không thể tạo được và phải nhận báo cáo từ bên ngoài. Thế mà một tối ưu hóa quan trọng lại được xây trên giả định rằng “điều đó là không thể”
Công cụ AI không phát hiện được tình huống này và đã thêm logic chuyển đổi như thể nó sẽ hoạt động bình thường. Nhờ cách triển khai nên nó chưa thành bug production, nhưng trong lúc đặt đúng câu hỏi cho team đối tác, chúng tôi phát hiện nhu cầu tương tự còn xuất hiện ở nơi khác
Cuối cùng không thành thảm họa chỉ vì có một người đào rất sâu vào vấn đề. Có lẽ trong tương lai các công cụ xác minh và mô hình thông minh hơn sẽ làm những đợt chuyển đổi như vậy dễ hơn, nhưng hiện tại mã sinh ra tuy có lúc đẹp mà vẫn dễ vỡ, nên phải liên tục theo sát
Tôi có một pattern kiến trúc khá lạ đã dùng khoảng hai tháng, và mỗi lần dùng đều thấy hơi khó chịu, nhưng mãi tối qua mới nhận ra đó không phải một abstraction tốt và có cách chia tách tốt hơn
Nếu để LLM sinh mã thì cảm giác khó chịu đó mờ đi rất nhiều, khiến tôi mất lâu hơn để nhận ra vấn đề và tìm giải pháp. Phần rìa có thể để sinh tự động, nhưng chức năng cốt lõi thì phần lớn vẫn phải tự viết
Ngay cả nếu biểu diễn bằng ngôn ngữ hình thức chính xác, LLM nằm dưới agent vẫn thiếu khả năng hiểu vì sao bất biến đó cần thiết, vì sao nó quan trọng. Có thể sẽ xuất hiện LLM đủ khả năng nối token với đặc tả hình thức và thậm chí viết cả chứng minh, nhưng mã kỳ quặc sinh ra từ phần phi hình thức của prompt vẫn sẽ tiếp tục xuất hiện
Chỉ thêm ràng buộc và prompt vào danh sách kỹ thuật hay đặc tả là không ngăn được. Dù có làm cái bẫy tốt hơn thì sinh vật vẫn chui ra được
Vấn đề là sự phình to của mã do cố vá thêm cho vừa prompt hay task. Nhiều khi ít mã hơn lại tốt hơn, và cần có người đoán được người khác muốn gì, kỳ vọng gì. Trình sinh mã rất tốt, nhưng nên dùng tiết chế hơn, đừng như vòi cứu hỏa
Hồi Copilot tự động hoàn thành một dòng, người ta nói “dù sao thì cả hàm vẫn phải do bạn viết”, rồi khi nó hoàn thành cả hàm thì lại nói “logic xung quanh hàm vẫn phải do bạn viết”, rồi khi xong cả phần logic đó thì lại nói “tính năng vẫn phải do bạn viết”
Giờ nó làm xong cả tính năng thì lại bảo “nhưng kiến trúc vẫn phải do bạn viết”. Tôi không biết các mô hình này có giải được bài toán kiến trúc hay không, nhưng việc kỳ vọng cứ liên tục dịch chuyển thì khá thú vị
Dù AI hoàn thành một dòng, cả hàm, hay cả tính năng và ticket, bạn vẫn phải đọc và hiểu mã
Tôi dùng AI mọi lúc và nó đang dần tốt hơn, nhưng vẫn review từng dòng. Ở mức từng dòng riêng lẻ thì ngày nay chưa chắc đã tốt hơn tab autocomplete của năm ngoái, có lúc rất hay nhưng có lúc cũng thực sự tệ
LLM rất tuyệt cho phát triển phần mềm, nhưng chỉ khi không để chúng viết kiến trúc. Hãy tự tạo module, struct, enum, và nếu có thể thì tự thêm field cùng các biến thể
Gắn doc comment cho từng struct, enum, field, module, rồi cho LLM trỏ vào module và cấu trúc dữ liệu đó để điền phần thân hàm cần thiết, cách này khá ổn
Dù có nói đi nói lại rằng “tuyệt đối không được blocking trên critical path”, LLM vẫn chèn blocking vào critical path, và dù bảo “làm X thì cần loại test Y”, nó vẫn chỉ làm X rồi quên test
Con người cũng không thể làm theo chỉ dẫn 100%, nhưng LLM thì ngẫu nhiên hơn nhiều. Sai sót của con người tương đối hiếm khi chính xác làm điều ngược hẳn với điều bạn muốn
LLM có thể nhìn thấy các bất biến quan trọng trong mã rồi vẫn tạo đường vòng, viết test khiến thất bại trông như thành công, nói rằng đã làm đúng yêu cầu rồi chôn nó trong một commit 5.000 dòng
Tôi tin LLM rất tuyệt và là tương lai, nên vì thế tôi đang làm một ngôn ngữ cho chúng là https://GitHub.com/Cuzzo/clear. Chúng ta phải vượt qua vấn đề của các ngôn ngữ đòi hỏi ngữ cảnh toàn cục ở những nơi lẽ ra không cần, thì mới dễ cộng tác cùng chúng hơn
Cũng đã có thành công, nhưng đôi khi nó gây bực bội đến mức tôi tự hỏi có đáng phải tiêu tốn sự tỉnh táo hay không
Không phải kiến trúc không quan trọng, mà là kiến trúc phù hợp hôm qua chưa chắc hôm nay vẫn còn phù hợp
Khi dùng coding agent, tôi đã đặt ra vài quy tắc
Thứ nhất, nếu tạo mã bằng agent thì đó phải là thứ mà nếu có thời gian tôi hoàn toàn tin chắc mình có thể tự viết đúng
Thứ hai, nếu không phải vậy thì tôi sẽ không đi tiếp cho đến khi hiểu hoàn toàn phần được sinh ra đến mức có thể tự tái tạo nó
Thứ ba, nếu vi phạm quy tắc thứ hai thì có thể tạo ra nợ nhận thức, nhưng phải trả hết trước khi tuyên bố dự án hoàn thành
Nợ tích lũy càng nhiều thì khả năng chất lượng mã sinh ra về sau giảm xuống càng cao, và nó cũng có cảm giác phình ra như lãi kép. Với dự án cá nhân, cách này vừa vui, vừa học được nhiều, lại giữ được một codebase mà mình có thể thoải mái hiểu rõ
Cần tìm điểm cân bằng để vẫn gắn kết với codebase mà không trở thành nút thắt của cả team
Claude là nhà toán học cỡ tiến sĩ còn tôi thì không, nhưng tôi biết chính xác tính chất của nghiệm mình muốn và cách kiểm tra nó có đúng không. Vậy nên thay vì giữ lời giải đơn giản và ngây thơ của mình, tôi giữ lời giải của Claude, ghi rõ điều đó trong pull request, và mọi người đều thấy đó là lựa chọn đúng
Tôi tò mò liệu nên có ngoại lệ trong kiểu trường hợp này không. Nếu AI không chỉ giỏi toán cao cấp mà còn giỏi coding hơn tôi rất nhiều, thì câu hỏi thú vị hơn là liệu tôi có dừng hẳn việc tự viết mã hay không, dù có mất khả năng tự đánh giá mã, miễn là vẫn giả định rằng tôi có thể đánh giá được test
Vì khoản nợ tích lũy đó chính xác là sự thiếu hiểu biết về mã, nên cách gọi này chuẩn hơn
Tôi không hiểu vì sao chỉ riêng AI lại đột nhiên bị đối xử khác đi
Cuối cùng vẫn phải đánh giá theo rủi ro và phần thưởng. Cần cân nhắc thiệt hại nếu sai, xác suất bị phát hiện qua test và review, và lợi ích nếu làm đúng. Thư viện và dịch vụ bên ngoài cũng vậy
Các quy tắc tài chính phức tạp trong một smart contract crypto không thể cập nhật và không có test hoàn toàn khác với một viewer trực quan hóa dữ liệu log nội bộ
Trên lý thuyết nghe có vẻ hay, nhưng thực tế thì bạn luôn chọn những lối tắt tinh thần mà chính mình cũng không nhận ra
Khi sửa lỗi trong một codebase xa lạ, nếu so việc tự làm với việc nghĩ rằng mình đã “hiểu hoàn toàn” những gì agent làm, thì lượng thứ còn lưu lại trong đầu sau một tuần là khác hẳn. Tự làm thì nó tích lại thành tri thức chung và phần quan trọng thường còn nhớ, còn nếu cố chiếm hữu như thể phần agent làm là của mình thì lúc đó có vẻ như hiểu nhưng rồi quên rất nhanh
Vì vậy trong các trường hợp như thế tôi kết luận rằng trợ giúp từ LLM phần lớn có hại cho mục tiêu của mình, ngay cả khi chưa tính tới các lo ngại khác như áp lực thời gian hay kinh doanh
Tôi cũng đã trải qua chuyện y hệt
Cú lừa diễn ra như thế này. Với một codebase tốt, AI có thể làm ra rất nhiều tính năng, thậm chí trông còn nhanh hơn, an toàn hơn và chính xác hơn. Đặc biệt ở những lĩnh vực bạn không rành thì lại càng thấy như vậy
Theo thời gian, codebase lớn lên, thời gian điều hướng kéo dài, tỷ lệ thất bại tăng lên. Vì không muốn thừa nhận, bạn càng ép mạnh hơn, rồi chỉ dừng lại khi việc thay đổi đã gần như bất khả thi
Khi nhìn lại mã thì gọi là spaghetti còn chưa đủ, nó giống Vạn Lý Trường Thành hơn
Cuối cùng tôi đã xóa 75.000 dòng trên tổng 140.000 dòng, và cảm thấy 3 tháng lao đầu mạnh vào agent coding là lãng phí. Tôi đã tạo ra tính năng vô dụng, tăng thêm bug, đánh mất mô hình tinh thần về mã, bỏ lỡ những quyết định khó chỉ nhìn thấy khi ở trong mã, và cũng làm người dùng thất vọng
Tôi không mỉa mai đâu, mà thật sự tò mò kỳ vọng ban đầu là gì và đến từ đâu
Có vẻ người ta kỳ vọng khác với LLM. Nếu bạn đưa một mô tả tính năng đã tóm tắt cho một “lập trình viên” ngẫu nhiên chỉ gặp trên mạng rồi nhận lại một đống triển khai nửa hỏng, sẽ chẳng ai ngạc nhiên cả
Nhưng đôi khi người ta lại kỳ vọng ở cái máy hay nói thao thao và ảo giác những phép màu mà họ còn chẳng kỳ vọng ở con người. Tôi tò mò niềm tin đó đến từ đâu
Giống như một thành phố lớn là tập hợp của các thành phố nhỏ, bạn có bản đồ, có thể phóng to vào khu vực cục bộ và làm việc trong phạm vi đó. Muốn uống một ly cà phê thì đâu cần biết hết mọi chi tiết của New York
Xây dựng kiến trúc lành mạnh có thể bảo trì là trách nhiệm của người dùng công cụ. AI không ngăn cản điều đó, và nếu cầm công cụ đúng cách thì thậm chí còn có thể hỗ trợ
Chẳng hạn coi mã do AI sinh ra ngay lập tức là mã legacy, đặt ranh giới đóng gói mạnh cùng interface được định nghĩa rõ, rồi tích hợp nó theo một luồng thủ công hơn
Có cả một phổ cách dùng, từ prompt một phát tới sinh mã inline, và tùy bài toán cũng như vị trí trong codebase mà cách phù hợp sẽ khác nhau
Sinh một lần phù hợp hơn ở giai đoạn prototype khi phải lặp lại đặc tả nhiều, còn khi prototype đã định hình thì nên hạ xuống mức sinh theo module hoặc file để làm có hệ thống hơn, đồng thời ở tầng đó vẫn phải giữ được một mô hình tinh thần tương đối tốt
Nếu có đọc nhưng không hiểu thì đáng lẽ nên yêu cầu chú thích chi tiết cho từng đầu ra; nếu đã biết mô hình sẽ chật vật hơn khi codebase lớn lên thì độ nghiêm trong việc xem xét đầu ra cũng phải tăng tương ứng với độ phức tạp
Tạo ra những “hòn đảo” mã chất lượng cao hơn, dùng AI hỗ trợ tái dựng ý đồ của lập trình viên và quy tắc nghiệp vụ, rồi tạo seam và unit test cho module mục tiêu
AI không nhất thiết chỉ để tăng throughput; nó cũng có thể là công cụ thăm dò và refactor linh hoạt để hỗ trợ việc tự viết mã hoặc triển khai bằng agent về sau
Mỗi lần đọc những bài kiểu này, tôi lại so tốc độ mà người ta nói AI mang lại với tốc độ tôi có được khi đơn giản là tự tay code
Tình cờ là tôi đang làm một dự án 3D MMO được 7 tháng, hiện đã chơi được, mọi người cũng thấy vui, đồ họa ổn, và có thể dễ dàng nhét vài trăm người vào server. Kiến trúc cũng khá tốt nên mở rộng tính năng dễ, và có lẽ sau khoảng 1 năm phát triển là có thể phát hành
Trong khi đó bài gốc sau 7 tháng vibe coding còn chưa làm nổi một TUI cơ bản. Tốc độ ra tính năng có thể tạo cảm giác cao, nhưng để làm kiểu UI cơ bản như vậy thì chậm đến khó tin. Có rất nhiều thư viện TUI tốt, và đây chỉ là loại việc điền dữ liệu cần thiết vào bảng, tự làm bằng tay chỉ mất vài tuần
Khi dùng AI, cảm giác tiến rất nhanh và làm được rất nhiều là rất mạnh, nhưng thực tế dường như trong nhiều trường hợp còn chậm hơn tự code rất nhiều. Dữ liệu về năng suất cũng có vẻ ủng hộ điều đó: người dùng AI cảm thấy nhanh hơn nhưng sản lượng thực tế lại ít hơn
Trong công việc phát triển phần mềm, phần tốn thời gian nhất là các cuộc họp để căn chỉnh kỳ vọng của stakeholder và giải pháp. Ở góc độ đó AI gần như không giúp gì, nên nếu so số giờ công từ lúc đề xuất đến lúc vào vòng lặp test thì kết quả sẽ khá thất vọng
Nhưng trong việc giải quyết vấn đề, sửa bug, và triển khai giải pháp đã được phê duyệt, tôi cảm thấy nó đã tốt hơn ít nhất 10 lần so với trước. Không chỉ về thời gian thuần túy mà còn ở khả năng diễn giải hành vi quan sát được và điều tra vấn đề
Dù vậy, cũng có người không thể tạo ra kết quả chính xác, có giá trị bằng AI. Nếu bạn biết chính xác mình muốn gì và muốn như thế nào thì AI giúp rất nhiều. Nếu giao cho nó điều mà đằng nào tôi cũng sẽ làm, nó làm nhanh hơn. Nhưng nếu bạn còn chưa biết rõ mình muốn gì thì AI lại gây hại cho tiến độ
Lý do những thứ người ta khoe là làm bằng LLM không mấy ấn tượng là vì đa số đều là thứ có thể tự làm bằng tay trong thời gian rất ngắn
Tôi cũng không quan sát thấy phần mềm ấn tượng tăng lên, và điều đó có vẻ khớp với thực tế là hiện nay LLM chủ yếu được dùng để giải quyết các vấn đề đơn giản thay vì những vấn đề quan trọng
Một điểm khác ít được nhắc tới là chất lượng mã
Codebase vibe coding là ví dụ rất điển hình cho việc LLM không hề giỏi viết mã đến vậy. Nó sửa sai lầm của chính mình rồi lại tạo lại ngay, và việc dùng pattern cũng không nhất quán
Gần đây Claude còn có những lựa chọn style code “thú vị” nhưng lại không khớp với style hiện tại của codebase
Cần dùng kiểu ngôn ngữ của “senior developer” để chặn những vòng lặp đó
Đoạn “trước khi viết mã, tự thiết kế các interface, kiểu message, quy tắc ownership cụ thể” chính là phần khó của việc coding
Khi đã có kiến trúc rồi thì viết mã rất dễ. Nếu không tự viết mã, sẽ khó nhận ra rằng bạn đã thiết kế một API cho phép null nhưng database thì không cho, hoặc dù có cho thì bạn vẫn bỏ lỡ các vấn đề nhỏ khác
Tôi không hiểu sao viết xong bài này mà tác giả vẫn chưa nhận ra vấn đề là AI. Không chỉ vì giao kiến trúc cho AI, mà còn vì đã không theo dõi cẩn thận mọi việc AI làm
AI là một trình sinh mã được tô vẽ hào nhoáng, và bạn phải kiểm tra mọi thứ nó làm. Phần khó của kỹ nghệ phần mềm chưa bao giờ là viết mã, mà là tất cả những thứ khác
Những lập trình viên thấy coding là khó thì rất thích AI coding. Vì việc trước đây khó giờ đã thành dễ
Ngược lại, với những người nghĩ coding là dễ, coding là bài toán về abstraction, khả năng bảo trì, khả năng mở rộng. Điều khó là đặt nền móng hợp lý để phần mềm có thể lớn lên, và khi tìm được abstraction đúng thì phần còn lại tương đối dễ
Với những người này, AI coding là công cụ hữu ích nhưng không phải công cụ thần kỳ. Tác giả bài gốc đã nhận ra giới hạn của AI nên thuộc nhóm thứ hai, và đã nhìn thấy phần khó mà AI không làm được
Một bên là những người dùng tab autocomplete rất mạnh hay chatbot ở cửa sổ bên cạnh nhưng vẫn review rõ ràng mọi thứ, bên kia là những người như Steve Yegge quảng bá editor mới để điều phối hàng chục agent như thể sẽ không đọc phần lớn mã nữa: https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16d...
Nhóm thứ nhất vẫn suy nghĩ sâu về thiết kế, interface, cấu trúc dữ liệu và review rất chặt. Nhóm thứ hai thì không như vậy nên đáng lo hơn
Chúng theo cách tiếp cận plan → red/green/refactor, và bản thân phần kế hoạch thì khá thuyết phục, có vẻ có cơ sở vì đã hút hết tài liệu và thảo luận diễn đàn vào
Vấn đề là khi bắt đầu làm việc thì chắc chắn sẽ lòi ra chỗ tài liệu và triển khai thực tế khác nhau. Có thể bộ công cụ chưa từng được dùng theo cách đó, tài liệu đã cũ, hoặc đơn giản là bug
Dù vậy, nếu mục tiêu dự án hay tính năng đủ rõ và có thể chạy/test cục bộ, agent vẫn có thể lặp đi lặp lại trong ngõ cụt kiến trúc rồi thoát ra được. Nó còn đi xem cả dependency và code thư viện, thậm chí đề xuất sửa upstream, khá giống việc tôi sẽ làm trong một phiên debug sâu
Vì thế tôi khá hài lòng với cách chỉ đạo và giám sát thay vì tự làm việc chán ngắt. Tuy nhiên, khá nhiều đồng đội của tôi vốn cũng không đào vấn đề kiến trúc sâu đến mức này mà thường mặc định “escalate cho kiến trúc sư”, nên về dài hạn điều đó có lẽ không tốt
Cửa sổ để có thể chạy và hiểu mọi thứ dường như đang đóng lại rất nhanh. Dù vậy, có lẽ chúng ta cũng sẽ thích nghi bằng cách tạo công cụ và framework mới, giống như vẫn dùng compiler dù không hiểu hết quá trình nó chuyển sang mã máy hay cơ chế branch prediction và caching của CPU hiện đại
Ở vị trí một người chưa có quá nhiều kinh nghiệm code, tôi đang học được nhiều hơn bao giờ hết bằng cách kiểm tra kết quả, xem cái gì đúng cái gì sai
Vì vậy tôi cũng không nghĩ chuyện này sẽ sớm cải thiện vượt bậc. Khi người ta hỏi “làm sao đầu ra của Claude lại tốt thế”, câu trả lời của tôi luôn là “tôi xem rất kỹ, tìm vấn đề rồi bảo Claude sửa”. Thực sự chỉ có vậy thôi, nhưng nghe xong là mắt họ đã đờ ra
Nó giống việc Google làm cho tìm kiếm thông tin dễ hơn, nhưng không loại bỏ yếu tố con người trong việc phân biệt thông tin tốt và xấu
Trước hết hãy nghĩ về bài toán, thiết kế cấu trúc và API, rồi mới giao phần triển khai cho AI
Tiêu đề là “quay lại viết code bằng tay”, nhưng việc thực sự đang làm là “tự làm công việc thiết kế trước khi mã được viết ra”
Như vậy thì có vẻ mã vẫn do Claude sinh ra
Nghiêm trọng hơn, thật khó hiểu khi trong suốt 7 tháng tác giả nghĩ rằng dự án vibe coding chạy tốt dù còn chưa xem mã nguồn được sinh ra, thậm chí còn mua cả domain
Nếu đó là side project và bạn theo diff để kiểm tra dần dần, thì việc không đọc quá sâu vào mã cũng không phải hoàn toàn kỳ quặc. Chắc chắn là một cách làm khác, nhưng chưa đến mức điên rồ
Cảm giác như đang xem các lập trình viên speedrun những bài học của quản lý dự án và quản lý sản phẩm
Giờ họ đang nhận ra đặc tả là hữu ích, và viết ra nhiều mã sai không làm dự án nhanh hơn. Lập trình viên thường khó chịu vì họp hành và thảo luận cản trở việc viết mã, nhưng những quá trình đó thường tồn tại để ngăn mọi người cùng nhau viết thêm nhiều thứ sai
Họ cũng đang nhận ra quản lý công việc là hữu ích, và giờ khi ngày càng nhiều người nói phải làm hết thiết kế từ trước thì lại đang tiến về mô hình waterfall
Tiếp theo sẽ là đặt tên cho prototyping, rồi nói về tính năng phát triển dần dần để quản lý đồng thời yêu cầu cũ và mới, rồi cuối cùng sẽ đi đến chuyện khách hàng cần tham gia nhiều hơn
Cần nhìn vào việc project manager và product manager thực sự làm gì. Họ dẫn dắt một sản phẩm là mã, nhưng lại không được kỳ vọng sẽ đọc mã, và phải đạt được điều đó chỉ bằng ngôn ngữ tự nhiên
Họ nghĩ con người không viết ra đồ hỏng à? Họ nghĩ team không bao giờ đi sai đường rồi đốt cả tuần, thậm chí cả tháng à? Giờ với vibe coding thì có thể trải nghiệm toàn bộ chuyện đó chỉ trong 30 phút. Từ góc nhìn của một cựu technical product manager thì cảm giác giống hệt
Thực ra có vẻ không phải đang tự viết mã bằng tay, nên sự khác biệt giữa tiêu đề và kết luận khá gây bối rối