Review code do AI sinh ra: từ checklist đến verdict có lý do
Câu hỏi bài này trả lời: nhận một diff do AI sinh ra mà test đã xanh, review thế nào để đi tới quyết định duyệt hay không, kèm lý do người khác kiểm lại được?
Cần biết trước: đọc được code Python cơ bản, biết diff và pull request. Nên đọc Tiêu chí hoàn tất có thể kiểm chứng trước, vì bài này dùng lại ý “một phép kiểm chưa từng đỏ thì chưa chứng minh được gì”. Phần lab chỉ dùng thư viện chuẩn của Python 3, không cài thêm gì.
Test xanh nói rằng các phép kiểm đã có đều đạt. Reviewer hỏi câu khác: thay đổi này có làm hệ thống tốt hơn không, và có điều gì mà các phép kiểm đã có không nhìn thấy? Với code do AI sinh ra, khoảng cách giữa hai câu hỏi dễ rộng hơn vì code và test thường đến từ cùng một nguồn, với cùng một cách hiểu yêu cầu. Tài liệu best practices của Claude Code nói rằng ngữ cảnh mới giúp review tốt hơn vì Claude không thiên vị code mà nó vừa viết. Trong user study của Perry và cộng sự (CCS 2023), người có trợ lý AI dựa trên model codex-davinci-002 viết code kém an toàn hơn nhóm không có trợ lý, và tin rằng code của mình an toàn hơn. Đó là một nghiên cứu với tác vụ và model của thời điểm đó, nên chỉ đáng dùng như lời nhắc cảnh giác, không phải con số áp cho mọi công cụ hôm nay.
Review để làm gì
Chuẩn review của Google Engineering Practices đặt mục đích chính là làm sức khỏe tổng thể của code base tốt dần theo thời gian. Từ đó có quy tắc: reviewer nên ưu tiên duyệt khi thay đổi chắc chắn làm hệ thống tốt hơn dù chưa hoàn hảo, và không duyệt thay đổi làm nó xấu đi. Cũng theo tài liệu này, reviewer có quyền sở hữu và trách nhiệm với code mình duyệt, và dữ kiện kỹ thuật thắng ý kiến cá nhân.
Vì vậy kết quả của một lần review là một verdict, không phải một cột dấu tick. Verdict có ba dạng: duyệt, yêu cầu sửa kèm lý do, hoặc chuyển cho người có chuyên môn phù hợp. Mỗi dạng cần lý do và bằng chứng. “Đã đi hết checklist” không phải lý do.
Ví dụ: diff làm test xanh mà vẫn sai
Yêu cầu: “Cho phép nhân viên hỗ trợ hoàn tiền từng phần cho đơn hàng; tổng số đã hoàn không được vượt số khách đã thanh toán.” Agent trả về một hàm refund và bốn test, tất cả đều xanh. Tạo thư mục trống, ví dụ review-lab, và các file sau. Dữ liệu hoàn toàn giả lập.
shop/orders.py là phần đã có sẵn trong code base trước khi có diff:
from dataclasses import dataclass
class RefundError(Exception):
pass
@dataclass
class User:
name: str
role: str
@dataclass
class Order:
id: int
owner: str
total: int
refunded: int = 0
shop/refunds.py là diff mà agent sinh ra, thứ bạn phải review:
from shop.orders import Order, RefundError, User
def refund(order: Order, amount: int, actor: User) -> int:
if amount <= 0:
raise RefundError("số tiền phải dương")
if amount > order.total:
raise RefundError("vượt số tiền đã thanh toán")
order.refunded += amount
return order.total - order.refunded
tests/test_refunds.py là bốn test đi kèm diff, cũng do agent viết:
import unittest
from shop.orders import Order, RefundError, User
from shop.refunds import refund
SUPPORT = User("an", "support")
class RefundTests(unittest.TestCase):
def test_partial_refund_reduces_remaining(self):
order = Order(id=1, owner="binh", total=100)
self.assertEqual(refund(order, 30, SUPPORT), 70)
def test_full_refund_leaves_nothing(self):
order = Order(id=2, owner="binh", total=100)
self.assertEqual(refund(order, 100, SUPPORT), 0)
def test_rejects_amount_above_total(self):
order = Order(id=3, owner="binh", total=100)
with self.assertRaises(RefundError):
refund(order, 150, SUPPORT)
def test_rejects_non_positive_amount(self):
order = Order(id=4, owner="binh", total=100)
with self.assertRaises(RefundError):
refund(order, 0, SUPPORT)
if __name__ == "__main__":
unittest.main()
Chạy test của diff:
python3 -m unittest discover -s tests
Ran 4 tests in 0.000s
OK
Test xanh, nhưng chưa thể duyệt. Google hỏi đúng câu cần hỏi ở phần test: test có thật sự đỏ khi code hỏng không, vì test không tự kiểm chính nó và phải có người kiểm test có hợp lệ không. Đọc diff với hai câu hỏi: ai được gọi hàm này và dòng nào từ chối người không được phép? và gọi lần thứ hai thì bất biến “tổng hoàn không vượt tổng đã thanh toán” còn đúng không?
Câu hỏi đầu có thể kiểm bằng một lệnh. Tham số actor xuất hiện ở đâu trong file?
grep -n actor shop/refunds.py
4:def refund(order: Order, amount: int, actor: User) -> int:
Chỉ một dòng: actor có trong chữ ký và không được dùng ở đâu. Hàm không từ chối ai. Với câu hỏi thứ hai, so sánh ở dòng 7 dùng order.total thay vì số còn có thể hoàn.
Reviewer viết hai probe, tức hai test nhắm đúng hai nghi vấn. Probe không thuộc diff; nó là công cụ của người review:
import unittest
from shop.orders import Order, RefundError, User
from shop.refunds import refund
SUPPORT = User("an", "support")
CUSTOMER = User("binh", "customer")
class RefundProbes(unittest.TestCase):
def test_customer_cannot_refund_even_own_order(self):
order = Order(id=1, owner="binh", total=100)
with self.assertRaises(PermissionError):
refund(order, 10, CUSTOMER)
def test_second_refund_cannot_exceed_remaining(self):
order = Order(id=2, owner="binh", total=100)
refund(order, 60, SUPPORT)
with self.assertRaises(RefundError):
refund(order, 60, SUPPORT)
Chạy probe trên diff gốc. Thông báo FAIL: đầy đủ khác nhau giữa các phiên bản Python, nên lab chỉ khóa các dòng ổn định:
python3 -m unittest review.probes
AssertionError: PermissionError not raised
AssertionError: RefundError not raised
Ran 2 tests in 0.000s
FAILED (failures=2)
Hai probe đỏ, đúng hai ca. Cho đến lúc này bạn có bằng chứng thay vì cảm giác. Lưu ý chính test của agent vẫn xanh trong lúc diff sai: không có test nào trong bốn test dùng người gọi không phải nhân viên, và không test nào gọi refund hai lần trên cùng một đơn.
Hai finding mẫu
Một finding đủ dùng khi người khác đọc xong có thể tái hiện, hiểu tác động và biết cách xác nhận đã sửa. Năm trường:
F1. Hoàn tiền nhiều lần vượt số đã thanh toán (mức: cao)
- Vị trí:
shop/refunds.pydòng 7,if amount > order.total. - Điều kiện tái hiện: đơn tổng 100; gọi
refund60 hai lần liên tiếp. Lần hai đáng lẽ bị từ chối vì chỉ còn 40, nhưng được chấp nhận vàorder.refundedthành 120. - Tác động: hoàn nhiều hơn số khách đã trả, mất tiền thật; lỗi nằm ở bất biến nghiệp vụ nên không có thông báo lỗi nào lộ ra.
- Cách xác minh: probe
test_second_refund_cannot_exceed_remainingđỏ trước sửa và xanh sau sửa; kiểm thêmorder.refundedkhông vượtorder.totalsau mọi chuỗi gọi trong test.
F2. Hàm hoàn tiền không kiểm quyền người gọi (mức: cao)
- Vị trí:
shop/refunds.pydòng 4 (tham sốactor) và cả thân hàm: không dòng nào đọcactor. - Điều kiện tái hiện: gọi
refund(order, 10, CUSTOMER)vớirole="customer"; hàm chạy và ghiorder.refunded. - Tác động: bất kỳ người gọi nào tới được hàm này đều hoàn tiền được, kể cả khách cho chính đơn của mình. Đây là dạng lỗi kiểm soát truy cập mà OWASP Top 10:2025, mục A01 xếp đầu bảng.
- Cách xác minh: probe
test_customer_cannot_refund_even_own_orderđỏ trước sửa, xanh sau sửa; thêm test từ chối vào bộ test chính thức của diff.
OWASP ghi hai điểm khớp với hai finding này: truy cập nên mặc định bị từ chối (deny by default) và các giới hạn nghiệp vụ nên được thi hành bởi domain model; đồng thời lập trình viên và QA nên đưa kiểm soát truy cập vào unit test và integration test.
Sửa cả hai trong shop/refunds.py, giữ nguyên bốn test cũ:
from shop.orders import Order, RefundError, User
def refund(order: Order, amount: int, actor: User) -> int:
if actor.role != "support":
raise PermissionError("chỉ nhân viên hỗ trợ được hoàn tiền")
if amount <= 0:
raise RefundError("số tiền phải dương")
remaining = order.total - order.refunded
if amount > remaining:
raise RefundError("vượt số tiền còn có thể hoàn")
order.refunded += amount
return remaining - amount
Chạy lại cả test của diff và probe:
python3 -m unittest discover -s tests 2>&1 | tail -n 3
python3 -m unittest review.probes 2>&1 | tail -n 3
Ran 4 tests in 0.000s
OK
Ran 2 tests in 0.000s
OK
Hai probe nên được chuyển vào tests/ của dự án để lần sau còn bắt được. Probe chỉ đứng ngoài khi nó còn là công cụ của riêng người review.
Ba tầng review theo trigger
Không phải diff nào cũng cần cùng mức soi. Cách chia dưới đây là của bài, khớp với thực hành chung hơn là một chuẩn:
| Tầng | Áp khi nào | Ai hoặc cái gì làm | Bắt được | Không bắt được |
|---|---|---|---|---|
| Kiểm tự động | Mọi diff | CI: format, lint, type, test, secret scan | Lỗi cú pháp, quy ước, hồi quy đã có test | Hành vi chưa có test: cả hai lỗi của ví dụ đều lọt qua |
| Review logic | Mọi diff | Người, hoặc agent khác tác giả | Bất biến sai, ca biên, nhánh thiếu, test không đỏ khi code sai | Điều cần chuyên môn mà reviewer không có |
| Review chuyên sâu | Diff chạm tiền, quyền hoặc danh tính, dữ liệu cá nhân, schema hay migration, dependency mới, đồng thời, API công khai | Người có chuyên môn của vùng đó | Rủi ro bảo mật, thiết kế, đồng thời | Thứ ngoài phạm vi người duyệt đã khai báo |
Google ghi rõ cách xử lý khi mình không đủ chuyên môn: nếu bạn hiểu code nhưng không thấy đủ năng lực cho một phần như quyền riêng tư, bảo mật, đồng thời, hãy bảo đảm có reviewer đủ năng lực trên thay đổi đó. Khi chỉ review một phần, ghi rõ trong comment phần nào đã review.
Ai chịu trách nhiệm, và agent tự review được đến đâu
- Người duyệt chịu trách nhiệm. Thay đổi đã merge là của người duyệt và người merge, bất kể code do người hay agent viết. Một lỗi đã lọt qua không được giải thích bằng “agent sinh ra như vậy”.
- Agent review là công cụ hỗ trợ, không phải cổng cuối. Agent đã viết code mà tự duyệt trong cùng ngữ cảnh dễ thiên vị code của mình (xem tài liệu Claude Code ở trên). Cách giảm: cho reviewer ngữ cảnh mới, chỉ có diff và tiêu chí; tài liệu Claude Code mô tả đúng mẫu reviewer trong subagent mới, thấy diff và tiêu chí chứ không thấy lập luận đã sinh ra thay đổi.
- Kết quả của reviewer agent cũng là finding cần xác minh. Nó chưa phải bằng chứng cho tới khi có lệnh hoặc test tái hiện được. Một lời “code ổn” không kèm bằng chứng thì không đổi được verdict.
Chuyển cho người có chuyên môn (hoặc người khác) khi gặp một trong các dấu hiệu:
- Diff chạm vùng ở bảng trên mà reviewer hiện tại không đủ chuyên môn.
- Finding không tái hiện được, hoặc reviewer không hiểu code. Google lưu ý: nếu bạn không hiểu code, nhiều khả năng người sau cũng không hiểu, nên yêu cầu tác giả làm rõ trước khi duyệt.
- Tác giả và reviewer bất đồng. Google đề xuất thử đạt đồng thuận theo tài liệu trước, rồi leo thang lên thảo luận rộng hơn, người phụ trách kỹ thuật hoặc maintainer; đừng để thay đổi nằm đó vì bất đồng.
- Diff chứa thứ không nên có trong code: thông tin đăng nhập, dữ liệu thật của khách, hoặc dependency mới không giải thích được.
Đối chiếu checklist với hai lỗi mẫu
Một checklist có ích khi mỗi mục là câu hỏi đòi bằng chứng, không phải ô tick. Đối chiếu với hai lỗi trên:
| Câu hỏi của checklist | Bằng chứng cần có | Bắt F1 (logic) | Bắt F2 (quyền) |
|---|---|---|---|
| Test của diff đã xanh chưa? | Lệnh và output | Không | Không |
| Test có đỏ khi code sai không (đưa bản lỗi hoặc viết probe)? | Output probe đỏ | Có | Có |
| Mỗi tham số của hàm có được dùng không? | grep -n tên tham số | Không | Có: actor chỉ có ở chữ ký |
| Ai gọi được hàm này, dòng nào từ chối người không được phép? | Chỉ ra dòng hoặc test từ chối | Không | Có |
| Sau lần gọi thứ hai và thứ ba, bất biến nghiệp vụ còn đúng không? | Test chuỗi gọi | Có | Không |
| Diff có chạm vùng cần review chuyên sâu (tiền, quyền) không? | Danh sách trigger đã đối chiếu | Báo cần soi | Báo cần soi |
Dòng đầu tiên là dòng nguy hiểm: trả lời “có” và không bắt được lỗi nào. Một checklist chỉ gồm các mục kiểu đó sẽ tick đủ mà vẫn cho qua cả hai lỗi.
Mẫu dùng lại
Mẫu finding:
F<n>. <tên ngắn> (mức: cao | trung bình | thấp)
- Vị trí: <file:dòng>
- Điều kiện tái hiện: <đầu vào hoặc chuỗi thao tác cụ thể>
- Tác động: <chuyện gì xảy ra với người dùng hoặc dữ liệu>
- Cách xác minh: <lệnh hoặc test; kết quả trước và sau sửa>
Mẫu verdict:
Verdict: duyệt | yêu cầu sửa | chuyển người có chuyên môn
Lý do: <các finding quyết định verdict>
Đã kiểm: <lệnh đã chạy, kết quả>
Chưa kiểm: <điều chưa kiểm và vì sao>
Điều kiện duyệt: <cần gì để chuyển sang duyệt>
Prompt cho agent reviewer (chạy ở ngữ cảnh mới, chỉ đưa diff và tiêu chí):
Bạn review diff dưới đây. Bạn không biết agent đã suy luận thế nào khi viết nó.
Tiêu chí: <yêu cầu và bất biến nghiệp vụ>
Với mỗi vấn đề, trả lời theo mẫu finding gồm vị trí, điều kiện tái hiện, tác động, cách xác minh.
Chỉ báo vấn đề bạn tái hiện được hoặc chỉ ra được dòng cụ thể.
Ghi rõ điều bạn chưa kiểm. Không kết luận "ổn" khi chưa nêu bằng chứng.
Giới hạn và lỗi thường gặp
Giới hạn của bài
- Lab giả lập và không gọi model nào. Bài cho thấy test của diff xanh trong lúc có hai lỗi, và hai probe do người viết bắt được; nó không đo reviewer agent bắt lỗi giỏi đến đâu hay review bằng người có bỏ sót hơn máy không.
- Ví dụ chỉ có hai lỗi và một hàm. Review thật còn thiết kế, độ phức tạp, đặt tên, tài liệu mà bài không đi sâu; xem danh mục điều cần soi của Google.
- Perry và cộng sự chạy với tác vụ, ngôn ngữ và một model của năm 2022; bài chỉ dùng nhận định định tính. Cách chia ba tầng và các dấu hiệu chuyển người là đề xuất của bài, không phải chuẩn.
- Lab chạy ngày 2026-10-02 trên macOS 27.0.1 arm64 (Python 3.14.8); Linux, Windows và Python 3.10–3.13 chưa thử. Trên Windows dùng
pythonthay chopython3và lệnhgrepcần Git Bash hoặc tương đương.
Lỗi thường gặp
| Lỗi | Hậu quả | Cách tránh |
|---|---|---|
| Coi test xanh là đủ để duyệt | Lỗi ngoài phạm vi test lọt qua | Hỏi test có đỏ khi code sai không; viết probe |
| Review do chính agent đã viết code, trong cùng ngữ cảnh | Thiên vị code của mình | Ngữ cảnh mới, chỉ có diff và tiêu chí; vẫn xác minh finding |
| Checklist toàn ô yes/no | Tick đủ mà không có bằng chứng | Mỗi mục kèm bằng chứng đọc lại được |
| Không ghi điều chưa kiểm | “Đã review” bị đọc thành “đã kiểm hết” | Mẫu verdict có dòng “Chưa kiểm” |
| Finding thiếu điều kiện tái hiện | Tác giả không sửa được hoặc cãi nhau về ý | Đủ năm trường; có lệnh xác minh |
| Để probe ngoài bộ test chính thức | Lỗi quay lại mà không ai bắt | Chuyển probe thành test của dự án sau khi sửa |
| Duyệt thay đổi về quyền hoặc tiền mà không có người chuyên môn | Rủi ro bảo mật lọt qua | Trigger chuyển người; ghi rõ phần đã review |
Học tiếp
- Tiêu chí hoàn tất có thể kiểm chứng: viết tiêu chí và verifier để diff có bằng chứng ngay từ đầu.
- Chọn context khi sửa code: đưa cho agent đủ ngữ cảnh để diff ít lỗi hơn ngay từ lần sinh.
- Dựng harness để AI agent làm việc đáng tin: đặt các cổng kiểm và cổng người ở đúng chỗ trong vòng đời task.
Nguồn tham khảo
- Google, The Standard of Code Review và What to look for in a code review, Engineering Practices (đọc ngày 2026-10-02).
- OWASP, Top 10:2025, A01 Broken Access Control (đọc ngày 2026-10-02).
- Claude Code, Best practices, các mục “Add an adversarial review step” và “Run multiple Claude sessions” (đọc ngày 2026-10-02).
- N. Perry và cộng sự, Do Users Write More Insecure Code with AI Assistants?, CCS 2023, arXiv:2211.03622 (v3, 2023-12-18).