Промпт-инжиниринг для веб-мастера: как получать предсказуемый результат от LLM

Большинство статей про промпты заканчивается на «будь вежливым и добавь «think step by step»». На практике предсказуемость ответа языковой модели зависит не от волшебных слов, а от структуры запроса и формата вывода — ровно так же, как предсказуемость API зависит от схемы, а не от того, как вы назвали переменную. Здесь разберём то, что реально двигает результат: анатомию промпта, разделение системной роли и пользовательских данных, формат вывода, few-shot и guard-формулировки против галлюцинаций.

Все примеры взяты из реальных системных промптов рабочих инструментов — не из учебных заготовок. Это не «магия промптов», а инженерия ввода: та же дисциплина, что в валидации форм и контрактах API, перенесённая на общение с моделью.

Часть I. Анатомия промпта: пять слоёв предсказуемого ввода

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

СлойВопрос, на который отвечаетЧто ломается без него
Роль«Кто отвечает?»Модель выбирает усреднённый стиль, не ваш домен
Задача«Что сделать?»Расплывчатый, пространный ответ
Контекст«По каким данным?»Додумывание вместо фактов (→ галлюцинации)
Формат вывода«В какой форме вернуть?»Невозможно разобрать ответ программно
Ограничения«Чего не делать?»Секреты в выводе, markdown-обёртка там, где нужен голый текст

Возьмём реальную строку из генератора SEO-метатегов — она собрана ровно по этим пяти слоям (роль → задача → контекст-переменные → формат → ограничение):

Ты — SEO-эксперт. Проанализируй текст статьи и сгенерируй SEO-метатеги.
{движок}{язык}{бренд}{ключевые слова}

Верни результат СТРОГО в формате JSON:
{
  "title": "title до 60 символов",
  "description": "meta description до 160 символов",
  "slug": "url-slug транслитом",
  "keywords": "5-7 ключевых слов через запятую"
}

Только JSON, без markdown и лишнего текста.

Здесь наглядно всё:

  • роль — «Ты — SEO-эксперт» (сужает стиль до предметной области);
  • задача — «проанализируй и сгенерируй метатеги»;
  • контекст — четыре переменные (движок/язык/бренд/ключевые слова), подставляемые до отправки;
  • формат — конкретный JSON со схемой и лимитами прямо в значениях-плейсхолдерах («до 60 символов»);
  • ограничение — «Только JSON, без markdown и лишнего текста».

Мнемоника для границы между слоями: роль говорит «откуда», формат говорит «куда», контекст — «из чего». Веб-мастер чаще всего пропускает формат и ограничения — и получает красивый, но непригодный для автоматической обработки текст.

Часть II. system и user — это не украшение, а граница доверия

Любой вызов модели несёт сообщения с полем role. Два основных роли различаются не вежливостью, а семантикой доверия:

$messages = array(
    array( 'role' => 'system', 'content' => $system_prompt ),          // правила игры
    array( 'role' => 'user',   'content' => "Проанализируй текст:\n\n" . $text ), // данные
);
  • system — инструкция, которую модель трактует как правило поведения. Она задаёт роль, формат и ограничения. Пользователь её не «перебивает» casually: то, что попало в system, держится до конца диалога.
  • user — содержимое для обработки. Это данные: текст статьи, код-фрагмент, описание товара.

Почему это важно именно веб-мастеру: если вы положите данные пользователя в системную роль, любая реплика из формы на сайте станет для модели командой. Это и есть prompt injection — и его полный разбор оставим статье про AI-безопасность (№6), но границы закладываются здесь: правила — в system, недоверенные данные — в user.

Практический приём из рабочих инструментов: пользовательский текст вставляется в user после жёсткого заголовка-маркера, чтобы модель отделяла инструкцию от payload:

Проанализируй текст и сгенерируй метатеги:

<текст из формы пользователя>

Сам заголовок («Проанализируй текст и…») — часть приложения к user-сообщению в коде, а не в system: так инструкция «что сделать» не конфликтует с правилом «как себя вести», а данные ясно ограничены снизу.

Часть III. Формат вывода важнее «волшебных слов»

Главная причина непредсказуемых ответов — не «плохая модель», а размытый формат. «Подробно опиши метатеги» даёт прозу, которую нельзя парсить. «Верни СТРОГО JSON с полями…» даёт то, что подставляется прямо в код.

Три рабочих приёма:

1. Задайте закрытый формат и назовите поля с лимитами. Схема в самом промпте — одновременно и инструкция, и документация:

Верни результат СТРОГО в формате JSON:
{ "title": "до 60 символов", "description": "до 160 символов", "slug": "транслитом" }

Значения-плейсхолдеры внутри JSON несут ограничения — модель удерживает лимит, не выдумывая отдельное правило.

2. Запретите обёртку явно. Модель по привычке оборачивает вывод в markdown ... — и ваш парсер падает. Отсюда ограничение «Только JSON, без markdown и лишнего текста». В генераторе конфигов это же правило сформулировано для файлов:

Пиши только содержимое файла, без пояснений вне него и без markdown-обёртки ```.

3. Читайте ответ как данные, а не как разметку. Ответ модели — недоверенный ввод. Его текст идёт в textContent/esc_html, а не как готовый HTML. Это ровно та же XSS-гигиена, что для формы: вывод модели — не исключение. (Связка с №6 и со статьёй про XSS/CSRF.)

Однострочный вывод этого раздела: инвестируйте в описание формата, а не в «умные» прилагательные. «Предоставь исчерпывающий, но лаконичный SEO-анализ» хуже, чем точная схема полей.

Часть IV. Few-shot: один пример стоит абзаца инструкций

Few-shot — это показать модели один-два прецедента «вход → желаемый выход» прямо в промпте. Работает надёжнее текстового описания формата, потому что модель имитирует образец, а не трактует правило.

Пример для генератора метатегов (схема → один прогнанный случай как эталон формы):

Пример.
Вход: статья «Как настроить PHP-FPM под WordPress».
Выход:
{"title":"PHP-FPM под WordPress: движок и пул","description":"Как PHP-FPM обслуживает WordPress: базовый php.ini, пул www.conf, Opcache. Где задать лимиты и почему настройка не подействовала.","slug":"php-fpm-pod-wordpress"}

Теперь обработай вход ниже в том же формате.

Что few-shot фиксирует лучше инструкции:

  • форму полей (какие ключи, в каком порядке, JSON-кавычки);
  • регистр и стиль значений (транслит в slug, а не кириллица — видно на примере);
  • длину «в натуре» (модель видит реальную длину title, а не «до 60» абстрактно).

Правило экономии: 1–2 примера, а не десять. Каждый пример — это токены на входе (а значит деньги и квота); при закрытом формате два примера покрывают вариативность, десятый почти ничего не добавляет. Пример уместен, когда формат сложнее словесного описания (вложенный JSON, конкретный DSL); для «верни JSON с тремя полями» достаточно самой схемы из Части III.

Рядом лежит приём из инструментов-конвертеров: «текст → Markdown → HTML → чистый текст» проще показать одним прецедентом на входе/выходе, чем описывать правила обработки. Тот же regex-инструмент внутри промпта опирается на формат «ответ на русском, regex в блоке кода» — и даёт нумерованный разбор по частям. Связка с Регулярные выражения на практике.

Часть V. Guard-формулировки: замок против галлюцинаций

Галлюцинация модели — это не «врал», а правдоподобно достроил то, чего во входе не было. Для веб-мастера это дорого: выдуманный CSS-класс, несуществующая константа wp-config, директива php.ini, которую ядро проигнорирует.

Два guard-приёма из реальных системных промптов.

1. Ограничить область утверждений — «ИСКЛЮЧИТЕЛЬНО показанный фрагмент». Реальная формулировка объяснителя кода:

Ты — терпеливый технический объяснитель кода. Объясняй ИСКЛЮЧИТЕЛЬНО показанный
фрагмент, а не вымышленный контекст и не функции, которых во входе нет.
Если фрагмент неполный или обрезан и без внешнего контекста что-то нельзя
утверждать уверенно — явно скажи, какого контекста не хватает, а не додумывай.
Пиши по-русски. Не оборачивай весь ответ в markdown-блок ```; используй обычный текст.

Здесь три разных замка в одной связке: запрет вымышленного контекста → разрешение признать нехватку данных («скажи, какого контекста не хватает») → формат ответа. Ключевое — второе: модели нужно легальное право сказать «не знаю», иначе она заполнит дыру выдумкой. Это и есть дешёвая страховка от галлюцинаций.

2. Отсечь опасную свободу вывода. В генераторе конфигов guard про секреты — он запрещает модели то, чего та в принципе знать не может:

ГЛАВНОЕ ПРАВИЛО БЕЗОПАСНОСТИ: никогда не подставляй реальные секреты (пароли,
ключи API, соли, токены) — только явные плейсхолдеры вида REPLACE_ME или
CHANGE_ME_WITH_REAL_VALUE.
Не выдумывай несуществующих директив, констант и переменных — только реально
существующие в выбранном формате.

Два правила, два класса ошибок: «значение похоже на настоящий пароль» (его зальют в прод и не поменяют) и «константа, которую ядро не знает» (настройка молча не применится). Оба — прямое следствие того, что модель не знает ваших секретов и вашего формата лучше справочника, и guard фиксирует это как запрет, а не как надежду. Подробный разбор «почему секрет — всегда плейсхолдер» — в Три конфига WordPress.

Сводка Части V: хороший guard отвечает «чего не делать» и даёт легальный выход «я не знаю», вместо молчаливой правдоподобной выдумки.

Часть VI. Предсказуемость = проверяемость: валидация ответа кодом

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

Минимальный цикл на PHP (та же дисциплина, что в [ai_seo_meta_example]):

PHP
<?php
// $raw — строка ответа модели. Не доверяем, что там "только JSON".
$json = json_decode( $raw, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
    // модель вернула мусор или обёртку — повтор/откат, а не показ пользователю
    error_log( 'bad llm json: ' . substr( $raw, 0, 120 ) );
    return null;
}

// 1. поля на месте
$title = $json['title'] ?? '';
$desc  = $json['description'] ?? '';

// 2. лимиты соблюдены (те самые "до 60/160", что были в промпте)
if ( mb_strlen( $title ) > 60 || mb_strlen( $desc ) > 160 ) {
    return null; // не прошло валидацию — генерируем заново, а не отдаём гостю
}

// 3. недоверенный текст выводим экранированным
echo esc_html( $title );

Три проверяемых свойства — зеркало слоёв из Части III:

  1. Это вообще валидные данные (JSON-парсится) — иначе ответ бесполезен программно;
  2. Ограничения соблюдены (длины, enum, regex-форма slug) — промпт попросил, код требует;
  3. Вывод экранирован (esc_html) — модель не защищена от генерации markup/скриптов лучше человека.

Здесь же про «температуру» и детерминизм: параметр температуры (temperature) управляет случайностью выбора токенов. Для задач с единственным правильным форматом (JSON, конфиг, slug) температуру опускают — так ответ стабильнее; для творческого текста её поднимают. Это не «сделать модель умнее», а сузить распределение, чтобы валидатор из Части VI реже откатывался.

Экономику предсказуемости нельзя игнорировать: повторная генерация при провале валидации стоит токенов, поэтому лимиты ввода/вывода и квота на IP — часть дизайна (разбор rate-limiting и квот — в Защита API). Тот же подход — «не верь на слово, проверь тестом» — лежит в основе Юнит-тестов на практике; вывод модели — просто ещё один внешний источник, который надо покрыть проверкой.

Инструменты под задачу

Всё выше — не теория «про промпты», а механика, на которой собраны рабочие инструменты codestack.space. Их системные промпты — живые образцы пяти слоёв из Части I, guard-формулировок из Части V и закрытого формата из Части III:

  • закрытый JSON-формат с лимитами прямо в схеме — генератор SEO-метатегов (title до 60, description до 160, «Только JSON, без markdown»);
  • guard «ИСКЛЮЧИТЕЛЬНО показанный фрагмент… не додумывай» — объяснитель кода;
  • «никогда не подставляй реальные секреты — только REPLACE_ME» — генератор конфигов;
  • детерминизм + валидация вывода — общая схема для всех инструментов серии.

Поговорить с моделью вживую, проверить структуру промпта на реальном запросе и увидеть, как меняется вывод при разных форматах, — можно в песочнице чата:

ИИ-помощник по коду
Гость
Токены: — / 50 000
Гостевой лимит — 50 000 токенов. Зарегистрируйтесь для лимита 200 000 токенов.

👋 Привет! Я AI-помощник на базе Qwen 3 Coder.

Задавайте вопросы, вставляйте код через кнопку «Код» — я проверю, объясню или перепишу.

Enter — отправить · Shift+Enter — новая строка

Чат позволяет задать системную роль и формат вывода прямо в запросе и сразу увидеть разницу между «подробно опиши» и «верни JSON с полями…» — то же, что разбирается в частях III–IV, но руками.

Чек-лист граблей

  • Не задали формат вывода → модель вернула прозу, парсер не может её разобрать; опишите поля и тип.
  • Положили данные пользователя в system → реплика из формы стала командой модели (путь к prompt injection); правила — в system, данные — в user.
  • Забыли запретить markdown-обёртку → ответ приходит в json , и json_decode падает; просите «только JSON, без обёртки».
  • Описали формат прилагательными, а не схемой → «лаконичный, но подробный» непредсказуем; дайте закрытую схему с лимитами.
  • Не дали модели право сказать «не знаю» → она заполнит дыру выдумкой; добавьте «если контекста не хватает — скажи, а не додумывай».
  • Ставите few-shot ради few-shot в 10 примеров → лишние токены и деньги на входе; для закрытого формата хватит 1–2.
  • Доверились формату без валидации → «до 60 символов» в промпте не гарантирует 60 в ответе; проверяйте длину/schema парсером.
  • Вывели ответ модели как HTML → модель так же генерирует markup, как и пользователь вводит; вывод — через esc_html/textContent.
  • Не занизили температуру для задачи с единственным ответом → slug/JSON «плывёт» от прогона к прогону; детерминизм снижает откаты валидатора.
  • Не учли стоимость ретрая → каждая невалидная генерация жрёт токены; задайте лимиты ввода/вывода и квоту.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.