*Bài viết nằm trong series "Hôm nay tôi lười"
Đúng vậy, mình lười, và mình nghĩ ai trong chúng ta đều sẽ có một phần như thế.
Đặc biệt khi chúng ta phải thực hiện những công việc mang tính lặp đi lặp lại mỗi ngày, điều đó sẽ khiến mọi người kiệt quệ cả về tinh thần lẫn cảm hứng mất.
Vì vậy trong series này, mình muốn thử biến đổi sự lười biếng thành động lực để tự động hoá.
Chúng ta sẽ cùng đi qua một chuỗi những bài viết nơi mình chia sẻ cách xây dựng những agent để có thể giải quyết những vấn đề đó giúp mình nhé.
Còn bây giờ, ngồi xuống, tựa lưng vào ghế, lấy vội một ly trà sữa và... bắt đầu thôi.
Vấn đề bắt đầu
À từ từ đã, để mọi thứ dễ hiểu hơn thì mình nghĩ chúng ta nên nắm rõ hơn về CLI(Command Line Interface) là gì.
CLI (Command Line Interface) là cách bạn tương tác với hệ thống hoặc dịch vụ thông qua dòng lệnh — thay vì bấm nút hay click trên UI, bạn chỉ cần gõ một câu lệnh trong terminal.
Chúng ta hẳn đã quen với các câu lệnh như git hay docker, đó là những ví dụ của CLI.
Về mặt tính năng, các hệ thống sẽ thích dùng CLI hơn vì nó nhanh, tính tự động cao, độ chính xác cao và dễ dàng tích hợp thay vì tương tác trực tiếp bằng GUI (Graphical User Interface)
Một khoảng thời gian trước, mình có task làm một CLI cho một số dịch vụ trên nền tảng GreenNode. Chúng ta sẽ tạm gọi nó là Watermelon CLI (vì Watermelon là nickname của mình). Mô hình hoạt động giai đoạn đầu đáp ứng được các tiêu chí cơ bản để vận hành, bao gồm việc gọi các APIs(Application Programming Interfaces) lên các dịch vụ trên GreenNode, mỗi dịch vụ là một subcommand riêng.
Mọi thứ đều ổn, CLI được phát hành, cài được, dùng được, trời đẹp, Phúc Long VNG có voucher,... Cho đến khi Teams mình ting ting: “Gia Anh ơi, service xyz có tính năng mới ấy, cập nhật lại CLI em nhé”.
Được thôi, mình sẽ đọc OAS(OpenAPI Specification) mới của dịch vụ xyz ấy để xem tính năng mới là gì, sau đó nhờ Claude / Cursor update giúp, push lên và đánh tag để release, và... Khoan đã, cứ mỗi lần một dịch vụ bất kỳ có cập nhật gì thì chẳng lẽ mình sẽ phải lặp đi lặp lại cái quy trình đó hay sao? Và lỡ trong quá trình xem spec mình có sai sót thì cũng sẽ phải tốn khá nhiều công sức để làm lại.
Các anh lead của mình cũng nhận ra vấn đề đó khi thấy một số schema chưa được cập nhật kịp thời, và có khuyên là nên làm một con agent hoặc một giải pháp để có thể cập nhật tự động CLI mỗi khi một dịch vụ có tính năng mới, hoặc các API cũ có sự thay đổi.
Ý tưởng ban đầu
Thực ra trong đầu mình lúc đó chưa hình dung rõ sẽ phải làm cái gì, nhưng hình dung sơ bộ hệ thống sẽ trông như thế này:
Mục tiêu của mình là sẽ tạo được một con agent có:
- Đầu vào là những spec mới khi một dịch vụ có cập nhật.
- Đầu ra sẽ là một PR trên repo CLI hiện tại để mình có thể review và release.
Về một mặt nào đó ta có thể tưởng tượng mình đang xây dựng một con mini Cursor chẳng hạn. Một số tính năng chính ta cần phải cân nhắc khi thiết kế agent này:
- Tự động cập nhật bản OAS mới nhất, có thể từ việc pull tự động theo chu kỳ hoặc các dịch vụ chủ động call khi có phiên bản mới.
- Agent này phải hiểu được cấu trúc của CLI hiện tại, các quy ước code, cách đặt tên và những ràng buộc nhất định.
- Phải có luồng tự sửa lỗi, mình không muốn có một cái PR mà thậm chí không build được.
- Agent phải được deploy ở một môi trường ổn định, chạy 24/7 và có thể tự động scale được.
- Dùng LLM một cách thông minh, mình không muốn chỉ cập nhật CLI mà bill gửi về mấy trăm ngàn đồng mỗi lần chạy, nó sẽ thật đau đớn biết mấy...
Được rồi, bắt tay vào làm thôi nhỉ?
Làm sao để agent biết được kiến trúc CLI hiện tại
Ví đây là một hệ thống tự động nên ta không thể dùng Claude hay Cursor như cách ta vẫn đang dùng hiện tại được mà chỉ có thể gọi LLM thuần thôi. Mã nguồn của CLI cũng không nằm hẳn trong cùng một môi trường với agent ngay từ đầu mà chỉ có thể clone về vì tương lai ta có thể đổi luồng chạy hoặc thay bằng một loại CLI khác. Vậy thì làm cách nào để agent có thể biết được CLI của chúng ta sẽ trông như thế nào, có những thành phần nào và hoạt động ra sao?
Với các AI coding tool hiện tại thì sẽ có khá nhiều cách tiếp cận, và chúng thật ra cũng khá thú vị ấy, mình sẽ hẹn các bạn ở một bài viết để nói sâu hơn về vấn đề này trong tương lai nhé. Còn hiện tại, mình lựa chọn một cách tiếp cận cổ điển, tôn trọng, đủ đơn giản để giản lược luồng hoạt động nhưng cũng đủ chi tiết để agent có thể làm theo: mình viết hẳn nó ra thành một file hướng dẫn luôn.
Từ từ đã, mình biết nghe nó không “agentic” lắm, nhưng tin mình đi, cách này đủ hiệu quả để chúng ta có thể bắt đầu làm. Thay vì bắt agent phải clone về và đọc hiểu cả một cái mã nguồn to bự của CLI, nó chỉ cần đọc một file hướng dẫn và xem nó như một Gold Standard để khi cần thay đổi một module nào đó, chỉ cần tuân theo đúng file đó là được.
Một số section tiên quyết trong file, mình đặt tên là AGENTS.md (để khỏi bị nhầm lẫn với README.md, vốn dành cho người đọc)
Mô tả nhiệm vụ và danh tính
Những agent đọc file này cần biết chính xác vai trò, nhiệm vụ và định danh của mình trước khi đụng đến mã nguồn CLI. Một số ví dụ:
Role: Senior MLOps & Go Engineer specialising in CLI development. You build production-quality, POSIX-compliant, script-safe tools. You write minimalistic "Senior Dev" code — no noise, no over-engineering, no unnecessary abstractions.
Mission: Consume an incoming OpenAPI/Swagger specification diff, translate API surface changes into the watermelon CLI, keep every existing behaviour intact, and leave the codebase cleaner than you found it.
Principles (non-negotiable):
• Backward compatibility is sacred. Never remove a flag, command, or behaviour.
• Every change must be observable. Update CHANGELOG.md with every commit-worthy change.
• .......
Với section này, agent có thể biết chính xác ngôn ngữ, framework, vai trò và những nguyên tắc làm việc trước khi bắt tay vào việc thay đổi mã nguồn. Nó sẽ giới hạn rất nhiều về tầm vực sinh code của agent, hạn chế những gì lan man và không phù hợp.
Kiến trúc mã nguồn CLI
Một trong những mục tối quan trọng, giúp agent có thể có một cái nhìn tổng quan về mã nguồn và cách nó đang phân tầng module hiện tại, thay vì phải xây dựng một knowledge graph từ con số 0.
Giao thức thực thi
Đây là mục ta sẽ quy định các bước để thay đổi một subcommand hoặc thêm vào một subcommand mới. Bạn định nghĩa càng rõ ràng thì agent sẽ càng dễ làm theo mà ít mắc sai sót hơn. Đây thực chất không phải một câu nói suông mà nó hoàn toàn có thể ảnh hưởng đến việc lựa chọn mô hình ngôn ngữ lớn: càng định nghĩa rõ ràng -> mô hình đơn giản cũng có thể xử lý được -> có những lựa chọn ít tốn kém hơn về mặt tài nguyên và chi phí.
Một số bước quan trọng nhất trong section này như sau:
Phân tích sự khác biệt
Đây là một cách tiếp cận mình nghĩ sẽ hiệu qua hơn là sinh code từ hư không, bởi vì nếu chúng ta nhìn nhận lại thì thực ra mỗi một dịch vụ đều sẽ có một luồng chạy của riêng nó, và mỗi khi có cập nhật thì cũng chỉ thay đổi 1,2 tính năng mỗi lần release chứ hiếm khi nào đập đi xây lại cả một dịch vụ được. Từ đó ý tưởng về phân tích điểm khác biệt ra đời: thay vì bạn xem một đầu vào OAS như một dịch vụ hoàn toàn mới, ta sẽ xem nó như một phiên bản mới của dịch vụ hiện có và tập trung phân tích những gì cần thay đổi để thực hiện cập nhật CLI.
Follow these phases in strict order for every spec change. Do not skip or reorder.
Phase 1 — DIFF & ANALYSE
1. Read the incoming OpenAPI/Swagger specification carefully.
2. Identify every change relative to the current codebase:
• New endpoints → need new client methods + cmd wiring
• Modified request/response schemas → need model updates + potential flag changes
• Removed endpoints → mark deprecated in CHANGELOG.md, do NOT remove cmd code
• Renamed fields → add new field, keep old field as // Deprecated: use NewField with backward-compat zero value
3. Categorise changes by service: identity, runtime, memory, or new service.
4. If a new service is introduced, create the full internal/<service>/ package following the patterns in Section 4.
5. List all changes you intend to make before writing code. Think first.
Tầm vực dịch vụ
Phân hoạch tầm vực của module CLI. Một file chỉ nên thuộc về một dịch vụ. KHÔNG NÊN trộn lẫn các dịch vụ với nhau trong cùng 1 file. Khi đó nếu dịch vụ A thay đổi thì agent sẽ chỉ cần lấy đúng các file của dịch vụ đó ra để phân tích và so sánh chứ không cần phải load toàn bộ repo. Điều này vừa đảm bảo agent sẽ không tốn quá nhiều token để đọc những thứ không cần thiết và tính độc lập giữa các module cũng được đảm bảo.
Kiểm thử
Một trong những cổng hải quan trước khi con agent của chúng ta bay bổng một cách quá đà là phải đảm bảo unit test pass hết cái đã. Agent sẽ phải pass hết các unit test có sẵn và nếu là dịch vụ mới thì phải tự viết thêm. Unit test thực ra cũng không đảm bảo CLI chạy đúng 100%, nhưng ít ra đó là một trong những cơ chế khiến CLI có một cái gì đó đảm bảo về mặt chất lượng hơn. Code Coverage là một trong các tiêu chí đó, hiện mình đang set nó ở ngưỡng 75%.
Phase 5 — TESTS
Rules:
• Every new or modified internal//client.go method must have a corresponding test using httptest.NewServer.
• Every new or modified internal//models.go struct must have a marshal/unmarshal round-trip test where the spec provides an example payload.
• Every new cmd/helpers.go function must be unit-tested in cmd/helpers_test.go.
• Target: ≥ 75% line coverage on every package you touch. Run go test -cover ./... to verify.
• Tests must not make real network calls. Use httptest.NewServer and inject the test server URL.
• Table-driven tests are preferred for functions with multiple input cases.
• Test file naming: _test.go in the same package.
CHANGELOG
Mỗi khi cập nhật một cái gì đó, bất cứ thứ gì, agent sẽ phải liệt kê những gì sẽ thay đổi vào CHANGELOG.md, mình không muốn con agent sẽ gen một đoạn mã random có thể kích hoạt Skynet và huỷ diệt loài người vào trong code CLI của mình. Do đó: mọi thay đổi đều phải tường minh.
Phase 6 — CHANGELOG
Update CHANGELOG.md before declaring the task complete.
Format (Keep a Changelog / Semantic Versioning):
## [Unreleased]
### Added
- <brief description of new command/flag/endpoint, one bullet per item>
### Changed
- <brief description of changed behaviour, one bullet per item>
### Breaking Changes
- <command> — <what changed and migration path with before/after shell examples>
### Dependencies Required
- `<module>@<version>` — <why it is needed> — reviewer must run `go get <module>@<version>` manually
Rules:
• Every spec-driven change gets at least one bullet.
• Breaking changes must include shell examples showing old vs new usage.
• If no third-party dependencies are needed, omit the ### Dependencies Required section entirely.
• Do NOT touch existing versioned sections (e.g. ## [1.0.0]). Only update ## [Unreleased].
Vùng cấm agent
Cho agent thực thi nhiều thứ thì đương nhiên ta cũng sẽ phải thêm vào một số ràng buộc, định nghĩa các vùng cấm mà agent không bao giờ được phép động vào.
Checklist cho mọi thay đổi
Cuối cùng, ta sẽ định nghĩa bộ checklist để agent có thể làm theo và đảm bảo trong trường hợp em nó mắt nhắm mắt mở mà quên đi mục nào đó thì vẫn có thứ có thể giữ nó lại.
Luồng hoạt động cuối cùng
Bước 1. Agent nhận được OAS mới.
Bước 2. Thực hiện phân tích sự khác biệt dựa trên OAS mới và OAS được cache lại từ lần trước. Lưu ý nếu đây là một dịch vụ mới (chưa có cache) thì sẽ thực hiện xây dựng đầy đủ tính năng của dịch vụ đó (nặng về workload, nhưng chỉ làm 1 lần).
Bước 3. Sau đó phân tích CLI cần thay đổi những gì dựa trên kiến trúc mã nguồn và file AGENTS.md, đưa ra quyết định.
Bước 4. Dựa trên quyết định đó, LLM Manager sinh code mới cho CLI và bắt đầu luồng tự điều chỉnh.
Bước 5. Luồng tự điều chỉnh được thiết lập bằng lệnh build, nếu có lỗi thì sẽ bắt luồng stderr để feed lại cho agent để fix.
Bước 6. Nếu thành công, tạo PR lên repo CLI hiện tại, lưu lại OAS mới và kết thúc luồng hoạt động.
Deploy ở đâu đây nhỉ?
Có khá nhiều lựa chọn để mình có thể deploy con agent này do về bản chất thì nó sẽ không cần quá nhiều tài nguyên để chạy, đa phần các tác vụ nặng nằm ở đoạn gọi LLM là chính.
Do đó có 2 vấn đề mình sẽ phải cân nhắc chọn:
1. Chọn LLM nào, host ở đâu để tối ưu chi phí?
2. Chọn môi trường deploy nào ổn định, autoscale và nhanh gọn để bắt đầu?
Đau đầu nhỉ? Mình đùa thôi, mình chả gặp vấn đề gì trong việc suy nghĩ về cái này cả. GreenNode MaaS và GreenNode AgentBase Engine là 2 stack quá phù hợp cho công việc này, lại còn là hàng nhà trồng được nên việc quản lý sẽ dễ dàng hơn rất nhiều.
GreenNode Model as a Service cung cấp cho ta một loạt những model tự host và có thể dùng ngay khi cần, và cũng rất tiện lợi khi hỗ trợ theo chuẩn OpenAI.
Còn về môi trường chạy, mình chọn AgentBase Runtime vì nó rất dễ deploy, tự động scale và tính ổn định cao. Các bạn có thể đọc thêm ở bài viết này.
Một số kết quả
Từ giờ, khi có một cập nhật nào về dịch vụ thì agent sẽ tự động thực hiện luồng chạy của mình, và sau khi xong, nó sẽ tự động tạo một PR để mình có thể review.
Trên đây là một PR tự động cập nhật CLI khi một dịch vụ có cập nhật thêm một số tính năng liên quan đến OpenClaw với các thay đổi được liệt kê đầy đủ ở CHANGELOG, giúp mình dễ biết được các thay đổi và quyết định nhanh hơn.
Và đoán xem, dù con này build cũng mất một mớ thời gian đấy, tuy nhiên từ sau khi có nó, việc cập nhật là tự động và dễ dàng hơn nhiều, khiến mình có dư dả thêm thời gian để làm một số thứ khác, hoặc chí ít là có thể làm một ly trà sữa chill chill ngoài bờ sông Sài Gòn...
Còn gì để làm không nhỉ?
Về mặt kỹ thuật, agent này vẫn còn khá nhiều chỗ có thể cải thiện, từ việc tối ưu để có thể phân tích module một cách mịn hơn, ở mức độ hàm thay vì là file, hoặc xử lý bất đồng bộ để có thể đồng thời chạy nhiều request change đồng thời trên cùng một CLI,... Nhưng
những thứ đó có thể đợi.... vì mình vừa bị bên QC vỗ vai và hỏi nhẹ nhàng: “Ơ thế mỗi lần nó cập nhật thì chị phải test lại hết à, do unit test cũng đâu đảm bảo được nó sẽ work khi release”. Cũng đúng nhỉ, agent sinh một mớ code thì QC cũng phải viết testcase mới, kiểm tra thử trên môi trường prod thì mới dám release, nghe có vẻ sẽ có rất nhiều việc cho QC đây. Thôi thì vấn đề agent đẻ ra thì... ta để agent giải quyết vậy.
Hẹn gặp các bạn ở bài viết lần sau, nơi chúng ta sẽ cùng đi qua quá trình xây dựng một con agent khác để thực hiện sinh testcase và chạy thực tế integration test để đảm bảo chất lượng đầu ra ổn định nhất.
Và cho đến lần đó, mình là Watermelon, cảm ơn các bạn đã đọc bài viết của mình.








