almdev Технический блог

Промпты на чтение кода: вопрос со ссылкой вместо «объясни»
Связный текст про архитектуру всегда связнее, чем сам проект.

Просьба описать архитектуру даёт документ, в котором пятая часть утверждений достроена из типичного проекта на этом стеке. Разбираю форму запроса, которая ломает механизм достройки: список вопросов, обязательное file:line и разрешение ответить «не найдено».

Подкатегория: Промпты

Чтение
6 мин
Технологии / версии
Промпты · Coding agent · PHP · Laravel
Серия
Цикл «Промпты» · часть 2 из 6
21 мая 2026 · 6 мин · 5 просмотров · AI
Список вопросов по коду, где каждый ответ помечен ссылкой на строку
Промпты 05/2026

Документ на полторы страницы, аккуратные разделы, верно названный стек — и три утверждения из пятнадцати выдуманы. Я получал такой результат столько раз, что перестал считать это дефектом инструмента. Это дефект вопроса.

Связность требует заполнить пробелы

Просьба «опиши архитектуру» — это заказ на связный текст. У связного текста есть свойство: в нём не бывает дыр. Абзац про аутентификацию не может закончиться словами «а дальше я не смотрел», потому что тогда это не описание.

Дыры при этом есть всегда. Файлов в среднем проекте больше, чем помещается в контекст, и часть из них прочитана не будет. Пробелы заполняются самым правдоподобным вариантом — тем, как устроен типичный проект на этом стеке. Для Symfony такого размера типичны JWT и RabbitMQ, поэтому в описании появляются JWT и RabbitMQ, даже если в проекте сессии и Redis.

Что делает эту достройку опасной: она неотличима от знания. Выдуманное утверждение написано тем же уверенным тоном, что и проверенное, стоит в том же ряду и выглядит так же правдоподобно. Отличить их можно только проверкой каждого, а проверять пятнадцать утверждений в прозе дороже, чем прочитать код самому.

Отсюда простое следствие. Не надо бороться с достройкой уговорами вроде «не выдумывай» и «будь точен». Надо менять форму ответа так, чтобы достраивать стало неудобно.

Вопрос вместо описания

Работает список пронумерованных вопросов, у каждого из которых есть точный ответ в коде.

Ответь на вопросы ниже. На каждое утверждение дай ссылку file:line.
Если ответа в коде нет — напиши «не найдено», не предполагай.

1. Где проверяется, что пользователь админ?
2. Где ставится nonce для Content-Security-Policy?
3. Что происходит при сохранении поста кроме записи в таблицу?
4. Какие скоупы есть у модели поста и чем они отличаются?
5. Откуда берётся контент постов при развёртывании на проде?

Разница с «опиши архитектуру» не в вежливости формулировки. Вопрос — это задача с проверяемым исходом. Описание — задача с исходом «получилось связно».

Ответы на эти пять вопросов для блога на Laravel выглядели так:

1. app/Http/Middleware/EnsureAdmin.php:13 — abort_unless на is_admin, 403.
2. app/Http/Middleware/SecurityHeaders.php:24 — random_bytes(16), затем
   Vite::useCspNonce и View::share('cspNonce').
3. app/Observers/PostObserver.php:15 — в saving пересчитывается
   reading_minutes, если изменился content_html; в saved сбрасывается
   кэш публичных списков.
4. app/Models/Post.php:74 released() — status + published_at;
   app/Models/Post.php:83 published() — released() плюс сортировка.
5. Не найдено однозначно: сидеры в database/seeders есть, но что именно
   запускается на проде, из кода репозитория не видно.

Четвёртый ответ мне понравился больше всех, потому что он поправил меня. В моём собственном файле контекста написано, что скоуп published() фильтрует по статусу и дате. Фильтрует released(), а published() добавляет сортировку. Мелочь, но именно из таких мелочей потом вырастает запрос без нужного условия.

Пятый ответ — честное «не найдено». Развёртывание живёт в scripts/update.sh, а вопрос был сформулирован про код приложения.

Что даёт file:line

Ссылка меняет не точность, а экономику проверки.

Утверждение без ссылки проверяется поиском: открыть проект, придумать, что грепать, пройтись по результатам, убедиться. Три-четыре минуты на утверждение, и это при условии, что я знаю проект. На пятнадцати утверждениях — час, которого у меня нет, поэтому я не проверяю, а верю. Так выдумка и попадает в документацию.

Утверждение со ссылкой проверяется взглядом. Открыл файл на нужной строке, увидел то, что обещано, — или не увидел. Пять секунд.

Есть и побочный эффект, который я не закладывал: требование ссылки меняет поведение до ответа. Чтобы дать file:line, файл нужно открыть. Агент, которому надо сослаться, читает больше и выдумывает меньше — не из добросовестности, а потому что сослаться на несуществующую строку сложнее, чем прочитать существующую.

Разрешение не знать

Строку «если ответа в коде нет — напиши „не найдено“» я долго считал вежливой формальностью. Она работает.

Без неё у ответа нет допустимого исхода «не нашёл». Задача сформулирована так, что пустой ответ равен провалу, а провал надо чем-то закрыть — правдоподобным предположением. С ней появляется второй легальный исход, и часть предположений превращается в честные пропуски.

Пропуски, кстати, полезнее ответов. «Не найдено, где проверяется владелец организации» на чужом проекте — это либо дыра в правах, либо неочевидное место, куда стоит посмотреть самому. И то и другое ценнее ещё одного абзаца про слои.

Пять групп вопросов

Набор вопросов важнее формулировки запроса. У меня сложились пять групп, которые дают максимум понимания за минимум чтения.

Точки входа. Контроллеры, консольные команды, обработчики очереди, подписчики событий, планировщик. Это скелет: всё остальное вызывается отсюда. Хороший вопрос — «перечисли маршруты из файла маршрутов и укажи, какой контроллер за каждым», плохой — «расскажи про контроллеры».

Данные. Самые большие таблицы, составные индексы, связи, мягкое удаление, enum-колонки. Схема рассказывает о продукте честнее кода: по таблице post_tag видно, что теги многие-ко-многим, независимо от того, что об этом думает сервисный слой.

Права. Где проверяется доступ и где не проверяется. Второе — отдельным вопросом, иначе не спросишь: «перечисли публичные маршруты, на которых нет проверки прав».

Асинхронное. Что уходит в очередь, что по расписанию, что делают наблюдатели моделей. Это то, чего не видно при чтении контроллеров, и то, что ломается тише всего. Пример с наблюдателем выше как раз оттуда: сохранение поста роняет кэш публичных списков, и по коду контроллера этого не узнать.

Странности. Что сделано не так, как принято на этом стеке. Формулируется хуже всех, отвечается тоже плохо, но сами попытки подсвечивают нестандартные места. В блоге ответом было «контент хранится как HTML в базе, а не в файлах, и печатается через очиститель» — ровно то место, где чужая правка вероятнее всего сломает вывод.

Чего спрашивать не стоит

Вопросы с ответом «да» получают ответ «да». «Правда ли, что права проверяются в политиках?» — плохой вопрос: он содержит гипотезу, и подтвердить её проще, чем опровергнуть. Спрашивайте «где проверяются права», без своей версии внутри.

Не стоит спрашивать про качество: «хорошо ли спроектирован модуль», «есть ли технический долг». Ответ будет длинным, вежливым и бесполезным. Технический долг ищется отдельной задачей с конкретной гипотезой, а не в разведке.

И не надо задавать сорок вопросов сразу. У меня рабочий диапазон — десять-пятнадцать за проход. Дальше ответы мельчают: на последние вопросы приходятся остатки внимания и остатки контекста.

Результат живёт в файле, а не в переписке

Разведка, которая осталась в чате, бесполезна через неделю. Контекст выгружен, сессия закрыта, всё начинается заново.

Проверенные ответы я переношу руками в AGENTS.md — не копированием вывода, а пересказом того, что подтвердилось. Из пятнадцати ответов до файла доезжает пять-семь строк.

## Как устроено

- Граница /admin — middleware EnsureAdmin, проверка is_admin.
- CSP: nonce генерируется в SecurityHeaders, шаблоны берут его из cspNonce.
- Пост при сохранении пересчитывает reading_minutes и сбрасывает кэш списков.
- Скоупы поста: released() фильтрует, published() фильтрует и сортирует.

## Чего здесь нет

- Ролевой модели. Есть один флаг is_admin.
- API наружу. Только веб-маршруты.
- Хранения статей в файлах. Контент — HTML в базе.

Раздел «чего здесь нет» бьёт ровно по механизму достройки. Он говорит, чего в проекте нет вопреки типичному, и следующая сессия не начинает с гипотезы про ролевую модель.

Что осталось

Способ переносится на любой стек и почти не требует настройки: пронумерованный список, обязательное file:line, разрешение не знать.

Чего я так и не решил — как проверять сами вопросы. Набор из пяти групп у меня подобран опытом, но я подозреваю, что он подогнан под мои проекты: монолиты на PHP с одной базой и очередью. На чужой архитектуре часть вопросов промахнётся мимо интересного, и понять это можно будет только по бесполезности ответов.

Серия

Цикл «Промпты»

#prompting #coding-agent #php #laravel #kontekst