Lỗi

Agent báo «xong», nhưng tính năng không chạy: vì sao xảy ra và cách kiểm chứng công việc của AI agent

Cập nhật: · Skillaria.Top

Agent báo «xong» vì nó tự đánh giá công việc và chỉ thấy những gì nó đã kiểm tra: thường là unit test, vốn không nhìn thấy các điểm nối giữa module, migration, cấu hình và kịch bản đầu cuối. Cách khắc phục nằm ở cấu trúc chứ không phải prompt: Definition of Done trong AGENTS.md, bắt buộc chạy E2E, script kiểm chứng là nơi duy nhất được đặt trạng thái passing, và người đánh giá độc lập với ngữ cảnh sạch.

Vì sao điều này xảy ra

Tình huống này quen thuộc với bất kỳ ai làm việc với agent: agent báo «xong», nhưng tính năng không chạy. Nó không cố tình nói dối. Nó báo cáo những gì nó đã kịp kiểm tra và tin chắc rằng như vậy là đủ. Khoảng cách giữa lời khai và thực tế là đặc tính của chính sơ đồ «người thực thi tự đánh giá công việc của mình», chứ không phải lỗi của một mô hình cụ thể.

Có ba nguyên nhân. Thứ nhất, mô hình có xu hướng tự tin quá mức: mức độ chắc chắn của câu trả lời cao hơn độ chính xác của nó. Thứ hai, agent đánh giá công việc trong cùng ngữ cảnh mà nó đã làm ra: nó đã tự thuyết phục rằng lời giải là đúng, và yêu cầu «hãy khách quan hơn» không thay đổi được ngữ cảnh đó. Thứ ba, và thực tế nhất, agent chỉ có quyền truy cập một phần kiểm chứng: unit test xanh rất nhanh, còn kịch bản đầu cuối thì không ai chạy.

Biên niên sử một lần «xong» giả

Agent được giao việc đặt lại mật khẩu. Nó thay đổi schema cơ sở dữ liệu, thêm endpoint, gắn mẫu email, chạy unit test — tất cả đều xanh. Trong khung chat xuất hiện câu «Xong» đầy tự tin.

Thực tế đã xảy ra như sau:

  • migration lỗi giữa chừng và để schema ở trạng thái không nhất quán;
  • mẫu email có, nhưng dịch vụ thư (SMTP) chưa được cấu hình trong môi trường — email không được gửi đi;
  • kịch bản «người dùng bấm liên kết → nhập mật khẩu mới → đăng nhập» chưa từng được chạy.

Không unit test nào có thể bắt được những vấn đề này: cơ sở dữ liệu đã được mock, việc gửi email đã được mock, và cũng không hề có lần chạy đầu cuối nào.

Vì sao test xanh không chứng minh tính năng hoạt động

Unit test kiểm tra một module trong sự cô lập: các phụ thuộc được thay bằng mock, thế giới bên ngoài bị ngắt. Điều này cho tốc độ và độ chính xác, nhưng cũng tạo ra một điểm mù có tính hệ thống. Điểm mù này không phải là lỗi của test, mà là thiết kế của chúng. Unit test xanh chỉ chứng minh rằng từng chi tiết hoạt động riêng lẻ, chứ không chứng minh rằng các chi tiết hoạt động cùng nhau.

Có bốn loại lỗi mà unit test về nguyên tắc không thấy được:

Loại lỗiVí dụVì sao unit test không bắt được
Interface không khớpMột thành phần trả về đường dẫn tương đối, thành phần khác lại cần đường dẫn tuyệt đốiMỗi thành phần được test với mock riêng, và mock đó «đồng ý» với mọi định dạng
Lan truyền trạng tháiSchema DB đã đổi, nhưng cache của ORM vẫn giữ bản cũTrong unit test không có cơ sở dữ liệu thật cũng không có cache
Vòng đời tài nguyênBộ mô tả tệp và kết nối không được giải phóng, bộ nhớ rò rỉ với tệp lớnTest chạy trên dữ liệu nhỏ và kết thúc trước khi tài nguyên cạn kiệt
Môi trường và cấu hìnhThiếu biến môi trường, SMTP chưa được cấu hình, migration chưa được áp dụngTest không khởi động ứng dụng với cấu hình thực tế

Các cấp độ kiểm chứng lồng nhau: mọi thứ unit test bắt được thì test tích hợp cũng bắt được, và mọi thứ test tích hợp bắt được thì lần chạy đầu cuối (E2E) cũng bắt được. Mỗi cấp tiếp theo nhìn thấy nhiều hơn một cách nghiêm ngặt. Vì vậy quyền tuyên bố «xong» chỉ nên thuộc về cấp cao nhất.

Đo lường vấn đề: verification gap

Trước khi sửa, hãy đếm. Lấy 10 tác vụ gần nhất do agent hoàn thành. Với mỗi tác vụ, trả lời hai câu hỏi: agent có báo «xong» không, và kết quả đó có được kiểm chứng độc lập xác nhận không (bạn tự chạy kịch bản, hoặc kiểm thử E2E xanh). Tỷ lệ các trường hợp không khớp chính là verification gap của bạn.

Con số này hữu ích vì hai lý do. Nó cho thấy quy mô vấn đề mà không cần tranh luận kiểu «hình như vẫn chạy ổn». Và nó là đường cơ sở: sau mỗi thay đổi trong quy trình, bạn tính lại trên một bộ tác vụ tương tự và xem có cải thiện không.

Kiểm chứng thực sự: ba lớp xác thực

Một phép so sánh dễ hiểu là xưởng bảo dưỡng ô tô. Xe phải đi qua ba trạm, và không được bỏ qua trạm nào.

  1. Chẩn đoán (vài giây): cú pháp, kiểu, linter. Bắt lỗi chính tả và lỗi thô.
  2. Bệ thử (vài phút): unit test và test tích hợp, ứng dụng khởi động được. Động cơ đã chạy, nhưng xe vẫn đứng yên.
  3. Lái thử: kịch bản đầu cuối đúng như người dùng thực hiện — qua API, trình duyệt hoặc CLI, với cơ sở dữ liệu và cấu hình thật.

Nguyên tắc tiến trình: không được lái thử khi chẩn đoán còn lỗi, và không được xuất xưởng khi chưa qua lái thử. Unit test xanh là trạm thứ hai đã vượt qua, chứ không phải trạm thứ ba.

Có thể gom tất cả vào một điểm vào duy nhất:

# Makefile
check:
	ruff check .
	mypy app
	pytest tests/unit tests/integration

e2e:
	docker compose up -d db mailhog
	alembic upgrade head
	pytest tests/e2e

verify: check e2e

Ở đây E2E khởi động cơ sở dữ liệu thật và máy chủ thư thử nghiệm, áp dụng migration và chạy trọn vẹn kịch bản. Migration lỗi hoặc thư chưa được cấu hình sẽ lộ ra ngay, chứ không phải ở phía khách hàng.

Definition of Done trong AGENTS.md

Agent không nên tự quyết định «xong» nghĩa là gì. Tiêu chí được ghi vào tệp hướng dẫn và phát biểu một cách kiểm chứng được:

## Definition of Done
Tính năng chỉ được coi là xong khi thỏa MÃN TẤT CẢ các mục:
1. make check kết thúc với mã 0
2. make e2e kết thúc với mã 0, kịch bản của tính năng có trong tests/e2e
3. Migration áp dụng được trên cơ sở dữ liệu sạch và có thể hoàn tác
4. Biến môi trường mới được mô tả trong .env.example
5. PROGRESS.md ghi lại đã làm gì và kiểm chứng như thế nào

KHÔNG phải là dấu hiệu xong:
- «code đã viết và trông có vẻ đúng»
- «unit test đều xanh»
- «trên máy tôi chạy được»

Một danh sách rõ ràng về những gì không được coi là hoàn thành hiệu quả hơn bạn tưởng: agent thôi dừng lại ở lần chạy xanh đầu tiên.

Ai được quyền đặt trạng thái «passing»

Bước tiếp theo là lấy trạng thái hoàn thành hoàn toàn ra khỏi tay agent. Các tính năng được lưu trong một danh sách (ví dụ feature_list.json) với các trường «hành vi», «lệnh kiểm chứng», «trạng thái». Chỉ có script mới được chuyển tính năng sang passing: nó chạy make check chung, sau đó lệnh kiểm chứng của riêng tính năng, và khi thành công thì ghi trạng thái kèm mã băm của commit làm bằng chứng. Không đạt thì tính năng vẫn ở trạng thái đang hoạt động, không có khái niệm «gần được rồi».

{
  "id": "F04",
  "behavior": "Người dùng nhận email có liên kết và đổi mật khẩu",
  "verification": "pytest tests/e2e/test_password_reset.py",
  "state": "active"
}

Có một cái bẫy ở đây: script chỉ kiểm tra đúng những gì được ghi trong trường verification. Nếu một kiểm tra kiến trúc thuộc Definition of Done nhưng không nằm trong lệnh của tính năng, nó sẽ là một lỗ hổng vô hình. Vì vậy make check chung phải bao gồm mọi kiểm tra bắt buộc, chứ không chỉ các test.

Người đánh giá độc lập

Lời tự khai của người thực thi không bao giờ là nguồn sự thật độc lập. Giải pháp mang tính cấu trúc là tách vai trò:

  • người lập kế hoạch chia yêu cầu thành các tính năng có tiêu chí chấp nhận và không viết mã;
  • người sinh mã triển khai một tính năng và chạy test, nhưng không tự quyết định là đã xong;
  • người đánh giá đưa ra kết luận theo danh sách tiêu chí.

Người đánh giá không được thấy lập luận của người sinh mã, nếu không nó sẽ thừa hưởng sự tự tin của người đó. Nó chỉ nhận yêu cầu, diff và kết quả chạy. Trong Claude Code, điều này dễ thực hiện bằng subagent: nó có ngữ cảnh riêng sạch sẽ. Còn tốt hơn nữa nếu người đánh giá là một mô hình khác, vì nó có những điểm mù khác. Đánh giá kiểu «trông có vẻ ổn» bị cấm: mỗi kết luận phải dẫn chiếu đến tệp, dòng hoặc log cụ thể.

Thông báo lỗi mà agent có thể tự sửa

Kiểm chứng chỉ có ích khi đầu ra của nó dẫn đến việc sửa lỗi. «Test failed» hay một AssertionError trơ trọi không nói lên điều gì với agent. Một thông báo tốt cho biết điều gì bị hỏng, vì sao quy tắc này tồn tại và cách sửa:

ERROR arch-check: app/services/refund.py import app/db/session.py
WHY: các service không truy cập cơ sở dữ liệu trực tiếp, chỉ qua repository
FIX: dùng RefundRepository từ app/repositories/refund.py

Định dạng như vậy biến kiểm chứng từ một lệnh cấm thành một gợi ý: agent tự sửa vi phạm mà không cần bạn can thiệp.

Kiểm tra thủ công: vẫn cần ở đâu

Tự động hoá không loại bỏ con người, nhưng thu hẹp vai trò của họ. Kiểm tra thủ công cần thiết ở những nơi khó chính thức hoá tiêu chí: giao diện trông như thế nào, văn bản, sự thuận tiện của kịch bản. Hãy làm theo danh sách tiêu chí chấp nhận, đừng chỉ «bấm thử». Nếu bạn viết cùng một nhận xét cho agent lần thứ hai, đó là tín hiệu để biến nó thành kiểm chứng tự động: một test, một quy tắc linter hoặc một guard kiểu grep trong make check.

E2E thay đổi hành vi của agent ngay cả trước khi chạy

Giá trị của kiểm thử đầu cuối không chỉ nằm ở những lỗi bắt được. Nếu agent biết trước rằng công việc của nó sẽ được kiểm tra bằng kịch bản E2E, nó sẽ viết mã khác đi: nghĩ đến hợp đồng với thành phần lân cận chứ không chỉ hàm của mình, xử lý các nhánh lỗi, và không vi phạm ranh giới kiến trúc vì vi phạm đó sẽ lộ ra khi chạy. Kiểm chứng hoạt động như một ràng buộc ở đầu vào, chứ không chỉ là bộ lọc ở đầu ra.

Có một điều kiện: E2E chỉ có ý nghĩa khi hệ thống có ranh giới rõ ràng. Trên một kiến trúc «mì spaghetti», kiểm thử đầu cuối chỉ chứng minh rằng mì spaghetti vẫn chạy, và không cho thấy ý đồ thiết kế đã bị vi phạm ở đâu. Vì vậy trước tiên cần cố định các lớp và quy tắc phụ thuộc, rồi mới xây kịch bản đầu cuối. Agent rất thích sao chép các mẫu có sẵn trong repository, kể cả những mẫu xấu, nên tốt nhất là đưa các ràng buộc vào ngay từ ngày đầu.

Những lỗi thường gặp

  • Yêu cầu agent «kiểm tra kỹ hơn». Ngữ cảnh vẫn vậy, đánh giá sẽ vẫn bị thổi phồng.
  • Coi độ phủ 100% là bảo đảm. Độ phủ unit test không bao phủ các điểm nối.
  • Tái cấu trúc trước khi tính năng được xác nhận. Thứ tự rất nghiêm ngặt: đúng đắn → hiệu năng → phong cách.
  • Mock trong E2E đúng những thứ hay hỏng nhất. Cơ sở dữ liệu, migration và cấu hình trong kiểm thử đầu cuối phải chạy thật; các API thanh toán trả phí bên ngoài thì có thể thay bằng máy chủ thử nghiệm.

Tóm lại

Khi agent báo «xong» mà tính năng không chạy, vấn đề gần như luôn nằm ở quy trình kiểm chứng chứ không phải ở mô hình. Hãy đo verification gap, ghi Definition of Done vào AGENTS.md, bắt buộc E2E, giao trạng thái hoàn thành cho script và thêm một người đánh giá độc lập. Cách làm này được trình bày chi tiết qua thực hành trong khoá học harness engineering.

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

Có thể yêu cầu agent tự chạy kiểm thử E2E không?

Có, nếu lệnh được ghi trong AGENTS.md như một phần bắt buộc của Definition of Done và agent có các công cụ cần thiết. Đáng tin cậy hơn là chạy nó thêm bằng hook hoặc trong CI, để không phụ thuộc vào trí nhớ của agent.

Kiểm thử E2E chậm, có cần chạy sau mỗi lần sửa không?

Không. Lint, kiểu và unit test luôn chạy; E2E chạy khi thay đổi ảnh hưởng nhiều hơn một lớp, và bắt buộc trước khi tuyên bố tính năng đã xong.

Làm sao kiểm tra giao diện frontend mà agent đã làm?

Bằng kiểm thử đầu cuối qua trình duyệt (ví dụ Playwright) với các thao tác nhấp chuột trên giao diện thật, còn giao diện trông thế nào thì kiểm tra thủ công theo danh sách hoặc qua ảnh chụp màn hình để người đánh giá xem xét.

Review mã của con người có thể thay cho người đánh giá không?

Review của con người rất hữu ích, nhưng tốn kém và không mở rộng được cho hàng giờ làm việc tự động. Người đánh giá có rubric sẽ loại bỏ các vấn đề rõ ràng trước khi đến tay con người, và con người quyết định các câu hỏi gây tranh cãi.

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