Mời bạn thưởng thức Newsletter #89.
Hybrid Quota-Linear Rate Limiter
Tony Finch chỉ ra một điểm yếu ít được để ý của các bộ giới hạn tốc độ (rate limiter) tuyến tính như leaky bucket hay GCRA: chúng phản ứng chậm với những client vượt giới hạn nhưng không quá nhanh. Với hạn mức q yêu cầu trong cửa sổ thời gian w, một client chạy nhanh gấp a lần mức cho phép vẫn gửi được khoảng q*a/(a-1) yêu cầu trước khi bị chặn — tức gấp đôi hạn mức khi a=2. Nguyên nhân là bucket khởi đầu đã đầy token, rồi trong cửa sổ đầu tiên lại được nạp thêm một hạn mức nữa. Cách đặt lại bucket theo cửa sổ cố định (quota-reset) chặt hơn, nhưng tốn bộ nhớ và khiến client dồn yêu cầu thành từng đợt.
Tác giả đề xuất một thuật toán lai với hai chế độ: chế độ bursty cho client ít lưu lượng, được gửi theo đợt nhưng chỉ được nạp tối đa một hạn mức mỗi cửa sổ; và chế độ smooth cho client lưu lượng cao, bắt đầu với bucket rỗng (kèm một khoản phạt âm) và buộc yêu cầu phải dàn đều. Thuật toán chuyển sang smooth khi client tiêu hết token cuối cùng, và quay lại bursty khi bucket hồi phục đủ một hạn mức. Kết quả tương đương chạy song song một bộ quota-reset và một bộ tuyến tính nhưng tốn ít bộ nhớ hơn. Dù vậy, chính tác giả khuyên không nên dùng nó: bộ giới hạn tuyến tính chỉ có vẻ “hào phóng” nếu bỏ qua việc client đã im lặng ở cửa sổ trước. Tốt hơn là định nghĩa lại bài toán để dùng một bộ giới hạn tuyến tính đơn giản như GCRA, vốn khuyến khích client gửi đều đặn.
Go is the Best Language for AI Agents
Burak Karakan, người đã làm việc chuyên nghiệp với Go suốt 8 năm và đang xây dựng Bruin — công cụ ETL mã nguồn mở viết bằng Go — cho rằng Go là ngôn ngữ phù hợp nhất để lập trình cùng AI agent. Lý do đầu tiên là Go được biên dịch: agent sinh ra rất nhiều mã nguồn trông có vẻ đúng, và trình biên dịch cùng hệ thống kiểu tĩnh giúp agent lặp lại cho đến khi loại bỏ được cả một nhóm lỗi về kiểu và tham số. So với Rust, Go có cú pháp và khái niệm đơn giản hơn, biên dịch nhanh hơn nên vòng phản hồi ngắn hơn, và lượng mã Go ngoài kia cũng nhiều hơn. Sự đơn giản còn giúp con người dễ đọc và nhận ra khi agent đang đi theo một hướng thiết kế kỳ lạ.
Go cũng là ngôn ngữ có quan điểm rõ ràng: có một cách chuẩn để chạy kiểm thử, định dạng mã hay xây dựng tệp thực thi, nên agent chỉ cần gọi gofmt thay vì cài thêm công cụ như thường thấy ở JavaScript. Việc biên dịch đa nền tảng dễ dàng cho phép chạy toàn bộ kiểm thử trên nhiều hệ điều hành sau mỗi thay đổi, một lợi thế lớn khi dùng background agent chạy trong các sandbox mà ta không kiểm soát được môi trường. Theo trải nghiệm cá nhân của tác giả, agent viết Go đúng ngay lần đầu khoảng 95% thời gian — không phải vì dữ liệu huấn luyện nhiều hơn Python, mà vì Go thường chỉ có một cách làm cho mỗi việc. Tác giả thừa nhận nhận định này khá cảm tính và có thể mất dần giá trị.
Your Agent Needs a Harness, Not a Framework
Dan Farrelly từ Inngest lập luận rằng AI agent cần một “harness” chứ không phải framework. Trong mọi ngành kỹ thuật, harness là lớp kết nối, bảo vệ và điều phối các thành phần mà không tự làm thay việc của chúng — như bó dây điện nối động cơ với cảm biến, hay dây an toàn giữ bạn khi ngã. Với agent, LLM là động cơ, công cụ là thiết bị ngoại vi, bộ nhớ là nơi lưu trữ; nhưng cần có thứ xử lý khi LLM hết thời gian chờ ở vòng lặp thứ năm hay khi hai tin nhắn va nhau. Mỗi framework lại tự xây từ đầu cơ chế thử lại, lưu trạng thái, hàng đợi và định tuyến sự kiện, trong khi hạ tầng bền vững hướng sự kiện đã giải quyết sẵn: mỗi lần gọi LLM hay công cụ là một “step” có thể thử lại độc lập, nên nếu tiến trình chết ở vòng thứ năm thì bốn vòng trước vẫn được lưu.
Để chứng minh, nhóm xây dựng Utah (Universally Triggered Agent Harness) — agent hội thoại qua Telegram hoặc Slack có công cụ, bộ nhớ, ủy quyền cho sub-agent qua step.invoke() và khả năng chịu lỗi, viết bằng ít mã TypeScript, không framework, chỉ dùng function, step và event của Inngest quanh vòng lặp suy nghĩ → hành động → quan sát. Bài học lớn nhất: quản lý ngữ cảnh mới là thử thách thực sự, vì kết quả công cụ phình to khiến mô hình mất phương hướng và gọi công cụ mãi không dừng. Nhóm khắc phục bằng cắt tỉa kết quả cũ theo hai tầng, nén lịch sử phiên, cảnh báo khi sắp hết số vòng lặp và tự nén lại khi gặp lỗi ngữ cảnh quá lớn.
An Interactive Intro to Quadtrees
Bài viết tương tác này giải thích quadtree — cấu trúc dữ liệu giúp tìm kiếm trong không gian hai chiều hiệu quả, xuất phát từ việc tác giả thấy Uber dùng nó trong các bài về thiết kế hệ thống. Với một ứng dụng bản đồ có hàng triệu địa điểm, cách đơn giản nhất là tính khoảng cách từ người dùng tới từng điểm, nhưng mỗi truy vấn khi đó tốn hàng triệu phép tính. Quadtree tổ chức lại chính không gian: chia một vùng hình chữ nhật thành bốn góc phần tư, và khi một vùng chứa vượt quá sức chứa (capacity) thì tiếp tục chia nhỏ. Vùng dày đặc được chia mịn, vùng thưa vẫn giữ nguyên, nên tìm một điểm chỉ cần đi xuống cây, mỗi tầng loại bỏ ba phần tư không gian — khoảng 10 bước cho một triệu điểm.
Tham số capacity quyết định hình dạng cây: capacity thấp tạo cây sâu, bỏ qua được nhiều vùng hơn nhưng tốn bộ nhớ; capacity cao tạo cây nông nhưng mỗi nút phải duyệt tuần tự nhiều điểm hơn, với giá trị từ 4 đến 16 là điểm khởi đầu hợp lý. Truy vấn theo vùng chỉ đi vào các nút có khung giao với vùng tìm kiếm và cắt bỏ toàn bộ nhánh còn lại; tìm điểm gần nhất duy trì khoảng cách tốt nhất hiện tại để cắt tỉa ngày càng mạnh. Ngoài tìm kiếm địa lý, quadtree còn dùng trong phát hiện va chạm giai đoạn sơ bộ (broad-phase) của game để tránh so sánh mọi cặp đối tượng, và trong nén ảnh theo vùng: vùng đồng màu lưu thành một khối lớn, vùng nhiều chi tiết được chia nhỏ.
Agentic Engineering Patterns
Simon Willison ra mắt một hướng dẫn về các mẫu (pattern) giúp khai thác tốt nhất các coding agent như Claude Code và OpenAI Codex. Hướng dẫn phân biệt rõ “agentic engineering” với “vibe coding”: vibe coding là để LLM viết mã mà không cần quan tâm đến mã nguồn, còn agentic engineering là kỹ sư chuyên nghiệp dùng agent để khuếch đại chuyên môn sẵn có của mình chứ không phải thay thế nó. Nội dung được sắp theo từng nhóm: nguyên tắc, làm việc với coding agent (cách agent hoạt động, dùng Git, sub-agent), kiểm thử và đảm bảo chất lượng, hiểu mã nguồn qua các bản hướng dẫn đọc tuần tự và giải thích tương tác, cùng các prompt có chú thích và phụ lục những prompt tác giả thường dùng.
Một vài mẫu tiêu biểu: “Viết mã giờ rất rẻ” — chi phí sinh mã gần như bằng không, nhưng mã tốt, tức là có kiểm thử, tài liệu và xử lý lỗi đầy đủ, vẫn có giá, nên cần xây dựng thói quen mới; “Tích lũy những gì bạn biết cách làm” — giữ một kho ví dụ chạy được để kết hợp lại và làm đầu vào cho agent; và “Red/green TDD” — viết kiểm thử trước, xác nhận nó thất bại rồi mới để agent triển khai, kèm lời khuyên chạy bộ kiểm thử ngay từ đầu phiên. Hướng dẫn cũng nêu các phản mẫu cần tránh, điển hình là đẩy mã chưa được xem xét sang cho đồng nghiệp, và liên tục được bổ sung thêm chương mới.
Things I Miss About Spring Boot After Switching to Go
Sushant Dhiman, sau 1,5 năm viết hệ thống production bằng Java và Spring Boot cho một startup rồi chuyển sang Go, chia sẻ những thứ anh nhớ ở hệ sinh thái Spring. Điều đầu tiên là triết lý “batteries included”: Spring Boot cung cấp sẵn gần như mọi tính năng cần cho production, trong khi Go theo triết lý tối giản với nhiều thư viện nhỏ thay vì một framework khổng lồ. Cụ thể, anh nhớ dependency injection tự động qua annotation như @Service và @Autowired, trong khi ở Go phải tự nối các phụ thuộc qua hàm khởi tạo — rõ ràng nhưng mã khởi động sẽ phình to khi hệ thống có hàng trăm phụ thuộc. Tương tự, validation khai báo với @NotNull hay @Email giúp tránh các chuỗi if/else kiểm tra thủ công bằng biểu thức chính quy.
Hệ sinh thái trưởng thành cũng là điểm cộng lớn: Spring Security hỗ trợ sẵn JWT, OAuth hay đăng nhập bằng form; Spring Data tự sinh truy vấn từ tên phương thức như findByEmail; Spring Boot Actuator cung cấp giám sát sức khỏe và số liệu chỉ với vài dòng cấu hình; Spring Cloud hỗ trợ đầy đủ cho microservices. Tuy vậy, tác giả cũng công bằng: Go cho quyền kiểm soát hoàn toàn và minh bạch với câu SQL, dễ gỡ lỗi hơn các truy vấn JOIN phức tạp qua ORM. Go còn thắng ở mô hình vận hành đơn giản — chỉ là một tệp nhị phân biên dịch sẵn, không phải tinh chỉnh JVM hay bộ thu gom rác, khởi động gần như tức thì và có cơ chế xử lý đồng thời được tích hợp ngay trong ngôn ngữ.
Design-First Collaboration with AI
Rahul Garg viết trên martinfowler.com về “Implementation Trap” khi làm việc với trợ lý lập trình AI: AI sinh mã nhanh đến mức điểm dừng tự nhiên giữa suy nghĩ thiết kế và viết mã biến mất. AI vẫn đưa ra các quyết định thiết kế về phạm vi, ranh giới thành phần, luồng dữ liệu hay xử lý lỗi, nhưng chúng bị chôn lặng lẽ trong mã. Người xem xét phải cùng lúc đánh giá quá nhiều khía cạnh nên dễ bỏ sót, và sửa hiểu lầm ở giai đoạn thiết kế luôn rẻ hơn nhiều so với lúc đã triển khai. AI còn hay tự thêm tính năng không ai yêu cầu, điều tác giả gọi là “technical debt injection”.
Giải pháp là tái hiện buổi thảo luận trên bảng trắng qua năm cấp độ, từ trừu tượng đến cụ thể: Capabilities (yêu cầu cốt lõi), Components (các khối xây dựng), Interactions (luồng dữ liệu và giao tiếp), Contracts (chữ ký hàm, kiểu, interface) và cuối cùng là Implementation. Quy tắc then chốt là không viết mã cho đến khi cấp độ 5 được duyệt. Trong ví dụ xây dịch vụ thông báo, ở cấp Components tác giả đã gạt bỏ một lớp RetryQueue thừa vì BullMQ vốn có cơ chế thử lại sẵn; còn khi đã thống nhất Contracts, có thể yêu cầu AI viết kiểm thử trước khi triển khai. Không phải tác vụ nào cũng cần đủ năm cấp: tiện ích đơn giản bắt đầu từ cấp 4, tính năng nhiều thành phần bắt đầu từ cấp 1. Cách làm này tốn thời gian hơn và không đáng với việc vặt, nhưng rất đáng đầu tư cho các tính năng phức tạp cần bảo trì lâu dài.
The Two Kinds of Error
Evan Hahn chia lỗi phần mềm thành hai loại. Lỗi expected xảy ra trong vận hành bình thường và không phải lỗi của lập trình viên: người dùng nhập dữ liệu sai, mạng chập chờn, chương trình không có quyền truy cập. Những lỗi này có thể phục hồi, không nên dùng throw, raise hay panic mà nên trả về một kết quả lỗi (như kiểu Result) để buộc phía gọi phải xử lý, và chỉ cần ghi log ở mức WARN hoặc INFO. Lỗi unexpected thì lẽ ra không bao giờ xảy ra — vi phạm assertion, lỗi logic, dữ liệu không hợp lệ từ cơ sở dữ liệu — và là dấu hiệu của bug. Không nên cố phục hồi mà cứ để chương trình crash, ghi log ở mức ERROR hoặc FATAL; theo tác giả, crash gây phiền toái trước mắt nhưng về lâu dài khiến phần mềm đáng tin cậy hơn.
Ranh giới giữa hai loại phụ thuộc vào bối cảnh: với một bản mẫu hay kịch bản nhỏ, mọi lỗi đều có thể coi là unexpected; với phần mềm cho tàu thăm dò không gian chạy 50 năm, gần như mọi lỗi, kể cả hỏng phần cứng, đều phải được xem là expected. Muốn phần mềm đáng tin cậy hơn thì xu hướng chung là coi ngày càng nhiều lỗi là expected. Tác giả cũng nhận xét các ngôn ngữ như Rust hay Zig xếp nhiều lỗi vào loại expected, còn JavaScript và Python thì ngược lại, và ông thích trình biên dịch khắt khe hơn cho phần mềm production.
Secure Go Error Handling Best Practices
Bài viết trên blog JetBrains GoLand tập trung vào khía cạnh bảo mật của xử lý lỗi trong Go. Vì lỗi trong Go là giá trị chứ không phải ngoại lệ, việc để lỗi lan lên và bị trả thẳng cho người dùng dễ làm lộ đường dẫn, câu SQL, thông tin xác thực hay stack trace. Nguyên tắc đầu tiên là tách biệt rõ phần hệ thống thấy và phần người dùng thấy: tạo một kiểu lỗi chứa cả thông điệp nội bộ lẫn thông điệp công khai, trong đó Error() chỉ trả về thông điệp an toàn, còn chi tiết kỹ thuật chỉ đi vào log. Tiếp theo, thay vì ghi log cả đối tượng request có thể chứa mật khẩu, hãy dùng builder hoặc hàm hỗ trợ chỉ cho phép một danh sách trường metadata an toàn; và khi không muốn phía gọi lần ngược tới nguyên nhân gốc qua errors.Is hay errors.As, hãy bọc lỗi kiểu “opaque” thay vì fmt.Errorf với %w.
Khi lỗi đi qua ranh giới, cần kiểm soát theo ba mức: giữa các tầng nội bộ, bọc lỗi thô của cơ sở dữ liệu thành lỗi nghiệp vụ; giữa các dịch vụ, chuyển sang mã lỗi chuẩn như mã trạng thái gRPC hay định dạng JSON thống nhất; và ra ngoài người dùng cuối, chỉ trả về chuỗi hoặc mã tĩnh đã định nghĩa sẵn kèm mã định danh request, không bao giờ là thông điệp được sinh động. Về log, hãy dùng log có cấu trúc như log/slog, zap hay zerolog, chỉ ánh xạ những trường cần thiết để gỡ lỗi, và cài đặt một interface Redactor để che các trường nhạy cảm khi buộc phải ghi cả request.
AI Coding Tools, Java & Compounding Engineering
Markus Eisele tổng hợp bài nói của Aleksander Stensby tại NDC Manchester 2025, với luận điểm: nếu AI sinh mã tệ cho bạn, đó thường là vấn đề quy trình làm việc chứ không phải vấn đề mô hình. Stensby gọi cách khắc phục là Compounding Engineering — dạy dỗ công cụ theo thời gian như cách kèm một lập trình viên junior. Ngữ cảnh vừa quý vừa khan hiếm: quá ít thì mô hình bịa, quá nhiều thì mất tập trung, nên hãy xóa cuộc trò chuyện thường xuyên và lưu quyết định kiến trúc, ràng buộc, các hướng đã loại bỏ vào tệp markdown để nạp lại đúng những gì cần. Các tệp quy tắc như CLAUDE.md là một kho tri thức sống: mỗi khi sửa một bug tinh vi, hãy cập nhật tệp để lỗi đó không lặp lại.
Các thói quen khác gồm luôn lập kế hoạch trước khi viết mã để phát hiện ý tưởng tồi sớm, chia công việc thành các tác vụ nhỏ với một trách nhiệm duy nhất, chọn mô hình có chủ đích (mô hình nhanh cho việc lặp lại, mô hình suy luận sâu cho kiến trúc), tự động hóa bằng slash command và sub-agent, dùng MCP để AI tương tác với hệ thống thật, và luôn có lưới an toàn bằng Git cùng các điểm khôi phục. Agent chạy nền vẫn phải được xem xét kỹ trước khi merge. Giá trị của kỹ sư nằm ở gu thẩm mỹ và khả năng phán đoán — nhận ra điều gì sai dù kiểm thử vẫn qua — thứ AI chưa làm tốt. Đầu tư vào quy trình thì kết quả tích lũy dần; không đầu tư thì chỉ nhận về “sự tầm thường nhanh hơn”.
YAML? That’s Norway Problem
Bài viết đào sâu “vấn đề Na Uy” của YAML: mã quốc gia NO bị các thư viện phổ biến như PyYAML hiểu thành giá trị boolean false thay vì chuỗi. Nguyên nhân là kiểu ngầm định cho scalar không có dấu ngoặc: bản nháp cuối YAML v1.0 (2004) và v1.1 (2005) coi các từ như yes/no hay on/off là boolean. Bản YAML 1.2 (2009) đã bỏ hành vi này, nhưng PyYAML và LibYAML — hai thư viện “chính thức” với những yêu cầu hỗ trợ v1.2 còn bỏ ngỏ từ 2016–2017 — vẫn chỉ theo v1.1. LibYAML lại nằm sâu trong cây phụ thuộc của rất nhiều công cụ nên thay đổi càng chậm và rủi ro.
Nhiều bài viết dừng ở giải pháp đặt giá trị trong dấu ngoặc kép ("NO"), nhưng tác giả cho thấy bức tranh phức tạp hơn. Trong hệ sinh thái Go, gopkg.in/yaml.v3 phổ biến nhất nhưng đã ngừng bảo trì và hỗ trợ lẫn lộn v1.1 với v1.2, còn thư viện được bảo trì tích cực phổ biến nhất lại mặc định theo v1.2; công cụ yq cũng mặc định theo v1.2, và Kubernetes ra mắt phương ngữ KYAML năm 2025 với mục tiêu an toàn, ít mơ hồ hơn. Tài liệu trên mạng thường nhầm phiên bản đặc tả, trong khi người dùng thì tranh cãi vấn đề đã được giải quyết hay chưa. Kết luận: hệ sinh thái YAML đang phân mảnh nhưng dần chuyển sang phiên bản chặt chẽ hơn; các dự án mới nhiều khả năng sẽ dùng thư viện mới không còn vấn đề này, còn các thư viện cũ, vốn vẫn phổ biến hơn, thì mắc kẹt với đặc tả cũ.
Bài viết đã được viết lại bởi Claude Code với Opus 5.5 vào ngày 27/09/2026.