# Test xanh hết mà agent vẫn toang lúc chạy thật: con bug nằm ở chỗ mình kiểm thử
Mình từng có một niềm tin rất ngây thơ: hễ cái gì có test xanh là cái đó an toàn.
Niềm tin đó sống rất dai, cho tới khi mình giao một con agent tự động cho người khác dùng — và nó sập ngay trong ngày đầu tiên. Không phải sập theo kiểu báo lỗi đỏ, mà sập theo kiểu tệ hơn nhiều: nó chạy trơn tru, trả kết quả, và kết quả sai. Trong khi ở máy mình, cái bộ test của mình vẫn xanh lè.
Đó là lúc mình hiểu ra một chuyện không mấy dễ chịu: mình không hề test con agent. Mình test cái phần dễ test quanh nó, rồi tự lừa mình rằng thế là đủ.
Con agent không sai. Cái sai là mình test nó như một cái function
Khi mình viết một hàm bình thường, mình có một hợp đồng rõ ràng: đưa vào cái này, trả ra cái kia, sai thì ném lỗi. Test kiểu gì cũng được — so sánh bằng, so sánh giá trị, assert thẳng.
Rồi mình mang đúng cái tư duy đó áp lên một con agent. Và tự nhiên mọi thứ lệch:
- ▹Cùng một đầu vào, chạy ba lần ra ba kết quả khác nhau. Không lần nào "sai" hẳn, nhưng không lần nào giống nhau.
- ▹Một hôm nó lọc quá tay, bỏ mất thứ đáng giữ. Hôm sau nó lọc quá lỏng, tha cả rác vào.
- ▹Cái assert mình viết để bắt "phải có chữ X" thì pass — nhưng câu văn quanh chữ X nghe như máy đọc danh sách, không phải như người.
Vấn đề gốc nằm ở bản chất: agent không phải một hàm xác định, nó là một hàm xác suất. Đầu ra của nó là một phân bố, không phải một giá trị. Và bạn không thể kiểm tra một phân bố bằng cách chạy nó đúng một lần trong môi trường sạch do chính bạn dựng.
Cái bẫy tinh vi nhất ở đây là: test của bạn xanh, không phải vì agent tốt, mà vì bạn đã vô tình chọn đúng những đầu vào mà nó xử lý ngon. Bạn test cái mình biết, agent chết ở cái mình không biết.
Chuyện "chạy ở nhà thấy ngon, tới chỗ khách thì toang"
Có một bài mình đọc dạo này, tiêu đề đại ý: "Chúc mừng, agent của bạn hoàn hảo. Tiếc là doanh nghiệp chạy bằng Slack."
Câu đó đau nhưng đúng. Rất nhiều agent được test trong một môi trường lý tưởng — dữ liệu đẹp, luồng sạch, không ai làm gì bất ngờ. Còn lúc nó ra đời thật, nó phải sống chung với một mớ hỗn độn: file gửi kiểu nửa vời, người ta gõ tắt, người ta đổi quy trình giữa chừng, và cái công cụ then chốt thì nằm trong một cái chat group chứ không nằm trong API nào cả.
Ba cái bẫy mình từng dính, chắc anh em cũng dính:
Một, "máy mình chạy được" không phải là một tiêu chuẩn. Môi trường của bạn là môi trường đã được thuần hoá — bởi chính bạn, vô thức, qua hàng chục lần chạy thử.
Hai, cái nền bên dưới vẫn đang thay đổi trong khi bạn ngủ. Model ngầm nâng cấp. Nguồn dữ liệu đổi cách trình bày. Prompt của bạn không đổi một chữ, nhưng hành vi thì đổi. Test hôm qua xanh không bảo chứng gì cho ngày mai.
Ba, thứ khách quan tâm không phải agent thông minh cỡ nào, mà là nó có làm họ mất mặt không.* Mà "mất mặt" là một thứ rất khó đưa vào unit test.
Ba lớp kiểm chứng cho một thứ vốn có xác suất
Mình bắt đầu nghĩ khác. Nếu đầu ra là xác suất, thì mình không chứng minh nó đúng — mình giới hạn thiệt hại và đo được độ lệch. Cụ thể là ba lớp chồng lên nhau.
Lớp 1 — Phần nào chắc được thì cứng hoá, và đừng để model dính vào.
Phần lớn các pipeline mình làm đều có một xương sống hoàn toàn xác định: cái gì đi vào, cái gì bị loại, dữ liệu được ghép theo luật nào. Những chỗ đó mình biến thành hàm thuần, assert cứng, test như code đời thường. Không tốn một token nào, và cũng không có chỗ cho "cảm hứng".
Nghe đơn giản, nhưng đây là chỗ nhiều người làm ngược: để model quyết cả những thứ mà một dòng if là xong. Rồi sau đó lại đi test cái kết quả xác suất ấy bằng một cái assert cứng — vừa đắt, vừa không bao giờ ổn định.
Lớp 2 — Phần nào sinh ra thì chấm theo tiêu chí, và chấm nhiều lần.
Với đầu ra do model sinh, mình không hỏi "có đúng không", mà hỏi "trong 20 lần chạy, bao nhiêu lần đạt". Mình viết ra vài tiêu chí nghe rất người thay vì một cái so sánh bằng:
# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai thật
tiêu_chí = {
"bám_đúng_nguồn": "mọi khẳng định phải truy được về dữ liệu gốc",
"không_bịa_số": "không có con số nào tự sinh ra",
"giọng_đọc_được": "câu văn không rơi vào kiểu liệt kê máy móc",
"đúng_định_dạng": "khớp schema đầu ra, kiểm bằng luật cứng",
}
điểm_giỏi = pass_rate(agent, đầu_vào, tiêu_chí, n=20) # đo phân bố, không đo 1 lần
# Ngưỡng quyết định: không phải "pass/fail", mà là "đủ chín để giao hay chưa".Chìa khoá ở đây là đo phân bố. Một lần chạy đẹp là may mắn. Hai mươi lần chạy mà 18 lần đạt mới là tín hiệu. Và quan trọng hơn cả: khi nó trượt, mình xem cái kiểu trượt, chứ không chỉ xem cái điểm. Trượt theo một hướng lặp đi lặp lại thì đó là bug thiết kế, không phải rủi ro ngẫu nhiên.
Lớp 3 — Cho agent tự soi chính nó, ngay lúc chạy.
Đây là phần mình ưng nhất trong đám bài đang trending: hướng "reflection" — cho agent một vòng tự kiểm tra trước khi trả kết quả. Nó không trả cái đầu tiên nghĩ ra, mà tự hỏi: kết quả này có thật sự bám dữ liệu không, có tự mâu thuẫn không, có đủ điều kiện mình đã đặt ra không? Sai thì nó sửa, rồi mới giao.
Mình để ý một điều: hiệu quả nhất không phải là khi mình bảo nó "hãy suy nghĩ kỹ hơn". Mà là khi mình đưa nó tiêu chí kiểm tra cụ thể, cùng thứ để đối chiếu. Reflection không có tiêu chí thì chỉ là tự tin thêm một lần nữa, y nguyên như câu trả lời đầu.
Và phải nói cho công bằng: vòng tự sửa không miễn phí. Nó tốn thêm một lượt chạy, và nếu bạn cho nó quyền sửa vô hạn, nó sẽ có lúc sửa cho đẹp thay vì sửa cho đúng. Mình chặn bằng một số lượt tối đa, và bắt buộc: sửa xong vẫn phải đi qua Lớp 1 và Lớp 2. Reflection là lưới đỡ, không phải giấy chứng nhận.
Một bộ khung mình đang dùng, gói gọn trong đầu:
# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai thật
def chạy_an_toàn(đầu_vào):
dữ_liệu = luật_cứng_lọc(đầu_vào) # Lớp 1: xác định, không tốn token
kết_quả = agent_sinh(dữ_liệu) # Lớp 2: phần có xác suất
kết_quả = tự_soi(kết_quả, tiêu_chí, max_vòng=2) # Lớp 3: reflection có trần
if not đạt_tiêu_chí_cứng(kết_quả): # đóng cổng nếu vẫn không đạt
return None # thà không giao, còn hơn giao rác
return kết_quảCái dòng return None kia là quyết định khó nhất, và cũng là dòng đáng giá nhất. Một hệ thống biết im lặng khi không chắc thì đáng tin hơn một hệ thống luôn luôn nói.
Vì sao chuyện này đáng tiền
Nếu bạn đang build automation để bán — cho khách, hay cho chính cái side project của bạn — thì đây là chỗ tiền nằm.
Khách không trả tiền để agent của bạn "hay". Họ trả tiền để họ không phải kiểm tra lại nó. Cái khoảnh khắc họ thôi không mở kết quả ra soi nữa, đó là lúc automation thật sự có giá trị — và cũng là lúc bạn biến từ "làm giúp một việc" thành "gánh một phần trách nhiệm".
Và cái trách nhiệm đó, bạn không gánh được bằng cách viết thêm mười cái test kiểu cũ. Bạn gánh nó bằng cách chia đúng ba lớp: cái gì cứng, cái gì đo được, cái gì để nó tự soi, và cái gì mình chịu trách nhiệm cuối cùng.
Nếu chỉ mang về một điều, mình muốn anh em mang về câu này: test xanh không có nghĩa là agent tốt. Nó chỉ có nghĩa là bạn chưa tìm ra đầu vào làm nó trượt. Việc của bạn không phải là làm cho hết xanh, mà là biết chính xác nó sẽ đỏ ở đâu — và biết nó tự đứng dậy được bao nhiêu lần trước khi bạn phải ra tay.
Kết
Con agent của mình bây giờ vẫn sai. Khác một chỗ: mình biết nó sai kiểu gì, sai tầm bao nhiêu phần trăm, và khi nào thì nó tự chặn lấy mình thay vì đẩy rác về phía người dùng.
Cách nhìn cũ — "viết xong, test pass, giao" — chỉ đúng cho phần cứng của hệ thống. Còn phần mềm của hệ thống, phần thật sự suy nghĩ, thì phải được đối xử như một người tập sự có thử việc: chấm theo nhiều lần, xem xu hướng, và giữ một cái cổng ở cuối do mình khoá.
Anh em đang giao agent cho ai, hay chỉ tự dùng? Và anh em có dám để nó im lặng khi không chắc không — hay đang ép nó phải luôn nói ra một câu trả lời? Kể mình nghe ở comment nhé, mình đoán chúng ta đang mắc cùng một cái bẫy test.
Một phần của chuỗi mổ xẻ kỹ thuật tại zeodavu.com/blog. Nếu thấy hữu ích, theo dõi [zeodavu.com](/blog) — mỗi bài là một thứ mình thật sự dùng, vấp, và sửa lại.