Главная / Статьи / Контекст, ограничения, формат вывода: шаблон промпта для надёжного кода от ИИ

Контекст, ограничения, формат вывода: шаблон промпта для надёжного кода от ИИ

Размытый промпт даёт размытый код. Шаблон из трёх частей — контекст, ограничения и формат вывода — помогает получить готовые к продакшену функции с первой попытки.

1086 слов

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

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

Почему размытый промпт даёт размытый код

Получив расплывчатую просьбу, модель заполняет каждый пробел допущением, и эти допущения редко совпадают с вашими. Какая версия языка? Как в вашей команде принято обрабатывать ошибки? Должен ли код быть безопасен при конкурентном доступе? Что делать при сбое — вернуть ошибку или упасть с паникой? Просьба в одну строку ни на один из этих вопросов не отвечает, поэтому модель откатывается к самому распространённому паттерну, который видела. Для реальной кодовой базы этот распространённый паттерн часто оказывается неправильным, и расхождение вылезает только на ревью или, что хуже, в продакшене.

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

Три раздела

Любой промпт для генерации кода раскладывается на три части именно в таком порядке.

  1. Контекст описывает, где будет жить код: язык и его версию, фреймворк, паттерны, которым нужно соответствовать, и уже используемые зависимости.
  2. Ограничения перечисляют правила, которые не обсуждаются: стиль обработки ошибок, требования к производительности, безопасность при конкурентном доступе, проверку входных данных и то, чего следует избегать (никаких внешних библиотек, никакого глобального состояния и так далее).
  3. Формат вывода говорит, в каком виде должен прийти ответ. Только функция или функция плюс пример использования? С комментариями или без? С тестами?

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

Одна задача, два промпта

Разницу проще всего увидеть на конкретном сравнении, поэтому вот одна задача, сформулированная двумя способами.

Расплывчатая версия просьбы выглядит так:

Напиши функцию для обработки пакета заказов.

А теперь структурированная версия:

Context: Go 1.22 service, part of an order-processing microservice.
Uses the existing Order struct (ID string, Amount float64, Status string).
Errors are wrapped with fmt.Errorf and %w, never panicked.

Constraints:
- Process orders concurrently, max 10 goroutines at a time
- If any single order fails, log it and continue (don't abort the batch)
- Return a slice of errors for failed orders, not a single error
- No third-party libraries, standard library only

Output format: just the function and a short usage example, no explanation.

С первой попытки получилось вот что:

func ProcessOrders(ctx context.Context, orders []Order, process func(Order) error) []error {
    sem := make(chan struct{}, 10)
    var wg sync.WaitGroup
    var mu sync.Mutex
    var errs []error

    for _, o := range orders {
        wg.Add(1)
        sem <- struct{}{}
        go func(o Order) {
            defer wg.Done()
            defer func() { <-sem }()

            if err := process(o); err != nil {
                mu.Lock()
                errs = append(errs, fmt.Errorf("order %s: %w", o.ID, err))
                mu.Unlock()
            }
        }(o)
    }

    wg.Wait()
    return errs
}

// usage
errs := ProcessOrders(ctx, orders, chargeOrder)
for _, e := range errs {
    log.Println(e)
}

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

Работает с любым языком

Шаблон не привязан к одному языку. Вот те же три раздела на другом стеке:

Context: Python 3.11 data pipeline script, uses pandas, runs as a scheduled job.

Constraints:
- Validate that required columns exist before processing, raise ValueError with column name if missing
- Handle empty DataFrames without crashing (return empty result, log a warning)
- No mutation of the input DataFrame

Output format: function only, type hints included, no docstring needed.

И результат:

import pandas as pd
import logging

def clean_transactions(df: pd.DataFrame, required: list[str]) -> pd.DataFrame:
    missing = [col for col in required if col not in df.columns]
    if missing:
        raise ValueError(f"missing required column(s): {', '.join(missing)}")

    if df.empty:
        logging.warning("received empty DataFrame, returning empty result")
        return df.copy()

    result = df.copy()
    result = result.dropna(subset=required)
    return result

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

А структура правда помогает?

Неформальный подсчёт по паре десятков промптов — как часто сгенерированную функцию приходилось переделывать перед слиянием — дал примерно такие цифры:

Vague prompts        |################........| ~80% needed rework
Context only          |##############..........| ~55% needed rework
Context + constraints  |######...................| ~25% needed rework
All three sections     |##.......................| ~10% needed rework

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

Мелкие привычки, которые окупаются

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

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

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

Ничто из этого не отменяет необходимости читать полученный код. Просто исправлять придётся меньше.