Головна / Статті / Контекст, обмеження, формат виводу: шаблон промпту для надійного коду від ШІ

Контекст, обмеження, формат виводу: шаблон промпту для надійного коду від ШІ

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

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

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

Дрібні звички, що окупаються

Вказуйте формат виводу. Не напишете щось на кшталт «лише код» — і відповідь щоразу приходитиме в обгортці з пояснень, які доведеться прогортати. За швидких ітерацій це зайве тертя.

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

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

Ніщо з цього не скасовує потреби читати отриманий код. Просто виправляти доведеться менше.