·

Tiếng Việt: Verifying Correctness

Verifying Correctness

Chạy được không có nghĩa là đúng — và với code AI-generated, khoảng cách giữa hai điều đó thường rộng hơn bạn tưởng.

Vì Sao Logic AI Cần Được Kiểm Chứng Chủ Động

Một trong những cạm bẫy lớn nhất khi làm việc với AI coding assistant là đánh đồng "code chạy không lỗi" với "code đúng logic". Compiler pass, test hiện có pass, ứng dụng không crash khi bạn thử vài lần — tất cả những tín hiệu này chỉ chứng minh code không có lỗi ở những gì đã được kiểm tra, không chứng minh logic đúng trong mọi trường hợp.

AI sinh code dựa trên pattern thống kê từ dữ liệu huấn luyện, không phải dựa trên việc "hiểu" bài toán nghiệp vụ cụ thể của bạn theo cách một kỹ sư con người hiểu. Điều này dẫn tới một loại lỗi đặc trưng: logic có vẻ hợp lý ở cấp độ tổng quát (giống với hàng nghìn implementation tương tự AI từng thấy) nhưng sai ở chi tiết cụ thể của bài toán bạn đang giải — một business rule đặc thù, một thứ tự ưu tiên xử lý riêng của hệ thống, một edge case chỉ tồn tại vì lịch sử phát triển sản phẩm của bạn.

Kiểm chứng chủ động (active verification) nghĩa là bạn không chờ bug tự lộ ra qua việc sử dụng thực tế — bạn chủ động đi tìm nó bằng cách suy luận qua logic, đặt câu hỏi, và thử nghiệm có chủ đích trước khi code đi vào production. Đây là kỹ năng cốt lõi phân biệt engineer chỉ "dùng AI để gõ nhanh hơn" với engineer thực sự khai thác được lợi ích của AI mà không đánh đổi chất lượng.

Mẹo: Đặt cho mình quy tắc: không bao giờ coi "test hiện có pass" là bằng chứng đủ cho một đoạn logic mới quan trọng — luôn tự hỏi "test này có thực sự cover đúng cái tôi lo ngại không?"

Trace Thủ Công Qua Từng Bước Thực Thi: Happy Path Và Xa Hơn

Kỹ thuật nền tảng nhất để verify logic là tự tay trace (lần theo) từng bước thực thi của code với một bộ input cụ thể — giống như bạn đang là compiler/interpreter chạy từng dòng bằng trí não.

Bắt đầu với happy path (luồng chính, không có lỗi). Chọn một input điển hình nhất, phổ biến nhất trong thực tế, và trace qua từng dòng: biến này nhận giá trị gì, điều kiện này rẽ nhánh nào, hàm này trả về gì. Ghi ra giấy hoặc comment tạm nếu cần — đừng chỉ đọc bằng mắt và "cảm thấy đúng".

Sau đó chủ động đi lệch khỏi happy path. Đây là bước quan trọng hơn nhiều nhưng thường bị bỏ qua. Tự hỏi và trace tiếp: nếu bước thứ 2 trả về rỗng thì sao? Nếu request thứ hai tới trong lúc request đầu chưa xử lý xong? Nếu external service trả về lỗi 500 thay vì 200?

Trace cả những "đường không đi" (paths not taken). Với mỗi câu lệnh điều kiện (if/else, switch, early return), tự hỏi: nhánh nào tôi CHƯA trace? Che gần hết code lại và chỉ tập trung vào một nhánh cụ thể để không bị phân tâm.

Đặc biệt cẩn trọng với trạng thái được chia sẻ (shared state) và side effect. Nếu hàm sửa đổi một object được truyền vào, hay ghi vào biến ngoài scope, trace xem thứ tự các lệnh gọi có ảnh hưởng gì tới trạng thái đó theo thời gian không.

Kỹ thuật này tốn thời gian hơn đọc lướt, nhưng nó là cách duy nhất thực sự đáng tin cậy để phát hiện lỗi logic tinh vi mà không cần chạy hàng trăm test case.

Mẹo: Với logic phức tạp, thử kỹ thuật "rubber duck ngược" — thay vì giải thích code cho vịt cao su nghe, hãy tự đóng vai "vịt cao su" và yêu cầu chính mình trace từng dòng ra thành tiếng hoặc viết ra giấy như thể đang dạy người khác.

Yêu Cầu AI Giải Thích Lý Do Của Chính Nó — Và Vì Sao Giải Thích Đó Cũng Có Thể Sai

Một kỹ thuật verify phổ biến là hỏi ngược AI: "Giải thích tại sao bạn viết code như vậy." Kỹ thuật này hữu ích nhưng có một cái bẫy quan trọng cần hiểu: lời giải thích của AI không phải là bằng chứng độc lập cho tính đúng đắn của code — nó chỉ là một output khác từ cùng một quá trình sinh có thể hallucinate.

Nói cách khác, nếu AI viết code sai vì hiểu sai bài toán, khi bạn hỏi "tại sao", nó có xu hướng sinh ra một lời giải thích nhất quán với code sai đó — không phải một lời giải thích độc lập, khách quan về tính đúng. Đây gọi là hiện tượng rationalization (hợp lý hóa ngược): model tạo ra narrative nghe thuyết phục để "giải thích" cho quyết định nó đã đưa ra, dù quyết định đó vốn dĩ sai.

Vậy dùng kỹ thuật này thế nào cho đúng?

Dùng nó để kiểm tra tính nhất quán nội bộ, không phải tính đúng tuyệt đối. Nếu lời giải thích của AI mô tả một behavior khác với những gì code thực sự làm (bạn phải tự verify bằng cách đọc code hoặc chạy thử), đó là tín hiệu cảnh báo rất mạnh — nghĩa là ngay cả AI cũng "không nhất quán" với chính output của nó.

Hỏi câu hỏi cụ thể, không hỏi chung chung. Thay vì "giải thích code này", hỏi "Điều gì xảy ra nếu tham số X là 0? Trace qua từng dòng và cho tôi biết giá trị của biến Y tại mỗi bước." Câu hỏi cụ thể buộc AI phải "trace" thay vì chỉ tóm tắt chung chung, và bạn có thể tự verify từng bước trace đó.

Luôn đối chiếu lời giải thích với việc tự trace tay hoặc chạy thử thật. Không bao giờ dừng lại ở việc "AI giải thích nghe hợp lý" — đó chỉ là bước đầu, không phải bước cuối.

Cẩn trọng đặc biệt khi AI tự tin tuyệt đối. Ngôn ngữ càng chắc chắn ("Đảm bảo xử lý đúng mọi trường hợp", "Đây chắc chắn là cách tối ưu") càng cần verify kỹ hơn — sự tự tin của AI không tương quan với độ chính xác thực tế.

Mẹo: Coi việc hỏi AI giải thích như một cách để "trích xuất giả định" nó đang mang, không phải để "xác nhận tính đúng". Sau khi có giải thích, việc verify vẫn phải do chính bạn thực hiện qua trace tay hoặc test thật.

Dùng Test Như Đặc Tả Có Thể Thực Thi

Test không chỉ để "bắt lỗi" — khi được viết đúng cách, test chính là một dạng đặc tả (specification) có thể thực thi được, mô tả chính xác behavior mong đợi bằng ngôn ngữ không mơ hồ.

Viết test trước khi đọc kỹ implementation, dựa hoàn toàn trên spec/yêu cầu. Đây là nguyên tắc quan trọng nhất: nếu bạn viết test sau khi đã đọc code, bạn có nguy cơ vô thức viết test khớp với những gì code đã làm, thay vì khớp với những gì nó nên làm — kể cả khi code đó sai.

Test case nên được đặt tên mô tả rõ hành vi kỳ vọng, không chỉ mô tả input — ví dụ should_reject_coupon_when_expired_at_midnight thay vì test_case_3. Cách đặt tên này biến bộ test thành tài liệu sống về behavior nghiệp vụ.

Dùng test như công cụ đối thoại với AI. Khi bạn có bộ test rõ ràng thể hiện đúng spec, bạn có thể yêu cầu AI: "Implementation phải pass toàn bộ các test sau, không được sửa test để pass" — điều này khóa chặt behavior mong đợi, ngăn AI "lách" bằng cách sinh code khớp với test sai hoặc sửa test cho dễ pass.

Luôn kiểm tra: test có thực sự thất bại khi logic sai không? Một sai lầm phổ biến là viết test có vẻ đúng nhưng thực chất không bao giờ fail dù logic sai (ví dụ assertion quá lỏng lẻo, hoặc test mock quá nhiều khiến logic thật không được chạy). Thử cố tình làm hỏng implementation và xem test có bắt được không — kỹ thuật này gọi là mutation testing (kiểm thử đột biến) thủ công.

Mẹo: Với mọi business rule quan trọng, viết ít nhất một test "phủ định" — test khẳng định rõ ràng điều gì KHÔNG được xảy ra (ví dụ "discount không bao giờ vượt quá order total"), không chỉ test khẳng định điều gì xảy ra.

Phân Tích Điều Kiện Biên: Checklist Boundary Case Có Hệ Thống

Phần lớn bug nghiêm trọng trong code — AI-generated hay người viết — tập trung ở boundary condition (điều kiện biên), không phải ở happy path. Dưới đây là checklist boundary case có hệ thống nên áp dụng cho mọi logic quan trọng:

Giá trị số: 0, số âm, số thập phân rất nhỏ, số cực lớn (tràn kiểu dữ liệu), giá trị bằng đúng ngưỡng (== thay vì < hoặc <=).

Chuỗi và collection: chuỗi/mảng rỗng, chuỗi chỉ có khoảng trắng, ký tự đặc biệt/unicode, mảng có đúng 1 phần tử, mảng cực lớn.

Thời gian: đúng nửa đêm, chuyển múi giờ, năm nhuận, ngày cuối tháng, DST (giờ mùa hè/đông) nếu áp dụng.

Null/undefined/missing: field bị thiếu hoàn toàn (không phải null mà là không tồn tại trong object), giá trị mặc định có được áp dụng đúng không.

Đồng thời (concurrency): hai request cùng sửa một resource cùng lúc, race condition khi đọc-rồi-ghi (read-then-write) không có lock.

Quyền hạn và trạng thái: user không có quyền, resource đã bị xóa/archive, resource đang ở trạng thái trung gian (pending, đang xử lý).

Biên của chính business rule: ví dụ với coupon giảm giá tối đa 50%, thử đúng 50%, 50.01%, và 49.99% — đây là nơi off-by-one error (lỗi lệch một đơn vị) thường ẩn náu nhất.

Kỹ thuật thực hành: với mỗi tham số đầu vào của hàm, liệt kê ít nhất 3-5 giá trị biên liên quan trước khi đọc implementation, sau đó verify từng giá trị đó qua code hoặc test thật.

Mẹo: Biến checklist boundary case này thành một prompt template — yêu cầu AI tự liệt kê boundary case theo từng nhóm ở trên cho hàm cụ thể, rồi bạn tự verify từng case đó độc lập thay vì tin danh sách AI đưa ra là đầy đủ.

Phát Hiện Lỗi Logic Trong Code Async

Code bất đồng bộ (asynchronous — xử lý không đồng bộ, không chờ tuần tự) là nơi AI đặc biệt dễ sinh lỗi logic tinh vi, vì thứ tự thực thi thực tế khó suy luận hơn nhiều so với code đồng bộ đọc từ trên xuống dưới.

Race condition khi nhiều thao tác async chạy song song. Kiểm tra: nếu hai Promise/async function cùng sửa một resource, thứ tự hoàn thành có ảnh hưởng tới kết quả cuối không? AI thường viết code giả định thứ tự "tự nhiên" (theo thứ tự viết) trong khi thực tế async không đảm bảo điều đó.

Quên await hoặc dùng sai Promise.all vs tuần tự. Một lỗi rất phổ biến: AI dùng vòng lặp for với await bên trong khi lẽ ra nên dùng Promise.all để chạy song song (gây chậm không cần thiết), hoặc ngược lại dùng Promise.all khi các thao tác cần chạy tuần tự vì phụ thuộc lẫn nhau (gây race condition).

Error handling không đúng trong chuỗi async. Kiểm tra: nếu một trong nhiều Promise trong Promise.all reject, code có xử lý đúng không, hay toàn bộ silent fail? Với Promise.allSettled, code có xử lý đúng cả trường hợp rejected không, hay chỉ giả định mọi thứ fulfilled?

Callback/event listener không được cleanup, gây memory leak hoặc gọi lại nhiều lần ngoài ý muốn. Đặc biệt phổ biến trong code frontend với useEffect hoặc event listener không có cleanup function tương ứng.

Timeout và cancellation không được xử lý. Nếu một async operation cần bị hủy (ví dụ user rời trang trước khi request hoàn thành), code có xử lý AbortController hay tương đương không, hay vẫn tiếp tục chạy và cập nhật state đã không còn tồn tại?

Deadlock hoặc pending vĩnh viễn. Nếu có logic chờ lẫn nhau (ví dụ hai queue chờ nhau), kiểm tra kỹ có thể xảy ra tình huống không bao giờ resolve không.

Mẹo: Với mọi đoạn code async quan trọng, tự vẽ ra (trên giấy hoặc công cụ diagram đơn giản) timeline các event xảy ra theo nhiều kịch bản thứ tự khác nhau — đây là cách hiệu quả nhất để phát hiện race condition mà đọc code tuần tự không thể thấy được.

Ví Dụ Thực Tế: Kiểm Chứng Một Business Rule Phức Tạp

Hãy xem một ví dụ cụ thể: business rule "Tính phí vận chuyển: miễn phí nếu đơn hàng trên 500,000đ, giảm 50% nếu là thành viên VIP, và có phụ phí 20,000đ cho khu vực hải đảo — các điều kiện áp dụng theo thứ tự ưu tiên: khu vực hải đảo được tính trước, sau đó mới xét miễn phí/VIP."

Khi AI generate implementation cho rule này, một lỗi logic điển hình có thể xảy ra: AI áp dụng giảm giá VIP hoặc miễn phí TRƯỚC, rồi mới cộng phụ phí hải đảo — sai thứ tự so với rule gốc, dẫn tới kết quả tính phí sai cho đúng nhóm khách hàng phức tạp nhất (VIP ở hải đảo với đơn hàng trên 500,000đ).

Quy trình verify:

  1. Trace tay với input cụ thể nhất khó nhất: đơn hàng 600,000đ, khách VIP, giao tới hải đảo. Theo rule gốc: hải đảo +20,000đ trước → sau đó miễn phí vận chuyển (vì trên 500,000đ) → phí cuối = 0đ (miễn phí áp dụng sau, ghi đè luôn cả phụ phí hải đảo, hay phụ phí hải đảo vẫn giữ nguyên? — đây chính là điểm mơ hồ cần làm rõ trước khi verify code).

  2. Nhận ra spec gốc thực ra chưa đủ rõ ràng — đây là phát hiện quan trọng của quá trình verify: đôi khi lỗi không nằm ở code mà nằm ở spec mơ hồ ngay từ đầu. Việc trace tay buộc bạn phải làm rõ câu hỏi này trước khi có thể verify code đúng hay sai.

  3. Làm rõ spec với stakeholder hoặc tự quyết định theo domain knowledge, sau đó viết lại rule rõ ràng: "Phụ phí hải đảo luôn được cộng vào trước, miễn phí/giảm giá chỉ áp dụng lên phần phí vận chuyển cơ bản, không áp dụng lên phụ phí hải đảo."

  4. Verify lại code với rule đã làm rõ, trace qua đúng 4 tổ hợp: (VIP/không VIP) x (hải đảo/không hải đảo), với giá trị đơn hàng ở cả hai phía của ngưỡng 500,000đ — tổng cộng 8 kịch bản cần trace.

Ví dụ này minh họa một bài học quan trọng: verify tính đúng đắn không chỉ là "code có khớp spec không" mà đôi khi là "spec có đủ rõ ràng để verify không" — và việc trace logic một cách có hệ thống thường là cách duy nhất để phát hiện sự mơ hồ này trước khi nó trở thành bug trong production.

Mẹo: Khi trace một business rule có nhiều điều kiện tương tác, luôn liệt kê ra bảng tổ hợp đầy đủ (truth table) thay vì chỉ trace vài case "có vẻ đại diện" — sự tương tác giữa các điều kiện thường ẩn chứa lỗi ở đúng những tổ hợp bạn không nghĩ tới.

Thực Hành: Kiểm Chứng Một Discount Calculator AI-Generated

Bước 1: Generate implementation ban đầu

Dùng prompt:

Viết hàm TypeScript calculateDiscount(orderTotal: number, isVip: boolean, couponPercent: number | null): number

Rule:
- Nếu có coupon (couponPercent), áp dụng giảm giá theo % đó lên orderTotal, tối đa giảm 50%.
- Nếu là VIP và orderTotal > 1,000,000, giảm thêm 10% trên phần còn lại sau coupon.
- Kết quả cuối không được thấp hơn 0.
- Trả về số tiền cuối cùng khách phải trả (đã làm tròn xuống hàng nghìn).

Bước 2: Yêu cầu giải thích trước khi đọc code

Prompt: "Trước khi tôi đọc code, hãy giải thích từng bước tính toán cho input: orderTotal=2,000,000, isVip=true, couponPercent=60 (vượt ngưỡng tối đa 50%)."

Ghi lại câu trả lời của AI. Chú ý xem AI có tự nhận ra 60% vượt ngưỡng 50% và clamp lại không, hay bỏ qua điều kiện đó trong lời giải thích.

Bước 3: Tự viết boundary test case

Tự liệt kê (không nhìn code) các case cần test: coupon = 0%, coupon = 50% (đúng ngưỡng), coupon = 51% (vượt ngưỡng, phải bị clamp về 50%), coupon = null, VIP với order đúng 1,000,000 (bằng ngưỡng, không phải lớn hơn), VIP với order 1,000,001, kết quả âm giả định (coupon 100% cộng thêm VIP).

Bước 4: Yêu cầu AI generate test cho đúng các case này

Prompt: "Viết test case cho các tình huống sau, dùng đúng giá trị: [dán danh sách boundary case bạn tự liệt kê ở bước 3]. Không tự thêm case khác."

Bước 5: Chạy test và điều tra lỗi

Chạy test suite. Với mọi test fail, trace tay qua code để xác định: lỗi ở đâu — clamp coupon sai, so sánh ngưỡng dùng >= thay vì > (hoặc ngược lại), làm tròn sai hướng?

Bước 6: Verify phiên bản async (nếu áp dụng)

Nếu discount calculator này gọi thêm một async service để verify coupon hợp lệ (ví dụ check database), verify riêng: điều gì xảy ra nếu service đó timeout? Nếu hai request tính discount cho cùng coupon (có giới hạn số lần sử dụng) chạy đồng thời, có race condition trong việc trừ số lượt sử dụng còn lại không?

Bài thực hành này bao phủ toàn bộ kỹ thuật đã học trong chủ đề: trace tay, hỏi giải thích và đối chiếu, dùng test làm đặc tả, phân tích boundary case có hệ thống, và xử lý rủi ro async.

Mẹo: Lưu lại discount calculator này cùng bộ test làm "bài tập chuẩn" (benchmark) để mỗi khi thử một AI assistant mới, bạn có cách nhanh để đánh giá mức độ nó sinh lỗi logic ở đâu.

Những điểm chính

  • "Chạy được" không đồng nghĩa với "đúng" — logic AI cần được kiểm chứng chủ động, không chỉ dựa vào test hiện có hay việc ứng dụng không crash.
  • Trace tay qua từng bước thực thi, cả happy path lẫn các nhánh lệch khỏi nó, là kỹ thuật nền tảng để bắt lỗi logic tinh vi.
  • Hỏi AI giải thích lý do của nó hữu ích để kiểm tra tính nhất quán nội bộ, nhưng lời giải thích đó cũng có thể sai (rationalization) — luôn đối chiếu với trace tay hoặc chạy thử thật.
  • Test tốt nhất nên được viết dựa trên spec, trước khi đọc kỹ implementation — dùng test như một đặc tả có thể thực thi, không chỉ công cụ bắt lỗi.
  • Boundary condition (giá trị số, chuỗi/collection, thời gian, null, đồng thời, quyền hạn, biên của business rule) là nơi phần lớn bug nghiêm trọng ẩn náu.
  • Code async có lớp rủi ro riêng: race condition, sai await/Promise.all, error handling sai, cleanup thiếu, và deadlock — cần phân tích timeline thay vì đọc tuần tự.
  • Đôi khi verify code lộ ra rằng chính spec gốc chưa đủ rõ ràng — làm rõ spec trước khi kết luận code đúng hay sai.