TL;DR
- Sáu trục lỗi đặc trưng của PR do coding agent viết mà checklist review human-author của bạn không bắt được.
- Checklist sáu mục chạy tuyến tính trong một lượt review; mỗi mục có một động tác cụ thể: đảo assertion, jump-to-definition, filter diff, grep import, đọc constructor, so commit message với diff.
- Quy tắc ra quyết định cuối: nudge-1-câu fix được → ship, không fix được → reject. Không “comment rồi để đó”.
📊 Kết quả thực tế
- Bạn ngồi xuống với một PR do AI viết và đi từ đầu đến cuối trong một lượt review duy nhất.
- Bạn bắt được ít nhất bốn trong sáu nhóm lỗi mà checklist cũ không calibrate cho.
- Bạn rời PR với một quyết định ship hoặc reject có căn cứ, không phải một comment chờ.
Năm 2026, hộp thư PR của bạn có ba PR AI đáp xuống mỗi giờ, và cách “đọc kỹ từng dòng” mà checklist cũ của bạn dạy không sống sót qua thực tế đó. Bạn cần một checklist review pull request do AI viết 2026 mới: cố định, đủ ngắn để chạy trong một lượt review, đủ chặt để bắt sáu trục lỗi mà coding agent hay tạo và human author hiếm khi gây.
Hãy tưởng tượng một PR cụ thể: bốn commit, message refactor: extract auth helper, diff đụng 12 file trong đó 8 file nằm ngoài auth/. Test thêm 47 assertion, 100% pass, coverage tăng 8%. CI xanh. Bạn merge không?
Nếu checklist của bạn vẫn là checklist của 2024, bạn vừa merge một trong sáu lỗi đặc trưng mà coding agent thường để lại. Post này là sáu mục cố định để bắt chúng trước khi merge.
Prerequisites:
- Đã review code 1+ năm; thoải mái với
git diff, GitHub PR UI, chạy test locally. - Đã từng dùng ít nhất một coding agent (Claude Code, Cursor agent, Codex) ở mức nhìn được diff nó tạo ra.
- Repo có sẵn test runner chạy được trên một PR (CI hoặc local).
Hai thuật ngữ load-bearing trong post này, định nghĩa một lần ở đây:
- AI-authored PR: pull request có phần lớn diff sinh bởi coding agent (Claude Code, Cursor agent, Codex), không phải hand-written rồi nhờ AI sửa lỗi. Phần lớn nghĩa là > 70% line được agent viết ra, kể cả khi human đã prompt và accept từng đoạn.
- Behavioral diff: cách so sánh PR theo hành vi quan sát được (test chạy thật, snapshot output, contract test) thay vì so sánh từng dòng diff. Bạn sẽ dùng behavioral diff ở Bước 2 và Bước 5.
Vì sao bạn cần checklist review pull request do AI viết mới cho 2026?
Checklist review human-author của bạn calibrate cho lỗi logic và edge case, đúng những trục mà một human gây ra khi mệt. AI mệt khác. Coding agent ít khi sai logic if/else, nhưng rất giỏi bịa ở những trục khác mà human author hiếm khi đụng tới.
Sáu trục lỗi đặc trưng của AI author:
| Trục lỗi | Human author hiếm gặp vì |
|---|---|
| Test giả, over-mock | Human viết test biết mình đang assert cái gì |
| API dùng sai nhưng nhìn hợp lý | Human đọc docs, agent generate signature plausible |
| Scope nở ngoài mô tả | Human ngại đụng file ngoài task |
| Import/symbol bịa | Human không bịa được tên không nhớ |
| Side-effect ẩn ở module level | Human ít khi thêm code top-level vô cớ |
| Commit message lừa | Human viết message theo intent, không theo diff |
Bí quyết: sáu trục trên không trùng với trục lỗi của human author. Một checklist calibrate cho human chỉ ngẫu nhiên bắt được trục này. Calibrate lại, không thêm bước, chỉ đổi mục tiêu.
Bước 1: Vì sao phải chốt scope kỳ vọng trước khi mở diff?
Trước khi mở tab Files changed, viết ra một đến hai dòng “PR này được phép đụng cái gì”. Đây là baseline cho Bước 4 (scope nở) và Bước 6 (commit lừa). Bạn cần một anchor tồn tại độc lập với diff: nếu mở diff trước thì confirmation bias kéo bạn rationalize scope nở thành “à, hợp lý”, còn nếu viết scope kỳ vọng trước khi thấy diff thì bạn có một mỏ neo không bị diff move. Step này bắt failure mode mô tả PR mơ hồ và scope drift im lặng.
Bí quyết: baseline scope kỳ vọng viết bằng câu cụ thể đến mức filter được
git diff --stat. “Auth refactor” thì không. “ThêmverifyJwtvàoauth/jwt.ts, sửa import ởauth/handler.ts” thì có.
gh pr view 1234 --json title,body,headRefNameOutput ví dụ:
{ "title": "refactor: extract auth helper", "body": "Extract `verifyJwt` from `auth/handler.ts` into `auth/jwt.ts`. No behavior change.", "headRefName": "feat/extract-auth-helper"}Scope kỳ vọng bạn viết ra: thêm auth/jwt.ts với function verifyJwt; auth/handler.ts đổi để import từ đó; không file nào khác đổi.
Verify: scope kỳ vọng của bạn có đủ cụ thể để filter git diff --stat không? “Auth refactor” thì không. “Thêm verifyJwt vào auth/jwt.ts, sửa import ở auth/handler.ts” thì có. Nếu mô tả PR không đủ để bạn viết được dòng cụ thể đó, bạn đã có một red flag trước cả khi mở diff.
Bước 2: Phát hiện test giả và over-mock, test có thật sự test gì không?
Coding agent rất giỏi viết test pass. Pass không có nghĩa là verify behavior. Đây là failure mode đặc trưng nhất của PR AI: agent được tối ưu để CI xanh, mà cách dễ nhất để CI xanh là mock mọi dependency rồi assert mock đã được gọi. Coverage tăng, behavior không được verify dòng nào. Bước này có hai động tác bắt buộc chạy song song: đảo một assertion ngẫu nhiên để xem code path có thật sự được touch không, và đếm tỷ lệ mock so với assertion thực để bắt over-mock kéo behavioral diff về zero.
Bí quyết: test giả luôn pass khi bạn đảo assertion vì code path không được touch. Đảo
assertEqualthànhassertNotEqual, chạy lại; nếu vẫn pass, test đang assert một thứ khác với cái bạn nghĩ.
Động tác (a): đảo assertion. Chọn một test có vẻ load-bearing, đảo assertEqual sang assertNotEqual hoặc đổi giá trị expected sang giá trị rõ ràng sai. Chạy lại:
pytest tests/test_jwt.py::test_verify_valid_token -x --tb=shortNếu test vẫn pass sau khi đảo, đó là test giả. Test không touch code path bạn nghĩ.
Động tác (b): mock audit. Đếm:
grep -c "mock\|patch\|MagicMock" tests/test_jwt.pygrep -c "assert" tests/test_jwt.pyTỷ lệ mock/assert > 1:1 là red flag. > 2:1 là test đang assert mock được gọi với args mà chính mock tự trả về. Behavioral diff bằng zero.
Lived failure mode: PR gần nhất tôi gặp thêm 47 assertion, 46 trong đó assert một mock đã được gọi với args mà mock tự return. Coverage tăng 8%. Không một dòng production code nào được verify. Đảo bất kỳ assertion nào trong 46 cái đó, test vẫn pass.
Verify: sau khi đảo assertion, test phải fail. Nếu pass, comment yêu cầu author viết một assertion thật trên production code path.
Bước 3: Verify từng call site, API có dùng đúng signature không?
Với mỗi function call mới hoặc đổi trong diff, jump-to-definition và so với docs chính thức. Đừng tin signature trong diff. Agent generate code dựa trên signature plausible, mà plausible không phải đúng. Các kiểu sai thường gặp: nhầm sync/async (await một function không trả Promise, silently no-op), sai thứ tự args (db.query(params, sql) thay vì db.query(sql, params)), dùng deprecated overload trông giống current. Bước này bắt failure mode mà type checker pass, lint pass, test pass với mock, nhưng runtime ở production thì dispatch sai signature.
Bí quyết: chỉ jump-to-definition không đủ vì agent có thể đã import từ một module đồng tên. Bạn cần đối chiếu signature với docs chính thức của version pinned trong
package.jsonhoặcrequirements.txt.
Ví dụ một call có vẻ ổn:
const result = await db.query(sql, params, callback);Jump-to-def thấy:
// node-mysql2 v3.xdb.query(sql: string, params: any[], callback: (err, rows) => void): voiddb.query không trả Promise. await nó resolve về undefined ngay lập tức, callback chạy bất đồng bộ sau đó. result luôn là undefined. Test có thể vẫn pass nếu mock db.query trả về kết quả mong đợi qua sync return. Đây là chỗ Bước 2 và Bước 3 bắt chéo nhau.
Verify: types/docs khớp; chạy với input edge (null, empty array, một row DB thật). Một integration test thật giá trị hơn ba unit test mock.
Bước 4: So với scope đã chốt ở Bước 1, scope có nở ngoài mô tả không?
Filter git diff --stat theo scope kỳ vọng từ Bước 1. Mỗi file ngoài scope phải có lý do cụ thể trong commit message. “Cleanup” và “drive-by fix” không phải lý do. Agent có xu hướng “tiện tay” refactor file lân cận khi context còn chỗ, và mỗi file ngoài scope là review surface bạn chưa load context cho, nên bạn skim, mà đó là chỗ bug ẩn. Bước này bắt failure mode scope drift im lặng: PR mô tả là một việc, diff thực hiện một việc rưỡi, phần thừa thường là chỗ regression sinh ra.
Bí quyết: đừng rationalize “thôi cũng hợp lý” cho file ngoài scope. Action duy nhất khi scope nở là yêu cầu split PR, không phải “merge cẩn thận hơn”.
git diff --stat origin/main...HEAD | grep -v '^ auth/'Output ví dụ:
src/utils/logger.ts | 12 ++++++------ src/api/users/handler.ts | 34 ++++++++++++++++-------- 2 files changedHai file ngoài scope. git log --oneline origin/main..HEAD cho biết lý do “while we’re here, updated logger format”. Đó là scope nở. Yêu cầu split PR. Đừng rationalize “thôi cũng hợp lý”.
Verify: list file ngoài scope ≤ scope kỳ vọng cho phép. Nếu vượt, action duy nhất là yêu cầu split, không phải “merge cẩn thận hơn”.
Bước 5: Bắt cả lỗi compile được, import có thật và side-effect có ẩn không?
Bước này có hai động tác cho hai failure mode liên thông: grep mọi import không quen thuộc và verify symbol resolve đúng module mong đợi, rồi đọc constructor và top-level statement của mọi file mới hoặc đổi và chạy test với network off. Hallucinated import có thể compile được nếu có một symbol đồng tên ở namespace khác, nên type checker và lint không bắt. Side-effect ngầm trong module-level code thường không gọi từ test, nên test không bắt. Chạy production thì firing, và đó là chỗ silent fail sinh ra.
Bí quyết: test pass khi network và disk write bị restrict là điều kiện cần. Nếu test cần network ở import time, đó là bug, không phải feature, và là red flag side-effect ẩn.
python -c "from utils.security import sanitize_html; print(sanitize_html.__module__)"Lived failure mode: một PR import from utils.security import sanitize_html. Package utils có, module security có, function sanitize_html không tồn tại ở module đó. Nhưng có một sanitize_html đồng tên ở bleach.sanitizer mà utils/__init__.py cũ re-export gián tiếp. Code compile, test pass (vì mock), production silently no-op trên payload chứa script tag. Phát hiện ra khi security audit chạy XSS payload.
Lần khác: PR thêm import requests ở đầu file. Bình thường, trừ chuyện trong cùng PR agent đã tạo requests.py ở thư mục lân cận. Python resolve local trước stdlib. requests.get(url, timeout=30) gọi vào local module với get no-op trả None. Production fail im lặng.
Side-effect ẩn: đọc constructor và mọi statement top-level của file mới. Grep requests.get(, open(, os.environ, socket., subprocess. ở module level. Nếu thấy, hỏi “cái này chạy lúc nào?”. Câu trả lời thường là “import time”, không bao giờ là cái bạn muốn.
# Test với network restricted để bắt module-level network callunshare -n pytest tests/ # Linux# hoặc dùng pytest-socketpytest --disable-socket tests/Verify: mọi import resolve đúng module mong đợi; test pass khi network và disk write bị restrict. Nếu test cần network ở import time, đó là bug, không phải feature.
Bước 6: Đọc commit history so với code, message có khớp diff không?
Đọc commit message với mindset “mô tả này có verifiable không?”. Với ít nhất một commit ngẫu nhiên, so message với diff dòng-bằng-dòng. Agent rất giỏi viết commit message thuyết phục cho một diff đang thực sự làm việc khác, và message hay làm bạn skim diff. Skim diff là cách bug merge. Bước này bắt failure mode commit lừa: message hợp lý, history sạch, nhưng nội dung diff lệch khỏi mô tả từ 30% trở lên và phần lệch đó thường là chỗ nguy hiểm.
Bí quyết: message khớp diff trong vòng ±20% là threshold an toàn. Lệch nhiều hơn, nudge author rewrite message; nếu họ không thể viết message khớp diff, họ chưa đọc lại diff của chính họ.
git log --oneline origin/main..HEADa1b2c3d fix: null check in verifyJwte4f5g6h refactor: extract auth helperi7j8k9l test: add jwt verification casesm0n1o2p chore: update package.jsonRandom pick a1b2c3d:
git show a1b2c3dMessage nói “null check in verifyJwt”. Diff thật sự đổi cả retry policy ở http/client.ts, giảm timeout từ 30s xuống 5s, và thêm một null check như một dòng. Message không lừa hoàn toàn (null check có thật), nhưng message che 80% diff.
Verify: message khớp diff trong vòng ±20%. Nếu lệch nhiều hơn, nudge author rewrite commit message. Đừng tự rewrite cho họ: đó là re-establish trust qua một việc nhỏ. Nếu họ không thể viết message khớp diff, họ chưa đọc lại diff của chính họ.
Ra quyết định: ship hay reject sau một lượt review?
Quy tắc cuối: bất kỳ check nào fail mà bạn không thể fix bằng một nudge một câu cho author thì reject. Nudge chỉ work khi root cause là local (rename biến, thêm một assertion, sửa một import). Không nudge khi Bước 2, Bước 5, hoặc Bước 6 fail. Đó là vấn đề trust, không phải vấn đề diff.
| Check fail | Nudge được? | Action |
|---|---|---|
| Bước 1 (scope không viết ra được từ mô tả) | Có | Comment: viết lại mô tả PR cho cụ thể |
| Bước 2 (test giả) | Không | Reject, kèm ví dụ một test cụ thể đảo assertion vẫn pass |
| Bước 3 (API sai signature) | Có nếu 1-2 chỗ | Nudge, kèm link docs; reject nếu lan rộng |
| Bước 4 (scope nở) | Không | Reject, yêu cầu split PR |
| Bước 5 (import bịa, side-effect ẩn) | Không | Reject, kèm output verify command |
| Bước 6 (commit message lừa) | Không | Reject, yêu cầu rewrite history |
Bí quyết: “comment rồi để đó” là phản pattern. Nó tạo ra queue PR half-reviewed mà không ai chịu trách nhiệm. Hoặc bạn ship, hoặc bạn reject. Nudge là một dạng ship-pending, không phải dạng review-pending.
Verified outcome: sau khi chạy checklist này, bạn có một trong hai output: (a) PR đã merge, hoặc (b) PR có comment reject với lý do cụ thể map vào một trong sáu bước. Không có output thứ ba.
Common pitfalls:
- Bỏ Bước 1 vì “mô tả PR rõ ràng rồi”: mô tả rõ với agent không có nghĩa scope kỳ vọng của bạn rõ. Vẫn viết một dòng.
- Mock-audit nhưng bỏ đảo assertion: mock-audit bắt over-mock; đảo assertion bắt test giả. Hai chuyện khác nhau, cả hai đều cần.
- Reject mềm (“Looks good, just a few nits”): nếu một trong sáu bước fail, dùng “Request changes” trên GitHub. Mềm với agent author không có lợi cho ai.
FAQ
Q: Checklist này có chạy được cho PR do human viết không?
A: Có, nhưng over-engineered. Sáu trục này calibrate cho coding agent. Với human author, dùng checklist cũ của bạn: nó bắt đúng trục lỗi mà human gây.
Q: Bước 2 (đảo assertion) có scale không khi PR có 100 test?
A: Sample. Đảo 3 assertion ngẫu nhiên trên 3 file test khác nhau. Nếu cả ba vẫn pass sau khi đảo, drill toàn bộ: đó là pattern, không phải tai nạn.
Q: Tôi nên dành bao nhiêu thời gian cho một PR AI?
A: Câu hỏi này tự trả lời sai. Bạn không có thời gian cố định cho mọi PR. Part 2 của series này sẽ dạy cách phân loại PR trước khi đọc dòng nào, để bạn dồn thời gian vào đúng PR cần.
Q: Tools nào tự động hóa được mấy bước này?
A: Một số bước (grep import, đảo assertion ngẫu nhiên, scope diff so với mô tả) encode được vào CI. Part 3 của series sẽ dựng bộ job CI và PR bot làm việc đó.
Q: Nếu PR pass cả sáu checks vẫn có bug thì sao?
A: Checklist này giảm false-pass đặc trưng AI, không thay thế domain knowledge. Review code vẫn cần senior judgment ở trục logic: đó là chỗ bạn vẫn có lợi thế hơn agent.
Đóng lại trước khi sang Part 2
Bạn vừa có một checklist sáu mục để ngồi xuống với một PR do coding agent viết và đi từ đầu đến cuối: sáu trục lỗi đặc trưng (test giả, API plausible nhưng sai, scope nở, import bịa, side-effect ẩn, commit lừa), một quy tắc quyết định cuối, không “comment rồi để đó”. Copy nó vào Notion team mai dùng.
Checklist này chạy ngon khi bạn có thời gian đọc trọn một PR, nhưng vỡ trận đúng cái khoảnh khắc ba PR AI khác đáp xuống cùng một giờ và bạn không thể đọc từng dòng của cả ba.
Tiếp theo trong series: Part 2 — Triage cả luồng PR AI theo blast radius.
What to read next
- Part 2 — Triage cả luồng PR AI theo blast radius: khi ba PR AI đáp xuống cùng giờ, đọc-từng-dòng không scale. Part 2 dựng quy trình triage chạy trước khi đọc dòng nào: phân tier theo mức ảnh hưởng, dồn thời gian vào tier cao, spot-check tier thấp bằng behavioral diff.
- Part 3 — Đẩy vòng review vào CI (xuất bản 2026-05-27): encode checklist này và triage signal của Part 2 thành bộ job CI và PR bot, để vòng review chạy đúng kể cả khi bạn đang họp hoặc đang ngủ.
- Review Report: bands, voice floor, và owner-wins: cách đọc output của một review pipeline đã có, làm bài thực hành cho mindset “review là một số quyết định, không phải một số comment”.