Agent Coding 101Chapter 4 of 10

Từ feature lớn tới spec và các task nhỏ

Cách làm rõ một feature còn mơ hồ, viết spec đủ dùng rồi chia thành các task mà agent làm được và mình kiểm tra được.

howznguyen

howznguyen

20 min read

Từ feature lớn tới spec và các task nhỏ

Ở bài 2, mình có nói một nguyên tắc khá đơn giản: đừng mở coding agent lên rồi giao luôn một feature khổng lồ.

Chọn một task nhỏ trước. Cho agent đọc code. Nói rõ giới hạn. Xem kế hoạch. Review diff.

Nhưng rồi sẽ tới lúc mình không thể chỉ sửa bug nhỏ mãi. Project thật vẫn có những việc kiểu:

Plain Text

Đây là lúc câu hỏi khó hơn xuất hiện: task nhỏ là nhỏ tới đâu, và chia kiểu gì để các phần ghép lại vẫn thành một feature đúng?

Hồi đầu mình hay để agent tự chia. Nó thường trả về một kế hoạch nhìn rất có tổ chức: database, backend, frontend, test. Đọc qua thấy hợp lý, gọn gàng như slide thuyết trình.

Tới lúc làm mới lộ ra vấn đề. Mỗi task sửa quá nhiều thứ. Thứ tự phụ thuộc không rõ. Frontend chờ backend. Test bị đẩy xuống cuối. Một quyết định nhỏ ở task đầu làm ba task sau phải đổi theo, giống rút nhầm một miếng trong trò xếp gỗ.

Bài này là cách mình đang dùng để biến một yêu cầu lớn thành , rồi từ spec chia ra các task mà agent có thể hiểu, làm, và mình có thể review từng phần.

Tính năng, đặc tả và task là ba tầng khác nhau

Ba từ này rất dễ bị dùng lẫn với nhau.

Với mình, feature idea là điều mình muốn người dùng làm được sau cùng.

Spec là bản mô tả cách feature cần hoạt động sau khi những chỗ mơ hồ đã được làm rõ và review. Nó giữ mục tiêu, yêu cầu, giới hạn, tiêu chí hoàn thành, các quyết định đã chốt, phần chưa làm và câu hỏi còn mở.

Task là một bước triển khai đủ nhỏ để làm và kiểm tra riêng. Nhiều task ghép lại để hoàn thành một spec.

Flow trông như vầy:

Plain Text
Workflow từ feature idea qua spec và các task nhỏ tới bước verify
Ý tưởng tính năng được làm rõ thành đặc tả, rồi mới chia thành task và kiểm tra ngược lại theo kết quả ban đầu.

Đặc tả không phải danh sách file sẽ sửa. Phần đó thuộc kế hoạch thực thi.

Spec trả lời "hệ thống cần hoạt động thế nào". Task và kế hoạch trả lời "mình sẽ thay đổi code theo những bước nào".

Ví dụ:

Plain Text

Nghe chỉ có một câu. Nhưng bên dưới có khá nhiều câu hỏi:

  • Ai được quyền gửi lời mời?
  • Mời bằng email hay username?
  • Link có hết hạn không?
  • Một email có thể nhận nhiều lời mời không?
  • Người đã là member thì sao?
  • Accept invite sẽ tạo account hay chỉ dành cho người đã đăng nhập?
  • Có cần resend hoặc revoke không?

Nếu đưa nguyên câu feature idea cho agent rồi bảo "implement", những câu chưa trả lời sẽ không tự biến mất.

Agent sẽ tự chọn câu trả lời. Nó không cố tình giành quyền làm product, chỉ là chỗ trống đang nằm đó và nó cần lấp vào để tiếp tục.

Và vì code của nó thường nhìn khá trôi chảy, mình có thể chỉ phát hiện giả định sai sau khi nó đã sửa cả database, API và giao diện.

⚠️

Spec không loại bỏ mọi điều chưa chắc chắn. Nó chỉ kéo các quyết định quan trọng ra trước khi agent bắt đầu sửa code, thay vì để chúng bị chôn trong diff.

Bắt đầu đặc tả bằng điều người dùng làm được

Trước khi chia task, mình cố viết lại feature bằng điều người dùng thật sự có thể làm và nhìn thấy.

Thay vì:

Plain Text

Mình viết:

Plain Text

Bây giờ spec bắt đầu có hình dạng rõ hơn.

Nó có ba user flow chính:

  1. Tạo lời mời
  2. Chấp nhận lời mời
  3. Quản lý lời mời đang chờ

Và có vài quy tắc dùng chung: ai có quyền mời, lời mời trùng xử lý ra sao, khi nào hết hạn và token được bảo vệ thế nào.

Đây là điểm bắt đầu tốt hơn nhiều so với việc nhìn vào cây thư mục rồi nói: "task một làm database, task hai làm API, task ba làm UI".

Nhìn codebase trước khi khoá đặc tả và chia task

Mình không nghĩ nên chia task chỉ dựa trên yêu cầu sản phẩm.

Cần nhìn codebase trước.

Không phải để đọc hết repo. Chỉ để biết feature mới sẽ đi qua những phần nào và project đã có pattern gì để tái sử dụng.

Với ví dụ invitation, mình có thể giao cho agent một task chỉ để tìm hiểu như sau:

Plain Text

Đầu ra của bước này không phải code. Nó là bản đồ để mình biết đang đứng ở đâu, đỡ chia task dựa trên một kiến trúc chỉ tồn tại trong trí tưởng tượng.

Agent có thể phát hiện project đã có bảng workspace_invitations, hoặc ngược lại chưa có email service. Nó có thể thấy permission nằm trong domain service chứ không nằm ở route. Nó có thể tìm ra app đã có pattern signed token cho password reset.

Những thông tin đó ảnh hưởng trực tiếp tới quyết định nào cần khoá trong spec và cách chia task sau đó.

Nếu chưa nhìn code mà đã chốt spec lẫn kế hoạch, mình rất dễ viết yêu cầu từ những điều tự đoán rồi tạo task cho một kiến trúc tưởng tượng.

Cách chia nghe hợp lý nhưng dễ gây kẹt

Một plan rất phổ biến trông như vầy:

Plain Text

Mình từng chia như vậy khá nhiều.

Nó gọn. Mỗi task ứng với một layer. Người đọc nhìn vào là hiểu phần kỹ thuật nào đang được làm.

Nhưng nó có vài vấn đề.

Sau task 1, chưa có gì dùng được. Sau task 2, vẫn chưa có luồng người dùng hoàn chỉnh. Task 3 có thể phát hiện cấu trúc API không hợp với giao diện. Task 4 mới lộ ra giả định sai ở schema, lúc đó phải quay lại sửa cả ba task trước.

Test để cuối cũng tạo ra một tín hiệu khá tệ: ba task đầu được xem là “xong” dù chưa có cách kiểm tra chúng hoạt động đúng.

Kiểu chia theo layer không phải lúc nào cũng sai. Với migration lớn hoặc refactor hạ tầng, đôi khi nó là lựa chọn hợp lý.

Nhưng với feature hướng tới người dùng, mình thích chia theo vertical slice hơn.

Ẩn dụ một khối nhiều tầng được chia theo technical layer và được cắt thành vertical slice
Chia theo layer tách riêng từng tầng kỹ thuật. Vertical slice cắt xuyên đủ database, API, UI và test để tạo một kết quả có thể kiểm tra.

Lát cắt dọc là phần làm được một việc trọn vẹn

Vertical slice đi qua những tầng kỹ thuật cần thiết để hoàn thành một việc nhỏ nhưng trọn vẹn.

Với invitation feature, một slice có thể là:

Plain Text

Task này có thể chạm vào persistence, domain logic, API, một form UI nhỏ và test liên quan. Nghe có vẻ nhiều layer hơn một task backend thuần túy.

Nhưng tất cả thay đổi đều phục vụ một kết quả duy nhất.

Khi task xong, mình có thứ để quan sát và kiểm tra:

  • Admin nhập email
  • Request được gửi
  • Invitation được lưu
  • Người không có quyền bị từ chối
  • Email trùng được xử lý theo rule đã chốt

Slice tiếp theo là:

Plain Text

Slice này lại đi qua token validation, membership logic, accept route, UI state và test. Nhưng nó có một điểm kết thúc rõ ràng.

Mình thấy task kiểu này dễ review hơn vì mọi file trong diff đang cùng kể một câu chuyện.

Một task tốt không nhất thiết chỉ chạm một layer. Nó cần tạo ra một thay đổi có cùng mục tiêu và có thể verify độc lập.

Task nhỏ tới mức nào là vừa

Không có số dòng code hay số file cố định.

Một task sửa hai file vẫn có thể rất khó nếu cách hoạt động còn mơ hồ. Một task sửa tám file vẫn có thể dễ review nếu đó là một thay đổi cơ học với quy tắc rõ ràng.

Mình thường dùng vài câu hỏi để kiểm tra.

Task có một kết quả rõ không?

Mình có thể mô tả kết quả của task trong một câu không?

Tốt:

Plain Text

Quá rộng:

Plain Text

Task có tự kiểm tra được không?

Sau khi task xong, có test, command hoặc flow cụ thể để chứng minh nó hoạt động không?

Nếu câu trả lời là "phải chờ ba task sau mới test được", task có thể đang bị chia sai ranh giới.

Agent có phải ôm quá nhiều quyết định cùng lúc không?

Nếu một task vừa thiết kế token, vừa chọn email provider, vừa sửa permission, vừa làm UI, vừa xử lý analytics, context sẽ loãng rất nhanh.

Có thể cần chốt một quyết định riêng trước, hoặc thu hẹp giai đoạn đầu.

Diff có review được trong một lượt không?

Mình không dùng một giới hạn cứng. Nhưng nếu mở diff lên mà phải nhớ năm cách hoạt động khác nhau cùng lúc, đó thường là dấu hiệu task đang ôm quá nhiều việc.

Nếu task hỏng, có quay lại được không?

Task có boundary rõ sẽ dễ revert, sửa lại hoặc giao tiếp tiếp hơn. Task thay đổi nửa hệ thống trong một lượt thì rất khó biết phần nào nên giữ.

Viết rõ task nào phụ thuộc task nào

Chia được danh sách task chưa đủ. Cần biết task nào phụ thuộc task nào.

Với invitation feature, plan có thể là:

Plain Text

T2 cần dữ liệu và token từ T1, nên không thể làm trước.

T3 cũng cần invitation tồn tại, nhưng sau T1 thì nó gần như độc lập với T2.

T4 nhìn giống một task “dọn trường hợp biên”, nhưng cần cẩn thận. Các quy tắc quan trọng như phân quyền hoặc token không được để trống ở task trước. T4 chỉ nên gia cố những gì đã có, không phải vá lại phần bảo mật cơ bản đáng lẽ phải làm từ đầu.

Viết thứ tự phụ thuộc ra giúp mình tránh hai lỗi:

  • Bắt đầu task khi prerequisite chưa ổn định
  • Cho hai task cùng sửa một vùng code rồi tốn công merge lại

Không cần vẽ graph đẹp. Một danh sách depends on là đủ.

Ví dụ từ một đặc tả tới các task

Giả sử sau khi tìm hiểu và qua vài vòng hỏi đáp, mình có một đặc tả rút gọn như sau:

Plain Text

Phần Không làm rất quan trọng.

Không ghi thì agent có thể thấy "invitation system" và tiện tay thêm resend, role selector, email preview, audit log. Toàn những thứ nghe hợp lý nhưng không thuộc phase này.

Tiêu chí hoàn thành trong đặc tả mô tả kết quả của cả tính năng. Khi chia task, mỗi task sẽ có tiêu chí hẹp hơn và liên kết ngược về phần đặc tả mà nó thực hiện.

Từ đặc tả trên, mình chia thành bốn task thực thi.

Task 1: Admin tạo lời mời

Kết quả: admin nhập email và tạo được một lời mời đang chờ.

Phạm vi:

  • Thêm invitation model hoặc dùng schema hiện có
  • Kiểm tra quyền admin
  • Chặn member hiện tại và pending invitation trùng
  • Tạo token an toàn với expiry 7 ngày
  • Thêm API và form UI tối thiểu
  • Gửi email qua pattern hiện có hoặc fake adapter trong test
  • Viết test cho happy path, permission và duplicate

Task này khá dày, nhưng chỉ có một việc: tạo lời mời.

Nếu codebase lớn hoặc email integration phức tạp, mình có thể tách phần email adapter thành prerequisite riêng. Nhưng không nên tách chỉ vì "email nằm ở folder khác".

Task 2: Người nhận chấp nhận lời mời

Kết quả: người đăng nhập đúng email dùng token hợp lệ để trở thành thành viên.

Depends on: Task 1.

Phạm vi:

  • Validate token, expiry và trạng thái pending
  • Kiểm tra email của user hiện tại
  • Tạo membership trong cùng một phù hợp
  • Đánh dấu invitation đã accepted
  • Thêm accept page với success và error state
  • Viết test cho valid token, expired token, wrong email và replay

Task 3: Admin xem và thu hồi lời mời

Kết quả: admin thấy danh sách lời mời đang chờ và thu hồi được một lời mời.

Depends on: Task 1.

Phạm vi:

  • Query pending invitations của workspace
  • Hiển thị email, thời gian tạo và expiry
  • Thêm revoke action với permission check
  • Không cho accept token đã revoke
  • Viết test cho list, revoke và unauthorized access

Task 4: Kiểm tra toàn luồng và gia cố ranh giới

Kết quả: toàn bộ giai đoạn lời mời hoạt động đúng khi ghép lại.

Depends on: Task 2 và Task 3.

Phạm vi:

  • Chạy xuyên suốt create, accept và revoke
  • Review transaction boundary
  • Kiểm tra log không lộ raw token
  • Kiểm tra race condition cho duplicate invitation và double accept
  • Cập nhật docs hoặc instruction nếu có convention mới

Task cuối không phải chỗ gom test của ba task trước. Mỗi task vẫn phải tự có test liên quan.

Nó là vòng integration và hardening sau khi các slice đã đứng được độc lập.

Tiêu chí hoàn thành phải nhìn thấy được

Task nhỏ nhưng mơ hồ thì agent vẫn phải đoán.

Ví dụ:

Plain Text

Mấy câu này nghe ổn nhưng không có điểm kiểm tra rõ.

Mình thích viết bằng những điều có thể quan sát:

Plain Text

Mỗi dòng đều có thể quan sát qua UI, API, database state hoặc test.

Đó là danh sách agent có thể dùng để tự kiểm tra trước khi báo xong. Mình cũng dùng đúng nó khi review, khỏi phải mỗi bên giữ một định nghĩa riêng về chữ “xong”.

Một mẫu task để dùng lại

Đây là format ngắn mình thấy đủ dùng:

Plain Text

Không phải task nào cũng cần điền đủ mọi dòng.

Nhưng mục tiêu, tiêu chí hoàn thành, phần không làm và điều kiện xong là bốn phần mình cố giữ. Chúng giúp agent biết đi đâu, giúp mình biết khi nào dừng và giảm chuyện task đang nhỏ bỗng ăn no rồi phình ra giữa đường.

Với Knowns, đây chính là chỗ của /kn-spec

Mình đang build Knowns nên workflow này cũng là lý do có skill /kn-spec.

Khi bắt đầu từ một idea như workspace invitation, mình có thể chạy:

Plain Text

Mục tiêu của bước này chưa phải tạo task hay sửa code.

/kn-spec đi theo một vòng: tìm vùng còn mơ hồ, hỏi để kéo quyết định ra khỏi đầu mình, khoá những gì đã thống nhất, rồi viết đặc tả gồm yêu cầu, tiêu chí hoàn thành, tình huống, phần không làm và câu hỏi còn mở.

Spec vẫn là draft cho tới khi mình review và approve.

Sau khi approve, nếu muốn chạy full workflow từ plan tới implement, review và verify:

Plain Text

Nếu chỉ muốn generate task rồi đi từng task bằng tay:

Plain Text

Điểm mình thích ở cách này là task không đứng riêng lẻ. Nó liên kết về đặc tả, biết mình thực hiện tiêu chí nào, và tới lúc kiểm tra có thể đối chiếu phần đã làm với kết quả ban đầu.

Nhưng không phải việc gì cũng cần /kn-spec.

Một bug nhỏ, thay đổi câu chữ hoặc trạng thái rỗng có phạm vi rõ thì tạo task trực tiếp sẽ nhẹ hơn:

Plain Text

Đặc tả có ích khi tính năng có nhiều cách hoạt động, quyết định, trường hợp biên hoặc đi qua nhiều phần hệ thống. Nếu yêu cầu đã rõ và việc chỉ mất một task, thêm đặc tả có thể chỉ làm quy trình nặng hơn.

Để agent đề xuất kế hoạch, nhưng đừng nhường luôn quyền quyết định

Agent khá giỏi tìm pattern và liệt kê vùng code bị ảnh hưởng. Mình vẫn dùng nó để draft plan.

Nếu chưa dùng một workflow như /kn-spec, prompt thủ công có thể như vầy:

Plain Text

Nhưng kế hoạch của agent vẫn chỉ là một đề xuất.

Mình cần review xem nó có tự đoán cách sản phẩm hoạt động không, có để test tới cuối không, có task nào quá rộng không, có hai task cùng sửa một vùng code không.

Agent giúp mình nhìn nhanh hơn. Nó không thay mình quyết định product nên hoạt động thế nào.

Khi nào không cần spec hoặc không cần chia

Không phải thay đổi nào cũng cần một spec, năm task, graph dependency và tài liệu dài.

Nếu thay đổi có một kết quả rõ, chạm một vùng code nhỏ, có test trực tiếp và không chứa quyết định mới, cứ để nó là một task.

Ví dụ:

Plain Text

Tách tiếp thành "tạo component", "thêm copy", "viết test" chỉ làm workflow nặng hơn.

Mục tiêu của chia task không phải tạo nhiều task nhất có thể.

Mục tiêu là tạo ranh giới vừa đủ để agent ít đoán hơn và mình review dễ hơn.

Nếu việc giải thích dependency còn dài hơn chính thay đổi, có thể task đó chưa cần tách thêm.

Kết

Ở bài trước, mình nói đừng giao feature lớn cho agent trong một câu rồi mong nó tự làm đúng mọi thứ.

Bước tiếp theo không phải viết prompt dài hơn. Mà là viết một spec đủ rõ, review nó, rồi biến spec thành một chuỗi kết quả nhỏ có phạm vi, thứ tự phụ thuộc và cách kiểm tra.

Mình thường đi theo thứ tự:

  1. Viết điều người dùng quan sát được
  2. Tìm hiểu codebase và cách làm hiện có
  3. Kéo vùng mơ hồ ra và khoá quyết định
  4. Viết, review và approve spec
  5. Chia task thực thi theo lát cắt dọc
  6. Viết phần phụ thuộc và tiêu chí hoàn thành cho từng task
  7. Làm rồi kiểm tra lại theo đặc tả

Task nhỏ không làm feature nhỏ đi.

Nó chỉ làm cho mỗi bước đủ rõ để cả mình và agent biết đang thay đổi điều gì.

Và khi từng bước đều có thể kiểm tra ngược về spec, feature lớn bớt giống một canh bạc hơn rất nhiều.

See yah.