Большинство разработчиков используют LLM наоборот. Мы описываем желаемое пятью словами, получаем 200 строк кода и следующий час тратим на отладку предположений, которые сделала модель.

Я три месяца работал по противоположному workflow: писал полное объяснение дизайна перед тем, как попросить хоть одну строчку кода. Результирующий код содержал меньше багов, но настоящее улучшение касалось моего собственного понимания.

Что такое literate programming с LLM?

Donald Knuth ввёл термин «literate programming» в 1984 году. Идея была проста: писать прозу, объясняющую программу, а затем вплетать код в эту прозу. Большинство разработчиков игнорировало это, потому что поддерживать два артефакта было утомительно. LLM меняют это уравнение. Проза больше не является документацией. Это prompt.

Когда вы объясняете свой подход LLM перед тем, как попросить код, вы делаете нечто отличное от стандартного prompt engineering. Вы не оптимизируете токены для модели. Вы заставляете себя чётко сформулировать constraints, edge cases и зависимости до того, как модель начнёт галлюцинировать архитектуру за вас. LLM становится rubber duck, которая отвечает, но только после того, как вы проделали настоящее мышление.

Эксперимент: 30 дней объяснений в первую очередь

Я выбрал 12 реальных задач из своего бэклога — от парсера CSV до стратегии переподключения WebSocket. Для каждой использовал два подхода:

  1. Краткий путь: вставить описание тикета в LLM и попросить код.
  2. Объяснение: написать от 300 до 500 слов, описывающих проблему, constraints, стратегию обработки ошибок и test cases. Затем попросить LLM реализовать этот дизайн.

Я оценивал вывод по трём критериям: баги, найденные на code review, время до merge и количество follow-up prompts, необходимых для исправления проблем.

Цифры были однозначны. Код с объяснением требовал в среднем 1,2 follow-up prompt по сравнению с 4,7 для краткого пути. Code review находил 0,3 бага на одно объяснение-первое решение против 2,1 бага для кода краткого пути.

Подвохом было время. Написание объяснения добавляло 15–20 минут к началу каждой задачи. Окупается ли это — зависит от того, что вы строите.

Как структурировать объяснение, которое действительно работает

Хорошее объяснение — это не просто более длинный prompt. Это design document с определённой структурой. Вот шаблон, который я отточил после первой недели:

## Problem
[What are we solving and why?]

## Constraints
[What can't we do? What formats, protocols, or dependencies must we respect?]

## Edge Cases
[What happens when input is null, empty, malformed, or hostile?]

## Error Handling Strategy
[Fail fast or fail graceful? What do we log? What do we return?]

## Test Cases
[The specific inputs and expected outputs that prove this works.]

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

Problem: Валидировать входящие подписи вебхуков. Некоторые отправители используют X-Signature: sha256=<hex>, а другие — X-Signature: <hex> без префикса. Нам нужно поддерживать оба варианта, не ломая существующие интеграции.

Constraints: Должен использовать модуль crypto Node.js. Должен использовать сравнение за постоянное время для предотвращения timing-атак. Должен возвращать чёткий boolean, никогда не выбрасывать исключение при malformed input.

Edge Cases: Заголовок отсутствует. Заголовок — пустая строка. Заголовок имеет неизвестный префикс вроде sha512=. Hex-строка нечётной длины или содержит не-hex символы.

Error Handling Strategy: Логировать конкретную причину сбоя на уровне debug, но возвращать false для всех невалидных случаев. Никогда не раскрывать детали подписи в сообщениях об ошибках.

Test Cases:

  1. Valid sha256=abc123... matches computed signature.
  2. Valid bare abc123... matches computed signature.
  3. Missing header returns false.
  4. Wrong signature returns false.
  5. Non-hex characters in signature return false.

Затем я добавил: “Implement this function in TypeScript.”

Модель вернул это:

import { createHmac, timingSafeEqual } from 'crypto';

export function validateWebhookSignature(
  payload: string,
  header: string | undefined,
  secret: string
): boolean {
  if (!header || header.length === 0) {
    return false;
  }

  const hexPattern = /^[0-9a-fA-F]+$/;
  let signatureHex: string;

  const prefixMatch = header.match(/^sha256=(.+)$/);
  if (prefixMatch) {
    signatureHex = prefixMatch[1];
  } else {
    signatureHex = header;
  }

  if (!hexPattern.test(signatureHex) || signatureHex.length % 2 !== 0) {
    return false;
  }

  const expected = createHmac('sha256', secret).update(payload).digest();
  const actual = Buffer.from(signatureHex, 'hex');

  if (actual.length !== expected.length) {
    return false;
  }

  return timingSafeEqual(actual, expected);
}

Это скомпилировалось с первой попытки. Важнее то, что оно обработало каждый edge case, который я указал, потому что я уже проделал всё мышление. Модели не нужно было угадывать, что я имел в виду под «validate».

Где это помогает, а где тратит время

Этот подход блистает для всего, что связано с branching logic, внешними зависимостями или аспектами безопасности. Валидатор вебхуков выше — идеальный пример. Модель не может знать вашу threat model, пока вы ей не скажете.

Но для boilerplate это излишне. Я не пишу 500 слов, чтобы сгенерировать React-компонент формы или стандартную миграцию базы данных. Для них подход краткого пути работает отлично, потому что паттерны хорошо представлены в training data, а stakes низки.

Настоящая ловушка — это средняя земля: задачи, которые кажутся простыми, но таковыми не являются. Парсер CSV кажется простым, пока вы не столкнётесь с полями в кавычках, содержащими запятые, или BOM-маркерами, или mixed line endings. Именно в этих задачах пропуск объяснения обходится вам дороже всего.

Workflow, который вы можете позаимствовать уже сегодня

Вам не нужно менять весь свой процесс. Попробуйте это для одной задачи на этой неделе:

  1. Откройте чат с LLM и сначала напишите объяснение. Пока не упоминайте код.
  2. Перечитайте своё объяснение. Если вы не можете чётко сформулировать edge cases — остановитесь. Вы ещё не готовы писать код, и уж точно не готовы поручать это LLM.
  3. Добавьте запрос на реализацию к тому же сообщению.
  4. Проверяйте вывод по своим собственным test cases, а не по интуиции.

Шаг, который большинство пропускает — это номер два. Если ваше объяснение расплывчато, LLM отразит эту расплывчатость обратно вам в виде тонких багов. Объяснение — это smoke test вашего собственного понимания.

Настоящая польза — не в коде

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

LLM по-прежнему делает typing. Но вы делаете thinking, а это всегда было bottleneck.

Если хотите попробовать сами — начните со следующего bug fix. Прежде чем просить помощи у LLM, напишите два абзаца, объясняющих, что, по вашему мнению, происходит и почему. Вы можете решить проблему ещё до того, как закончите писать.