Промпт для код-ревью должен сказать модели, что искать, насколько серьёзна каждая находка и какое доказательство привести. «Проверь этот код» даёт дружелюбный список советов, где дыра в безопасности стоит между советом про docstring и предпочтением в именовании. Промпт для ревью с чек-листом, уровнями серьёзности и просьбой о диффах даёт отчёт, по которому можно действовать, находка за находкой.
От «проверь это» к чек-листу с серьёзностью
В коде на обеих вкладках два настоящих бага. Сравните, как их подаёт каждый ответ.
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]}В целом функция короткая и понятная. Несколько советов:
- Добавьте аннотации типов, например
username: str, чтобы сигнатура была яснее. - Добавьте docstring с описанием того, что возвращает функция.
- Используйте параметризованные запросы. Сборка SQL через f-строки может привести к SQL-инъекции.
- Обработайте отсутствующих пользователей.
fetchone()может вернутьNone. - Именование:
fetch_user, возможно, лучше описывает обращение к базе данных.
Первый ответ нашёл оба бага, и в этом проблема оценки ревью по тому, что в нём упомянуто. Инъекция стоит третьей в списке из пяти, смягчена до «может привести», без исправления, рядом с советом про docstring. Читатель, пробежав ответ глазами, добавил бы аннотации типов и пошёл дальше.
Второй промпт изменил четыре вещи:
- Рамки. «Только баги, безопасность и обработка ошибок. Стиль пропускай» убирает шум. Стиль: работа линтера и форматтера, которые быстрее и никогда сами себе не противоречат.
- Серьёзность. Она заставляет модель ранжировать, а ранжированный список говорит, с чего начать.
- Вход, который вызывает проблему. «
x' OR '1'='1» превращает расплывчатый риск в демонстрацию, которую можно запустить. Это ещё и лучший фильтр ложных срабатываний, как показывает последний раздел. - Диффы. Маленький дифф на каждую находку можно применить или отклонить отдельно. Переписанная функция смешивает все исправления с изменениями, о которых никто не просил.
«Если на каком-то уровне серьёзности ничего нет, не выдумывай» важнее, чем кажется. Когда модель просят сделать ревью, она склонна выдавать находки, даже когда искать почти нечего, и самые слабые раздувают список. Явное разрешение ничего не сообщать на каком-то уровне это раздувание убирает.
Контекст, который нужен ревьюеру
Ревьюер-человек знает, для чего код и откуда приходят его входные данные. Модель знает только то, что вы вставили. «username приходит прямо из формы входа»: это предложение, которое делает SQL-инъекцию находкой высокой серьёзности, а не теоретической. Полезный контекст для ревью:
- Откуда приходят входные данные: от пользователей, из другого внутреннего сервиса, из файла конфигурации, который вы контролируете.
- Что вызывает код и что ожидает получить обратно.
- Среда выполнения: версия языка, фреймворк, драйвер базы данных. Исправление инъекции выше использует
?, потому что такой заполнитель у sqlite3; некоторые другие драйверы используют%s. - Что для этого изменения важнее всего: правильность, безопасность, производительность или всё сразу.
Промпт для ревью, который можно переиспользовать
Этот блок превращает чек-лист в шаблон. Поменяйте язык, фокус и код, а правила оставьте. Отключите часть с ограничениями, и вы быстро увидите, сколько шума возвращается без неё.
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. Высокая: функция возвращается раньше, чем завершается хоть один запрос (строка 3)
forEach не ждёт асинхронных колбэков. loadAll сразу возвращает пустой массив results, а добавления происходят позже. Любой вызывающий код получает [].
2. Средняя: неудачные запросы считаются данными (строка 5)
Ответ 404 или 500 в fetch не является исключением. JSON-тело с ошибкой разбирается и добавляется так, будто это элемент, а тело не в формате JSON заставляет res.json() выбросить исключение внутри колбэка, который никто не ждёт.
Исправление для обеих, которое заодно сохраняет порядок результатов таким же, как в 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();
+ })
+ );
}
Строка с ролью задаёт планку («только о реальных проблемах»), и именно здесь ролевой промптинг помогает больше всего. Если ревью читает программа, например скрипт, который публикует находки комментариями к pull request, просите JSON с фиксированными полями вместо прозы; как это сделать, показано на странице структурированный вывод.
Что делать с ложными срабатываниями
Часть находок окажется неверной. Модель не видит вызывающий код, который проверяет вход, ограничение в базе данных, которое делает какой-то случай невозможным, или причину, по которой странная на вид строка написана намеренно. Она также может ошибаться в том, как ведёт себя библиотека; почему уверенное утверждение не равно проверенному, объясняет страница галлюцинации ИИ.
Проверяйте каждую находку её входом. Если вы не можете подобрать вход, который в вашей системе доходит до проблемы, возразите, дав недостающий контекст, а не принимайте исправление:
function formatPrice(cents) {
return `$${(cents / 100).toFixed(2)}`;
}
Её вызывает только `renderCart`, который выходит досрочно, если `Number.isInteger(cents)` не true. Это всё ещё проблема? Если нет, отзови находку.С такой проверкой нет: formatPrice всегда получает целое число, так что находка неприменима, и я её отзываю.
Одного проверка не покрывает: отрицательные целые проходят Number.isInteger, и formatPrice(-500) возвращает "$-5.00". Если до этой функции могут доходить возвраты или скидки, возможно, вам нужно "-$5.00". Если не могут, менять нечего.
Модель, которой показали новые доказательства, должна либо отозвать находку, либо объяснить, почему она остаётся в силе с учётом этих доказательств. Полезно и то и другое. Модель, которая просто соглашается с каждым возражением, не делает ревью, поэтому оценивайте ответ по его доводам, а не по тому, согласился ли он. Код, который модель написала сама, ревьюйте в новом разговоре, чтобы ревью не читалось через то же рассуждение, которое этот код породило. Те же привычки (рамки, контекст и тесты) с самого начала делают код лучше; см. промпты для написания кода.
Часто задаваемые вопросы
Какой промпт хорош для код-ревью?
Назовите, что проверять (баги, безопасность, обработку ошибок), попросите указать серьёзность каждой находки и требуйте конкретный вход, который вызывает проблему, плюс дифф, который её исправляет. Скажите модели пропускать форматирование и именование, если об этом не просили. Дайте ей контекст, который был бы у ревьюера-человека: для чего этот код и что его вызывает.
Может ли ИИ заменить код-ревью человеком?
Нет, но это полезный первый проход. Он быстрый и хорошо ловит частые паттерны багов: необработанный None, пропущенные await, SQL, собранный из строк. Он не знает правил вашего продукта, остальной кодовой базы и причин, по которым было принято решение, и часть его находок окажется неверной. Используйте его перед ревью человеком, а не вместо него.
Почему ИИ в код-ревью сообщает о проблемах, которых нет?
Модель видит только вставленный вами код. Если значение проверяет вызывающий код или какой-то случай в вашей системе невозможен, модель этого знать не может и всё равно отмечает риск. Просьба дать конкретный вход, на котором код ломается, отсеивает многие такие находки: находка, для которой нет реалистичного входа, обычно ложное срабатывание.
Стоит ли просить ИИ переписать код во время ревью?
Просите маленькие диффы, по одному на находку, а не переписанный файл. Полное переписывание смешивает исправления с изменениями, о которых никто не просил, и ревьюить придётся ещё и само переписывание. Диффы позволяют принять или отклонить каждую находку отдельно.