Контекст, обмеження, формат виводу: шаблон промпту для надійного коду від ШІ
Розмитий промпт дає розмитий код. Шаблон із трьох частин — контекст, обмеження й формат виводу — допомагає отримати готові до продакшену функції з першої спроби.
Попросіть ШІ-асистента «написати функцію для перевірки введення користувача», і у відповідь часто отримаєте текст, схожий на код: він запускається, має сигнатуру й не робить нічого з того, що вам потрібно. Обробки помилок немає, і немає навіть натяку на те, що взагалі мало вважатися «коректним».
Зазвичай у такому результаті звинувачують модель, але здебільшого винен промпт. Почніть ставитися до промпту як до контракту API, а не як до випадкового прохання, і якість відповіді різко зросте, хоча модель розумнішою не стане. Просто їй більше не доведеться вгадувати. Нижче — структура з трьох частин, яка виправляє цю ситуацію.
Чому розмитий промпт дає розмитий код
Отримавши невиразне прохання, модель заповнює кожну прогалину припущенням, і ці припущення рідко збігаються з вашими. Яка версія мови? Як у вашій команді заведено обробляти помилки? Чи має код бути безпечним за конкурентного доступу? Що робити в разі збою — повернути помилку чи впасти з панікою? Прохання в один рядок не відповідає на жодне з цих питань, тож модель повертається до найпоширенішого патерну, який бачила. Для реальної кодової бази цей поширений патерн часто виявляється хибним, і розбіжність вилазить лише на рев'ю або, що гірше, у продакшені.
Чарівної фрази, яка це виправить, немає. Ліки — дати те, що новому інженерові розповідають у перший же день: контекст, обмеження й чітке визначення готовності.
Три розділи
Будь-який промпт для генерації коду розкладається на три частини саме в такому порядку.
- Контекст описує, де житиме код: мову та її версію, фреймворк, патерни, яким треба відповідати, і вже використовувані залежності.
- Обмеження перелічують правила, які не обговорюються: стиль обробки помилок, вимоги до продуктивності, безпеку за конкурентного доступу, перевірку вхідних даних і те, чого слід уникати (жодних зовнішніх бібліотек, жодного глобального стану тощо).
- Формат виводу каже, у якому вигляді має прийти відповідь. Лише функція чи функція плюс приклад використання? З коментарями чи без? З тестами?
Пропустіть одну частину — і щось піде не так. Без контексту код не впишеться у ваш стек. Без обмежень код «працює», але ігнорує ваші домовленості. Без формату виводу потрібні вам п'ятнадцять рядків опиняться загорнутими в три абзаци пояснень.
Одна задача, два промпти
Різницю найпростіше побачити на конкретному порівнянні, тож ось одна задача, сформульована двома способами.
Невиразна версія прохання виглядає так:
Напиши функцію для обробки пакета замовлень.
А тепер структурована версія:
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
Це не контрольований експеримент, а лише особистий журнал, але тенденція була стабільною. Десять секунд на обмеження обходяться дешевше, ніж десять хвилин налагодження коду, який мовчки припустив не те, — навіть для одноразових скриптів.
Дрібні звички, що окупаються
Вказуйте формат виводу. Не напишете щось на кшталт «лише код» — і відповідь щоразу приходитиме в обгортці з пояснень, які доведеться прогортати. За швидких ітерацій це зайве тертя.
Використовуйте заборонні обмеження. «Без сторонніх бібліотек» або «тут без рекурсії» відсікають цілу категорію відповідей, які інакше довелося б відхиляти й запитувати знову. Виключити заздалегідь краще, ніж виловлювати потім.
Нарощуйте обмеження в уточненнях. Якщо перша відповідь близька до потрібної, не починайте заново. Додайте ще одне обмеження й попросіть доопрацювати. «Та сама функція, але зроби ліміт горутин налаштовуваним» дасть більше, ніж новий промпт, бо модель уже має робочу версію як відправну точку.
Ніщо з цього не скасовує потреби читати отриманий код. Просто виправляти доведеться менше.