Một prompt review code nên nói cho model biết cần tìm gì, mỗi phát hiện nghiêm trọng đến mức nào, và cần đưa ra bằng chứng gì. "Review đoạn code này" sẽ nhận về một danh sách góp ý thân thiện, trong đó một lỗ hổng bảo mật nằm lẫn giữa mẹo viết docstring và một sở thích đặt tên. Một prompt review có checklist, mức độ nghiêm trọng và yêu cầu diff sẽ nhận về một báo cáo bạn có thể xử lý ngay, từng phát hiện một.
Từ "review đoạn này" đến checklist có mức độ nghiêm trọng
Đoạn code ở cả hai tab đều có hai bug thật. Hãy so sánh cách mỗi câu trả lời trình bày chúng.
def get_user(conn, username):
cur = conn.cursor()
cur.execute(f"SELECT id, email FROM users WHERE name = '{username}'")
row = cur.fetchone()
return {"id": row[0], "email": row[1]}Nhìn chung hàm ngắn gọn và dễ theo dõi. Một vài góp ý:
- Thêm type hint, ví dụ
username: str, để chữ ký hàm rõ ràng hơn. - Thêm docstring giải thích hàm trả về gì.
- Dùng truy vấn tham số hóa. Ghép SQL bằng f-string có thể dẫn đến SQL injection.
- Xử lý trường hợp không có người dùng.
fetchone()có thể trả vềNone. - Đặt tên:
fetch_usercó thể mô tả việc truy cập cơ sở dữ liệu rõ hơn.
Câu trả lời đầu tiên tìm ra cả hai bug, và đó chính là vấn đề khi đánh giá một lần review qua những gì nó nhắc tới. Lỗi injection nằm thứ ba trong danh sách năm mục, được nói nhẹ đi thành "có thể dẫn đến", không có cách sửa, đứng cạnh một mẹo về docstring. Người đọc lướt qua sẽ thêm type hint rồi bỏ qua.
Prompt thứ hai thay đổi bốn điều:
- Phạm vi. "Chỉ bug, bảo mật và xử lý lỗi. Bỏ qua phong cách" loại bỏ phần nhiễu. Phong cách là việc của linter và formatter, chúng nhanh hơn và không bao giờ tự mâu thuẫn.
- Mức độ nghiêm trọng. Nó buộc model phải xếp hạng, và một danh sách có xếp hạng cho bạn biết nên bắt đầu từ đâu.
- Một đầu vào gây lỗi. "
x' OR '1'='1" biến một rủi ro mơ hồ thành một minh chứng bạn có thể chạy. Đây cũng là bộ lọc tốt nhất cho cảnh báo sai, như phần cuối sẽ cho thấy. - Diff. Một diff nhỏ cho mỗi phát hiện có thể được áp dụng hoặc từ chối riêng rẽ. Một hàm được viết lại trộn mọi chỗ sửa với những thay đổi không ai yêu cầu.
"Nếu ở một mức độ nào đó không có gì, đừng bịa ra" quan trọng hơn vẻ ngoài của nó. Khi được nhờ review, model có xu hướng đưa ra phát hiện ngay cả khi chẳng có gì nhiều để tìm, và những phát hiện yếu nhất chỉ làm danh sách dài thêm. Cho phép rõ ràng việc không báo cáo gì ở một mức độ sẽ cắt bớt phần độn đó.
Ngữ cảnh người review cần có
Một người review biết code dùng để làm gì và đầu vào của nó đến từ đâu. Model chỉ biết những gì bạn dán vào. "username lấy thẳng từ một form đăng nhập" là câu biến SQL injection thành một phát hiện mức cao thay vì chỉ là lý thuyết. Ngữ cảnh hữu ích cho một lần review:
- Đầu vào đến từ đâu: người dùng, một dịch vụ nội bộ khác, một tệp cấu hình do bạn kiểm soát.
- Cái gì gọi tới đoạn code và nó mong nhận lại gì.
- Môi trường chạy: phiên bản ngôn ngữ, framework, driver cơ sở dữ liệu. Cách sửa lỗi injection ở trên dùng
?vì sqlite3 dùng placeholder đó; một số driver khác dùng%s. - Điều bạn quan tâm nhất với thay đổi này: tính đúng đắn, bảo mật, hiệu năng, hoặc cả ba.
Một prompt review dùng lại được
Khối này biến checklist thành một mẫu. Đổi ngôn ngữ, trọng tâm và code, còn các quy tắc thì giữ nguyên. Tắt phần ràng buộc là cách nhanh để thấy bao nhiêu phần nhiễu quay lại khi thiếu nó.
async function loadAll(ids) {
const results = [];
ids.forEach(async (id) => {
const res = await fetch(`/api/items/${id}`);
results.push(await res.json());
});
return results;
}1. Cao: hàm trả về trước khi bất kỳ request nào xong (dòng 3)
forEach không chờ các callback async. loadAll trả về mảng results rỗng ngay lập tức, còn các lệnh push xảy ra sau đó. Mọi nơi gọi đều nhận được [].
2. Trung bình: request thất bại bị coi là dữ liệu (dòng 5)
Response 404 hay 500 không phải là ngoại lệ trong fetch. Một body lỗi dạng JSON sẽ được phân tích và push vào như thể là một item, còn một body không phải JSON khiến res.json() ném lỗi bên trong một callback không ai await.
Cách sửa cho cả hai, đồng thời giữ kết quả theo đúng thứ tự của ids:
async function loadAll(ids) {
- const results = [];
- ids.forEach(async (id) => {
- const res = await fetch(`/api/items/${id}`);
- results.push(await res.json());
- });
- return results;
+ return Promise.all(
+ ids.map(async (id) => {
+ const res = await fetch(`/api/items/${id}`);
+ if (!res.ok) throw new Error(`Item ${id}: HTTP ${res.status}`);
+ return res.json();
+ })
+ );
}
Dòng vai trò đặt ra một tiêu chuẩn ("chỉ vấn đề thật và không gì khác"), đó là chỗ role prompting giúp ích nhiều nhất. Nếu một chương trình đọc kết quả review, chẳng hạn một script đăng các phát hiện thành bình luận trên pull request, hãy yêu cầu JSON với các trường cố định thay vì văn xuôi; đầu ra có cấu trúc chỉ cách làm.
Xử lý cảnh báo sai
Một số phát hiện sẽ sai. Model không thấy được nơi gọi đã kiểm tra đầu vào, ràng buộc cơ sở dữ liệu khiến một trường hợp không thể xảy ra, hay lý do một dòng code trông lạ là có chủ đích. Nó cũng có thể hiểu sai cách một thư viện hoạt động; ảo giác AI giải thích vì sao một khẳng định tự tin chưa phải là một khẳng định đã được kiểm chứng.
Hãy thử từng phát hiện với đầu vào gây lỗi của nó. Nếu bạn không thể tạo ra một đầu vào chạm tới vấn đề trong hệ thống của mình, hãy phản biện bằng phần ngữ cảnh còn thiếu thay vì chấp nhận cách sửa:
function formatPrice(cents) {
return `$${(cents / 100).toFixed(2)}`;
}
Nó chỉ được gọi từ `renderCart`, hàm này return sớm trừ khi `Number.isInteger(cents)` là true. Đây có còn là vấn đề không? Nếu không, hãy rút lại phát hiện đó.Với phép kiểm tra đó thì không: formatPrice chỉ luôn nhận một số nguyên, nên phát hiện này không áp dụng và tôi rút lại.
Có một điều phép kiểm tra chưa bao quát: số nguyên âm vẫn qua được Number.isInteger, và formatPrice(-500) trả về "$-5.00". Nếu tiền hoàn lại hoặc giảm giá có thể đi tới hàm này, có lẽ bạn muốn "-$5.00". Nếu không, chẳng có gì cần thay đổi.
Một model khi được đưa bằng chứng mới nên hoặc rút lại phát hiện, hoặc giải thích vì sao nó vẫn đúng khi đã xét đến bằng chứng đó. Cả hai đều hữu ích. Một model cứ đồng ý với mọi phản biện thì không phải đang review, nên hãy đánh giá câu trả lời qua lý lẽ của nó, chứ không phải qua việc nó có đồng ý hay không. Với code do chính model viết, hãy review trong một cuộc trò chuyện mới, để việc review không bị nhìn qua cùng lối suy luận đã tạo ra đoạn code. Cũng những thói quen về phạm vi, ngữ cảnh và test giúp code tốt hơn ngay từ đầu; xem prompt để viết code.
Câu hỏi thường gặp
Prompt tốt để review code là gì?
Nêu rõ cần review những gì (bug, bảo mật, xử lý lỗi), yêu cầu mức độ nghiêm trọng cho mỗi phát hiện, và đòi một đầu vào cụ thể gây ra vấn đề cùng một diff để sửa nó. Dặn model bỏ qua định dạng và cách đặt tên trừ khi được hỏi. Cung cấp cho nó ngữ cảnh mà một người review sẽ có: code dùng để làm gì và cái gì gọi tới nó.
AI có thể thay thế việc review code của con người không?
Không, nhưng nó là một lượt review đầu hữu ích. Nó nhanh, và giỏi phát hiện các kiểu bug phổ biến như None chưa được xử lý, thiếu await hay SQL ghép từ chuỗi. Nó không biết các quy tắc của sản phẩm, phần còn lại của codebase hay lý do một quyết định được đưa ra, và một số phát hiện của nó sẽ sai. Hãy dùng nó trước khi người thật review, chứ không phải thay cho người thật.
Vì sao AI khi review code lại báo những vấn đề không có thật?
Model chỉ thấy đoạn code bạn dán vào. Nếu một giá trị đã được nơi gọi kiểm tra, hoặc một trường hợp không bao giờ xảy ra trong hệ thống của bạn, model không thể biết điều đó nên vẫn đánh dấu rủi ro. Yêu cầu một đầu vào cụ thể gây lỗi sẽ lọc bớt nhiều trường hợp như vậy: một phát hiện không có đầu vào thực tế nào kích hoạt được thường là cảnh báo sai.
Có nên nhờ AI viết lại code trong lúc review không?
Hãy yêu cầu các diff nhỏ, mỗi phát hiện một diff, thay vì một tệp được viết lại. Viết lại toàn bộ sẽ trộn các chỗ sửa với những thay đổi không ai yêu cầu, và bạn lại phải review cả bản viết lại. Diff cho phép bạn chấp nhận hoặc từ chối từng phát hiện một cách riêng rẽ.