- Gần đây, khi việc ứng dụng agent AI ngày càng tăng, xu hướng tận dụng stack lai dựa trên ngôn ngữ Go cũng gia tăng
- Agent có đặc tính thời gian chạy dài, chi phí cao và thường xuyên chờ nhập/xuất
- Go cung cấp mô hình đồng thời hiệu năng cao như goroutine nhẹ, cơ chế hủy tập trung và nhắn tin dựa trên channel
- Thư viện chuẩn rất đồ sộ, và với công cụ profiling (pprof), việc theo dõi rò rỉ bộ nhớ và luồng trở nên dễ dàng
- Tuy nhiên, Go cũng có những hạn chế như thiếu hệ sinh thái machine learning, hiệu năng đỉnh không quá nổi trội và hỗ trợ bên thứ ba kém hơn so với các ngôn ngữ khác
Agent là gì
- Agent chạy trong một vòng lặp lặp lại và chỉ một quy trình có thể tự quyết định bước thực thi tiếp theo
- Không đi theo lộ trình được định nghĩa sẵn như workflow, mà xác định việc kết thúc dựa trên điều kiện (ví dụ: “kiểm thử thành công”) hoặc số lần lặp tối đa
- Khi triển khai trong dịch vụ thực tế, agent thường chạy lâu từ vài giây đến vài giờ, và chi phí cao do gọi LLM, thao tác trình duyệt, v.v.
- Vì phải xử lý đầu vào của người dùng (hoặc đầu vào từ agent khác), nên có nhiều thời gian chờ I/O
Vì sao ngôn ngữ Go phù hợp với agent
Đồng thời hiệu năng cao
- Goroutine của Go có thể chạy đồng thời hàng nghìn đến hàng chục nghìn luồng nhẹ chỉ với 2KB bộ nhớ
- Mỗi goroutine tận dụng đa lõi để xử lý song song, và có thể vận hành cả những agent đang I/O hoặc ở trạng thái chờ mà không tạo gánh nặng
- Thông qua giao tiếp dựa trên channel, việc đồng bộ được thực hiện bằng truyền thông điệp thay vì chia sẻ bộ nhớ (giảm thiểu sử dụng Mutex)
- Phù hợp để agent trao đổi thông điệp bất đồng bộ và quản lý trạng thái
Cơ chế hủy tập trung
- Khi sử dụng context.Context của Go, hầu hết thư viện và API đều hỗ trợ tín hiệu hủy nên việc dừng thực thi rất dễ dàng
- Trong khi Node.js hay Python có nhiều mẫu hủy khác nhau cùng tồn tại, Go cho phép hủy và thu hồi tài nguyên một cách an toàn theo phương thức nhất quán
Thư viện chuẩn phong phú
- Go có thư viện chuẩn rất đồ sộ, hỗ trợ gần như mọi lĩnh vực như HTTP/web, tệp, mạng và I/O
- Mọi I/O đều giả định hoạt động blocking bên trong goroutine, nên có thể viết logic nghiệp vụ theo kiểu thẳng dòng (straight-line)
- Python thì phức tạp hơn do nhiều mô hình đồng thời như asyncio, threading, process cùng tồn tại
Công cụ profiling và chẩn đoán
- Với các công cụ tích hợp như pprof của Go, có thể theo dõi theo thời gian thực rò rỉ bộ nhớ và rò rỉ goroutine (luồng)
- Đây là điểm mạnh khi chẩn đoán các vấn đề rò rỉ có thể phát sinh ở agent chạy lâu và chạy đồng thời
Hỗ trợ coding bằng LLM tốt
- Nhờ cú pháp đơn giản và thư viện chuẩn phong phú, LLM có thể viết tốt mã mang phong cách Go đặc trưng
- Mức độ phụ thuộc vào framework thấp nên LLM ít phải bận tâm đến phiên bản hay pattern
Hạn chế của Go
- Thư viện bên thứ ba và hệ sinh thái còn thiếu so với Python và TypeScript
- Không phù hợp để tự triển khai machine learning trực tiếp (bị giới hạn về hiệu năng và hỗ trợ)
- Nếu cần hiệu năng đỉnh cao, Rust và C++ sẽ tốt hơn
- Với các nhà phát triển quen xử lý lỗi thoải mái hơn, error handling của Go có thể hơi bất tiện
2 bình luận
Java thì không, Go thì có, còn hơn Go là Rust :)
Ý kiến trên Hacker News
Nhấn mạnh rằng trong hầu hết các hệ thống agent, yếu tố gây độ trễ lớn nhất cuối cùng vẫn là các lần gọi LLM. Những ưu điểm được nhắc trong bài không hẳn có lợi cho một ngôn ngữ cụ thể nào, vì phần lớn là thời gian chờ dài, sử dụng tài nguyên đắt đỏ, đầu vào từ người dùng hoặc agent khác, và thời gian chờ I/O lớn. Với đặc tính này, thay vì tốc độ hay hiệu suất thực thi phía server, lợi thế quan trọng hơn lại là hệ sinh thái thư viện AI đồ sộ và mức độ hỗ trợ mà các ngôn ngữ như Python có sẵn. Cũng có ý kiến cho rằng trong Python phải cân nhắc
asynciohay thư viện multithreading, nhưng thực tế phát triển agent không quá khó, và vì đã có người từng xây dựng các workflow liên quan nên có thể bắt đầu khá dễ dàngKhi xây dựng agent bằng Go, tôi thấy một lợi thế lớn là các mẫu quản lý đồng thời và backpressure đã được định hình rất tốt. Agent đa phần bao gồm các transaction với dịch vụ bên ngoài chậm, và các mẫu concurrency của Go rất hữu ích cho kiểu công việc này. Tất nhiên ngôn ngữ không quá quan trọng, và có vẻ JavaScript mới là thứ được dùng nhiều nhất. Tuy vậy, nếu là sinh mã thì tôi cảm thấy tổ hợp Go + LLM có sự cộng hưởng khá tốt
Go khác Python ở khả năng xử lý đồng thời rất tốt và việc triển khai cũng dễ hơn. Với Go chỉ cần phân phối static binary, nên không bị vướng các vấn đề về môi trường và dependency như Python
Agent đóng vai trò như một tầng orchestration, và tôi nghĩ Go, Erlang, Node đặc biệt phù hợp. Không nhất thiết phải có thật nhiều thư viện liên quan đến AI; với các tác vụ nhiều I/O, chỉ cần trừu tượng hóa phía sau giao diện công cụ theo từng domain rồi xây dựng subsystem bằng ngôn ngữ phù hợp khi cần
Go không có nhiều lợi thế cho kiểu workload này, vì phần lớn thời gian là dành cho việc chờ I/O. Hệ thống kiểu của Go khá hạn chế, và nhiều tính năng vốn có sẵn trong các ngôn ngữ hiện đại thì ở Go phải đi đường vòng. TypeScript là một ngôn ngữ glue rất tốt cho mục đích AI, và cùng với Python có hỗ trợ thư viện rất mạnh. Lý do tôi thích TypeScript hơn Python là hệ thống kiểu của nó mạnh và trưởng thành hơn nhiều. Python cũng đang cải thiện nhanh. Tôi không thấy có cơ sở chắc chắn cho nhận định rằng việc dừng các tác vụ chạy dài trong Node.js và Python là rất khó. Phần lớn công cụ đã hỗ trợ tính năng này, và các ngôn ngữ chính vẫn là Python và JS
Tôi đã thử nghiệm framework agent dựa trên Elixir và BEAM, và hiện tại tôi nghĩ tổ hợp BEAM + SQLite là lý tưởng nhất cho agent. Có thể thay thế agent một cách an toàn mà không cần redeploy ứng dụng, và khả năng đồng thời của BEAM là quá đủ cho việc này. Việc triển khai agent có trạng thái hoặc agent tạm thời cũng rất dễ. Sắp tới tôi dự định xây dựng base agent bằng Python, Typescript, Rust và cũng sẽ làm MCP server để có thể phát triển các agent phức tạp theo sở thích ngôn ngữ của từng người
Tôi khuyên dùng dự án Extism và Elixir SDK. Với tổ hợp này, bạn có thể dùng Elixir để làm core service, routing, message passing và tận dụng lợi thế của BEAM/OTP, đồng thời cũng có thể nhúng các agent ở dạng plugin là những mô-đun Wasm nhỏ gọn viết bằng ngôn ngữ khác
Extism
Elixir SDK
Tôi tò mò không biết có lý do gì để chọn SQLite thay vì
mnesia, data store tích hợp sẵn của BEAM khôngmnesia docs
Phần lớn thời gian của agent được dùng để chờ phản hồi từ LLM và gọi các dịch vụ bên ngoài (API, DB). Ảnh hưởng về hiệu năng của runtime ngôn ngữ trên thực tế gần như không đáng kể. Nếu nói đến tính năng ngôn ngữ thực sự quan trọng với hiệu năng và khả năng mở rộng của agent, thì đó có lẽ là hiệu năng serialize và deserialize JSON
Vì vậy tôi nghĩ dùng ngôn ngữ xử lý JSON tự nhiên như TypeScript sẽ tốt hơn. Hệ thống kiểu của TypeScript cũng mạnh hơn Go rất nhiều
Theo kinh nghiệm của tôi, ngoài các lệnh gọi LLM thì thứ tốn kém nhất trong agent là giải quyết xung đột khi chỉnh sửa bất đồng bộ (
merge,diff,patch). Việc này cũng có thể giao cho thư viện cấp thấp, nhưng là một bài toán khó tối ưu không kém gì serializationĐiều này làm tôi nhớ đến hướng dẫn xây dựng agent của ampcode.com. Nhờ tính chất ngôn ngữ động, Python rất tự nhiên trong các cách dùng như biến method thành lệnh gọi tool bằng decorator, tạo danh sách bằng cách lặp các hàm tool, hay nhanh chóng chuyển thành JSON schema. Mặt khác, với cấu trúc mà nhiều trigger bên ngoài khác nhau (ví dụ: input người dùng, email Gmail, tin nhắn Slack, v.v.) kích hoạt việc chạy agent mới, thì cách dùng channel và vòng
switch fortrong Go trực quan hơn nhiều. Trong Python phải tự tạo nhiều queue và thread riêng nên phức tạp hơnNếu xét theo logic của bài viết thì Elixir mới là ngôn ngữ lý tưởng cho agent
Hệ thống kiểu hạn chế và thiếu sót của Go gần như không phù hợp với hầu hết mọi ứng dụng. Trên thực tế, nhược điểm lớn nhất của Go chính là bản thân ngôn ngữ. Những yếu tố ngoài ngôn ngữ mới là thứ khiến người ta còn chấp nhận Go
Tôi đã lập trình bằng Go trong nhiều năm và đồng ý rằng hệ thống kiểu của nó có nhiều vấn đề. Những người làm trong lĩnh vực LLM có vẻ hầu như chỉ dùng Python hoặc JavaScript. Tôi nghĩ mọi người nên chuyển sang các ngôn ngữ hiện đại, nhưng Go có thể vẫn là lựa chọn đỡ tệ hơn so với mớ import và package lộn xộn của Python/JavaScript
Tôi muốn nghe cụ thể hơn việc các giới hạn của hệ thống kiểu Go gây cản trở cho việc tạo agent như thế nào
Thực tế việc Go có hệ thống kiểu tĩnh là vì họ không tìm ra cách nào khác để đạt được yêu cầu hiệu năng. Đúng hơn nên xem nó như một ngôn ngữ được dùng theo kiểu dynamic typing, và những lời chê trách đó là do hiểu sai mục tiêu thiết kế ngôn ngữ. Có thể lập luận rằng các ngôn ngữ dynamic typing nhìn chung là không phù hợp, nhưng việc Python, Erlang, Elixir và các ngôn ngữ dynamic typing khác vẫn được dùng sôi nổi lại cho thấy dynamic typing phù hợp hơn với bài toán này
Nhiều giá trị trả về không kết hợp được với nhau, hỗ trợ lỗi thì tốt hơn exception nhưng rất dài dòng, channel dễ dùng sai, còn kiểu enum thì gây thất vọng. Dù vậy, interface lại hoạt động tốt một cách bất ngờ, và hệ thống đóng gói khá trơn tru. Khi học Rust, tôi nhận ra cấu trúc file của nó phức tạp hơn Go rất nhiều. Thậm chí vì ngôn ngữ đơn giản nên cũng dễ làm ra nhiều công cụ lint/code generation khác nhau. Về bảo trì dài hạn, code Go khiến tôi ít lo hơn so với Python/JS
Nếu có một phương ngữ LISP/Scheme biên dịch sang Go được duy trì tốt thì sẽ thật tuyệt
Với các tiến trình phải chờ lâu và có chi phí thực thi cao, một nhược điểm là nếu tiến trình chết thì mất toàn bộ công việc. Serialize trạng thái vào cơ sở dữ liệu trong lúc chờ có thể an toàn hơn, nhưng có vẻ không có ngôn ngữ nào giúp làm việc này thật dễ dàng. Viết state machine dựa trên checkpoint không hề đơn giản
Checkpoint-based state machine chính là tính năng cốt lõi mà các nền tảng như Hatchet(hatchet.run) và Temporal(temporal.io) cung cấp. Chúng lưu lại lịch sử thực thi hàm trong workflow, rồi tự động phát lại lịch sử đó khi có interrupt. Tôi cho rằng lưu lịch sử tiến trình theo đầu ra hiệu quả hơn nhiều so với memory snapshot. (nhà sáng lập Hatchet)
Dù là goroutine, thread hay chuỗi chạy dài thì cuối cùng cũng phải tách thành các đơn vị công việc atomic, và việc serialize trạng thái là bắt buộc. Cách này đáp ứng được các yêu cầu như khôi phục sau lỗi, theo dõi lỗi, tham chiếu lại kết quả, phân tán đa node, v.v. Framework Oban(github.com/oban-bg/oban) của Elixir đi theo hướng này, và tôi cũng khuyên đọc bài viết về Oban nhấn mạnh tầm quan trọng của việc lưu bền các tác vụ bất đồng bộ. (tác giả Oban)
Tôi đang phát triển một thư viện agent dựa trên golang, và nghĩ rằng chỉ cần logging đủ tốt thì lúc nào cũng có thể khôi phục trạng thái agent. Chỉ cần biết timestamp và run cha là có thể dựng cây thực thi con/nhánh. Có thể dùng map kết hợp với DB để quản lý session và tái dựng khi cần. Cấu trúc là không giữ từng object riêng lẻ; với object stateless thì tra theo id trong map, còn action, step, context trước đó thì đặt trong object trạng thái. Tính nhất quán của agent/workflow cũng được xử lý bằng cách quản lý kết quả theo hash. Hiện mới chỉ triển khai các agent/tool cơ bản, còn logic logging, khôi phục và hủy vẫn chưa làm
Temporal khá hữu ích cho việc checkpoint các tiến trình dài hạn và cũng trung lập với ngôn ngữ
Tôi cũng đang cân nhắc job queue, và nghĩ đến việc có nên tự làm một queue rudimentary trong Postgres hay không. Ưu điểm là phân tán workload giữa các server, tránh mất task khi tiến trình kết thúc, và có độ quan sát tốt hơn. Đổi lại, độ phức tạp của code có thể tăng rất mạnh nên tôi cảm thấy không dễ để thiết kế kiến trúc một cách gọn gàng
Các kỹ sư AI cực kỳ ngại dùng JavaScript. Việc TensorFlow for Swift bị khai tử chính là dấu chấm hết cho sự đa dạng ngôn ngữ trong AI
Tôi nghĩ việc né JavaScript không chỉ xảy ra với kỹ sư AI. Với tư cách là người đã viết JS hơn 30 năm, tôi cũng đồng ý
JS là một ngôn ngữ rất tệ, và việc đem nó lên backend là một sai lầm. TypeScript rốt cuộc cũng không giải quyết được các vấn đề nằm ở nền JS. Tôi tránh dùng JS hay TS và thích các lựa chọn khác như Go, Rust, Python, Ruby, Elixir, F# hơn
Tôi tò mò vì sao JS lại đặc biệt phù hợp với agent
Tôi cảm thấy lĩnh vực ML cần một mô hình đồng thời tốt hơn. Tôi đã thử làm ML bằng Go nhưng gần như bất khả thi vì thiếu hỗ trợ thư viện và phải phụ thuộc vào các lệnh gọi gRPC bên ngoài hoặc wrapper. Python thì có giới hạn còn C++ thì quá dài dòng nên làm giảm năng suất khá nhiều