Hướng dẫn

Cách giao việc cho AI agent: một tính năng mỗi lần, danh sách tính năng và tiêu chí hoàn thành

Cập nhật: · Skillaria.Top

Giao việc cho AI agent theo từng việc một: tại mỗi thời điểm chỉ có đúng một tính năng đang làm, nó có hành vi quan sát được và lệnh kiểm tra chạy được, còn mọi thứ phát hiện trên đường đi agent ghi vào backlog chứ không sửa ngay. Trạng thái của tất cả tính năng được lưu trong danh sách máy đọc được (ví dụ feature_list.json), và chỉ script kiểm tra mới được chuyển tính năng sang trạng thái «xong», không phải chính agent.

Vì sao agent ôm mọi việc cùng lúc

Bạn yêu cầu agent làm chức năng đăng ký. Vài giờ sau, diff có mười hai tệp bị sửa và tám trăm dòng mới: mô hình người dùng, xác minh email, kết nối dịch vụ gửi thư, băm mật khẩu, middleware đã được tái cấu trúc và các test đã được chuyển chỗ. Mọi thứ liên kết chặt với nhau, và không kịch bản nào chạy trọn vẹn. Phiên làm việc tiếp theo mở đoạn code này và không biết bắt đầu từ đâu.

Nguyên nhân không phải «mô hình kém». Con người có cảm giác «hôm nay thế là đủ» và sự mệt mỏi thông thường — chính những thứ đó giới hạn phạm vi công việc. Agent thì không có cả hai. Thêm câu «tiện thể sửa luôn phần xử lý lỗi» chỉ tốn vài chục token của nó, nhưng cái giá với dự án là thật: sự chú ý bị phân tán, độ liên kết tăng lên, còn việc thì không xong.

Có một phép tính đơn giản. Gọi C là dung lượng ngữ cảnh hữu ích, k là số tác vụ agent giữ cùng lúc. Mỗi tác vụ nhận khoảng C/k tài nguyên suy luận. Khi giá trị này xuống dưới mức cần để hoàn thành một tác vụ, không tác vụ nào được hoàn thành. Vì thế câu trả lời đúng cho chuyện giao việc cho AI agent không bắt đầu từ cách viết prompt, mà từ một giới hạn: một tác vụ đang làm tại một thời điểm.

Quy tắc một tính năng (WIP=1)

WIP — work in progress, số tác vụ đang làm dở. Với agent, con số này được đặt bằng một: tại mỗi thời điểm chỉ có đúng một tính năng đang hoạt động, tính năng tiếp theo chỉ bắt đầu sau khi tính năng hiện tại vượt qua kiểm tra. Quy tắc được ghi vào tệp hướng dẫn của dự án (AGENTS.md hoặc CLAUDE.md) để agent thấy nó trong mọi phiên:

## Quy tắc làm việc
- Tại mỗi thời điểm, ở trạng thái `active` có ĐÚNG MỘT tính năng.
- Chỉ bắt đầu tính năng tiếp theo sau khi tính năng hiện tại đã chuyển sang `passing`.
- Không được «tiện thể» refactor, đổi tên, tổ chức lại thư mục
  hoặc sửa các lỗi không liên quan. Phát hiện vấn đề — ghi vào `BACKLOG.md` và làm tiếp.
- Thay đổi ngoài phạm vi tính năng đang làm được coi là vi phạm, kể cả khi chúng hữu ích.

Mục cuối quan trọng hơn vẻ ngoài của nó. Agent thật lòng cho rằng những việc «tiện thể» của mình là hữu ích, và thường đúng là vậy. Nhưng một chỉnh sửa hữu ích ngoài phạm vi vẫn trộn hai việc vào cùng một diff, và từ đó không thể hoàn tác hay kiểm tra riêng từng việc được nữa.

BACKLOG.md: đưa việc «tiện thể» đi đâu

Thôi thúc «tiện thể sửa luôn» không thể chỉ đơn giản bị cấm — nó cần một lối thoát hợp pháp. Lối thoát đó là tệp BACKLOG.md: agent ghi vấn đề phát hiện được trong một đến hai dòng và quay lại tính năng hiện tại. Thông tin không bị mất, còn phạm vi không phình ra. Nếu không có backlog, quy tắc một tính năng sớm muộn cũng bị agent lách, vì với nó, việc «lặng lẽ để lại lỗi» trông còn tệ hơn là vi phạm quy tắc.

# BACKLOG.md
- [ ] middleware/errors.py: định dạng lỗi khác nhau ở /auth và /orders
- [ ] tests/: fixture người dùng bị lặp lại trong ba tệp
- [ ] có cần xác minh e-mail khi đăng ký không? — hỏi lại

Tiêu chí hoàn thành: «xong» phải là một lệnh

Nửa còn lại của vấn đề là agent tự quyết định khi nào tác vụ đã xong. «Code trông ổn», «hàm có tồn tại», «không có lỗi biên dịch» — tất cả đều là cảm nhận chứ không phải sự thật. Tiêu chí hoàn thành của một tác vụ với agent phải là một lệnh chạy được, cho kết quả rõ ràng: đạt hoặc không đạt.

F01: đăng ký người dùng
verification: pytest tests/e2e/test_auth.py::test_register_flow
state: active

Sự khác biệt giữa tiêu chí yếu và tiêu chí chặt thể hiện rõ qua ví dụ:

Tiêu chí yếuTiêu chí chặt
Code biên dịch đượcpytest tests/e2e/test_cart.py -q chạy đạt
Không có lỗi cú phápcurl tới /cart trả về mã 200
Hàm addToCart tồn tạiSau khi thêm sản phẩm, GET /cart hiển thị nó với đúng tổng tiền
Trông có vẻ đúngnpx playwright test cart.spec.ts chạy đạt

Lệnh kiểm tra tốt là lệnh có tính xác định (cùng code thì cùng kết quả), chạy được mà không cần người tham gia, và kiểm tra hành vi từ bên ngoài — đúng như người dùng hoặc client API sẽ thấy. Nếu không viết được lệnh như vậy, đó là tín hiệu rằng tác vụ được mô tả chưa rõ.

Chia nhỏ: một tính năng nên có kích thước nào

Câu hỏi hay gặp nhất sau «một tính năng mỗi lần» là tính năng có kích thước thế nào. Tác vụ quá rộng sẽ không xong trong một phiên, còn tác vụ quá nhỏ bị chìm trong chi phí khởi động, ngữ cảnh và kiểm tra.

  • Quá rộng: «làm giỏ hàng». Đây là một tuần công việc và hàng chục kịch bản.
  • Vừa phải: «người dùng thêm sản phẩm vào giỏ và thấy nó trong giỏ». Một hành vi quan sát được, một kịch bản kiểm tra.
  • Quá nhỏ: «tạo trường quantity trong model CartItem». Đây là một bước triển khai, không phải hành vi; kiểm tra riêng nó không có ý nghĩa.

Cách kiểm tra kích thước tiện dụng: một tính năng = một phiên agent = một commit có ý nghĩa = một kịch bản end-to-end (E2E). Nếu tính năng không nằm gọn trong một phiên, hãy chia tiếp. Nếu không viết được kịch bản riêng cho nó, nó quá nhỏ — hãy gộp với tính năng bên cạnh.

Ví dụ chia nhỏ tác vụ «giỏ hàng»

Yêu cầu rộng được tách thành các đơn vị nguyên tử, mỗi đơn vị có một hành vi và lệnh kiểm tra riêng:

  1. Thêm sản phẩm vào giỏ và thấy nó trong danh sách.
  2. Đổi số lượng sản phẩm — tổng tiền được tính lại.
  3. Xóa sản phẩm khỏi giỏ.
  4. Giỏ hàng của khách được giữ lại sau khi đăng nhập vào tài khoản.
  5. Không thể thêm sản phẩm đã hết hàng — hiển thị lỗi dễ hiểu.

Kiểm tra cuối cùng cho việc chia nhỏ: không đơn vị nào đòi hỏi phải «tiện tay» sửa đơn vị khác. Nếu mục 4 cần viết lại mục 1, thì thứ tự hoặc ranh giới đã được chọn chưa hợp lý.

Danh sách tính năng: feature_list.json

Quy tắc một tính năng giữ trật tự bên trong một phiên. Nhưng ai nhớ giữa các phiên điều gì đã xong? Ghi chú trong chat kiểu «auth và danh mục đã xong, giỏ hàng gần xong nhưng còn lỗi» không giúp gì cho phiên mới: «gần» nghĩa là gì, những test nào đã đạt, cái gì chặn thanh toán — đều không rõ. Mỗi phiên mới giống như một tài xế mới lái cùng một chiếc xe: nếu ghi chú chỉ nằm trong đầu người cũ, người mới không biết gì cả.

Vì vậy trạng thái được đưa ra ngoài, vào một tệp máy đọc được. Trong khóa học, tệp này là .agent/feature_list.json:

[
  {
    "id": "F01",
    "behavior": "Người dùng đăng ký bằng e-mail và mật khẩu rồi đăng nhập luôn",
    "verification": "pytest tests/e2e/test_auth.py::test_register_flow",
    "state": "passing",
    "evidence": "a3f9c12: 1 passed in 2.31s"
  },
  {
    "id": "F02",
    "behavior": "Người dùng thêm sản phẩm vào giỏ và thấy nó trong danh sách",
    "verification": "pytest tests/e2e/test_cart.py::test_add_item -q",
    "state": "active"
  },
  {
    "id": "F03",
    "behavior": "Thanh toán đơn hàng bằng thẻ",
    "verification": "pytest tests/e2e/test_payment.py -q",
    "state": "blocked",
    "blocker": "chưa có khóa thử nghiệm của cổng thanh toán"
  }
]

Mỗi mục có ba trường bắt buộc: mô tả hành vi (quan sát được từ bên ngoài), lệnh kiểm tra (chạy được, không cần người) và trạng thái. Trường thứ tư không bắt buộc là evidence: hash commit và đầu ra của kiểm tra, là bằng chứng rằng việc chuyển sang passing thực sự đã xảy ra.

Vì sao dùng JSON chứ không dùng danh sách Markdown? Không chỉ agent đọc tệp này, mà cả script cũng đọc: bộ lập lịch chọn tính năng tiếp theo đang ở not_started, bộ kiểm chứng lấy lệnh kiểm tra, báo cáo phiên được tạo tự động, còn phân bố trạng thái cho ra số liệu về sức khỏe dự án. Danh sách tính năng là xương sống của harness, không phải một ghi nhớ.

Bốn trạng thái và các chuyển đổi được phép

Chuyển đổiKhi nào
not_started → activeTính năng được đưa vào làm; không có tính năng nào khác đang active
active → passingScript kiểm tra kết thúc với mã 0
active → blockedCó vật cản bên ngoài; lý do được ghi vào blocker
blocked → activeVật cản đã được gỡ bỏ

Không có chuyển đổi nào khác. Cụ thể là không có đường đi tới passing mà bỏ qua kiểm tra, và không có trạng thái «gần xong».

Cổng passing: agent có thể yêu cầu, nhưng không tự tuyên bố

Quy tắc trung tâm của toàn bộ sơ đồ: agent không tự sửa trường state bằng tay. Nó có thể chạy kiểm tra, nhưng không thể tự tuyên bố kết quả — việc đó do script làm. Khác biệt giữa «agent đã báo cáo» và «script đã xác nhận» là khác biệt giữa lời nói và sự thật. Quy tắc cũng được ghi vào tệp hướng dẫn:

## Quy tắc danh sách tính năng
- Tệp: `.agent/feature_list.json`. Đây là nguồn sự thật duy nhất về phạm vi.
- Chỉ có đúng một tính năng ở trạng thái `active`.
- Bạn KHÔNG sửa trường `state` bằng tay. Nó do `scripts/verify.sh` thay đổi.
- Để chuyển tính năng sang `passing`, chạy `./scripts/verify.sh F02` và đạt mã 0.
- Nếu tính năng bị chặn — chuyển sang `blocked` và bắt buộc ghi `blocker`.
- Tính năng mới chỉ được thêm vào `not_started` và chỉ kèm lệnh kiểm chứng.

Chính script kiểm tra ở hai mức: trước hết là lần chạy chung của dự án (tính năng không được làm hỏng phần còn lại), sau đó là lệnh kiểm tra của riêng tính năng. Thành công cục bộ mà có hồi quy toàn cục thì không được coi là thành công.

#!/usr/bin/env bash
# Thành phần duy nhất có quyền đặt passing.
set -uo pipefail
FID="$1"
FILE=".agent/feature_list.json"

CMD=$(jq -r --arg id "$FID" '.[] | select(.id==$id) | .verification' "$FILE")
if [ -z "$CMD" ] || [ "$CMD" = "null" ]; then
  echo "FAIL: không có lệnh kiểm chứng cho $FID"; exit 2
fi

echo "== global check =="
make check || { echo "FAIL: kiểm tra chung đỏ, $FID không thể là passing"; exit 1; }

echo "== feature check: $CMD =="
if OUT=$(eval "$CMD" 2>&1); then
  SHA=$(git rev-parse --short HEAD)
  LAST=$(printf '%s\n' "$OUT" | tail -n 1)
  TMP=$(mktemp)
  jq --arg id "$FID" --arg ev "$SHA: $LAST" \
    'map(if .id==$id then .state="passing" | .evidence=$ev else . end)' \
    "$FILE" > "$TMP" && mv "$TMP" "$FILE"
  echo "PASS: $FID"
else
  printf '%s\n' "$OUT"
  echo "FAIL: $FID"; exit 1
fi

Quy tắc «đừng sửa state» viết bằng chữ vẫn chỉ là một lời đề nghị. Để nó trở thành ràng buộc, có thể khóa việc ghi vào tệp bằng cơ chế: một kiểm tra trong CI đối chiếu evidence với lịch sử commit, hoặc giới hạn quyền của agent trong công cụ bạn dùng. Nguyên tắc vẫn như mọi chỗ khác trong harness: quy tắc quan trọng được củng cố bằng cơ chế.

Đo lường agent có hoàn thành việc đến cùng không

Một chỉ số hấp dẫn là khối lượng code trong một phiên. Trên thực tế nó cho kết quả ngược lại: diff lớn thường có nghĩa là agent đã tản mát sang nhiều hướng. Hãy đếm các tính năng ở trạng thái passing, không đếm số dòng.

Một chỉ số tiện dụng trong khóa học là VCR (Verified Completion Rate): số tác vụ đã được kiểm chứng chia cho số tác vụ đã kích hoạt. Quy tắc đơn giản: chừng nào VCR còn nhỏ hơn một, không được kích hoạt tác vụ mới. Đây là áp lực ngược mà harness tác động lên agent, và cũng là cách biến phần việc dang dở thành một con số nhìn thấy được, thay vì một cảm giác.

Lỗi thường gặp khi giao việc cho agent

  • Tác vụ không có lệnh kiểm tra. «Làm chức năng đăng ký» mà không có kịch bản — agent tự quyết định chỗ dừng, và quyết định sai.
  • Nhiều tính năng trong một yêu cầu. «Làm giỏ hàng và thanh toán» là hai tác vụ đang active, và không cái nào được làm đến cùng.
  • Trạng thái chỉ nằm trong chat. Tiến độ ghi trong hội thoại sẽ biến mất khi phiên kết thúc. Nó phải nằm trong tệp.
  • Tự báo cáo thay cho kiểm tra. «Agent nói các test đã đạt» không giống với mã trả về của script.
  • Không có backlog. Cấm «tiện thể» mà không có chỗ ghi lại những gì đã phát hiện thì agent sẽ lách.

Khi một tính năng mỗi lần đã chưa đủ

Sơ đồ «một tính năng — một phiên — một kiểm tra» chạy tốt với một agent trong một vòng lặp. Khi số tác vụ tăng lên và xuất hiện các nhánh độc lập, sơ đồ được mở rộng thành hệ thống nhiều vòng: một nút lập kế hoạch riêng, các nút triển khai, một nút kiểm tra và các cạnh nối giữa chúng — song song, có điều kiện, và quay lại triển khai nếu kiểm tra không đạt. Nhưng ngay cả ở đó, mỗi nút triển khai vẫn làm việc theo cùng quy tắc: một tác vụ, một tiêu chí hoàn thành chạy được, trạng thái nằm ngoài đầu agent. Đồ thị không hủy bỏ kỷ luật một tính năng mà được xây dựng trên nền tảng đó.

Một cảnh báo quan trọng từ cùng nguồn: vòng lặp tối ưu trung thực theo chỉ số mà nó được cho. Nếu tiêu chí hoàn thành được chọn chưa tốt — ví dụ «ticket đã đóng» thay vì «vấn đề đã được giải quyết» — agent sẽ học cách đáp ứng tiêu chí chứ không phải giải quyết tác vụ. Vì thế, việc diễn đạt hành vi và lệnh kiểm tra đáng được dành thời gian nhiều hơn người ta tưởng.

Tóm lại

Ngắn gọn, cách giao việc cho AI agent là: từng việc một, với hành vi quan sát được và lệnh kiểm tra chạy được; mọi thứ phát hiện trên đường đi được ghi vào backlog; và danh sách tính năng nằm trong tệp, nơi chuyển sang «xong» do script thực hiện chứ không phải agent. Hãm phanh cho agent là việc của harness — bản thân nó sẽ không tự bật. Quy tắc WIP=1, feature_list.json và verify.sh được giải thích và thực hành trong khóa học harness engineering trên một dự án xuyên suốt.

Câu hỏi thường gặp

Có thể giao cho agent nhiều tác vụ cùng lúc nếu chúng nhỏ không?

Có thể gom chúng vào danh sách tính năng, nhưng tại một thời điểm chỉ được có một tác vụ đang làm. Agent chỉ nhận tác vụ tiếp theo sau khi tác vụ hiện tại vượt qua kiểm tra, nhờ vậy mỗi việc đều được làm đến cùng.

Nếu không thể viết kiểm tra tự động cho tác vụ thì sao?

Thường điều đó có nghĩa là tác vụ được viết dưới dạng hành động chứ không phải hành vi. Hãy diễn đạt lại qua những gì người dùng hoặc client API sẽ thấy; với phần giao diện, các test e2e trên trình duyệt là lựa chọn phù hợp.

Ai viết feature_list.json — con người hay agent?

Danh sách ban đầu nên được soạn cùng nhau: agent đề xuất cách chia nhỏ, con người chỉnh sửa. Sau đó agent có thể thêm tính năng mới, nhưng chỉ ở trạng thái not_started và kèm lệnh kiểm tra.

feature_list.json khác gì các task trong công cụ quản lý công việc?

Công cụ quản lý công việc dành cho con người, còn danh sách tính năng dành cho máy đọc: mỗi mục có lệnh chạy được và trạng thái nghiêm ngặt do script thay đổi. Hai thứ có thể đồng bộ với nhau, nhưng agent cần tệp trong repository.

Quy tắc một tính năng có làm chậm công việc không?

Phiên làm việc ngắn hơn, nhưng tỷ lệ tác vụ được làm đến cùng tăng lên. Thời gian trước đây dùng để gỡ đoạn code dở dang lẫn của ba tính năng được tiết kiệm.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Harness Engineering: инженерия надёжности AI-агентов”.

Đến khóa học