EasyТеория3 min

Что изменилось: от детерминированного к недетерминированному

Фундаментальный сдвиг в разработке ПО при использовании AI-ассистентов

Мир до AI-ассистентов: детерминизм

Традиционная разработка программного обеспечения -- это мир детерминизма. Один и тот же исходный код, прошедший через один и тот же компилятор с одними и теми же настройками, всегда порождает один и тот же результат. Линтер, получив одинаковый файл, всегда выдаст одинаковые предупреждения. Тесты при одинаковом состоянии дают одинаковый результат.

Детерминированный мир:

   Исходный код → Компилятор → Бинарный файл
   (всегда один)   (правила)    (всегда тот же)

   Код + Линтер → Предупреждения
   (те же)         (те же)

   Код + Тесты → Результат
   (те же)        (тот же: pass/fail)

Свойства детерминированной разработки

Свойство Описание Пример
Воспроизводимость Результат идентичен при повторном запуске gcc main.c дает одинаковый бинарник
Предсказуемость Можно предсказать результат до запуска Синтаксическая ошибка всегда будет найдена
Верифицируемость Легко проверить корректность Тесты либо проходят, либо нет
Отладка Ошибку можно воспроизвести и изолировать Breakpoint всегда срабатывает одинаково

Инженеры десятилетиями строили инструменты и процессы вокруг этих свойств. CI/CD-пайплайны рассчитывают на воспроизводимость. Code review рассчитывает на предсказуемость. Тестовые фреймворки рассчитывают на верифицируемость. Отладчики рассчитывают на воспроизводимость багов.

Инструменты детерминированного мира

# Compiler -- deterministic transformation
gcc -O2 -o app main.c      # Always the same binary

# Linter -- deterministic analysis
phpstan analyse src/         # Always the same warnings

# Formatter -- deterministic transformation
prettier --write src/        # Always the same formatting

# Tests -- deterministic verification
phpunit --testsuite=unit     # Same pass/fail results

Все эти инструменты объединяет одно: им можно доверять на уровне процесса. Если линтер сказал "ошибок нет" -- значит, ошибок (которые он знает) действительно нет. Если тесты прошли -- значит, проверяемое поведение работает.


Появление AI-ассистентов: недетерминизм

AI-ассистенты для разработки (GitHub Copilot, Claude Code, Cursor, Windsurf) работают принципиально иначе. Это вероятностные модели -- они не применяют жесткие правила, а генерируют наиболее вероятный вывод на основе статистических паттернов.

Недетерминированный мир:

   Промпт + Контекст → LLM → Код (вариант A)
   (тот же)           (модель) ↘ Код (вариант B)
                               ↘ Код (вариант C)

   Один и тот же запрос может породить разный код
   при каждом вызове.

Что делает AI-ассистент

Когда вы просите AI написать функцию, происходит следующее:

  1. Токенизация -- ваш промпт разбивается на токены
  2. Обработка контекста -- модель "видит" ваш промпт, контекст файлов, историю чата
  3. Генерация -- модель последовательно выбирает наиболее вероятные следующие токены
  4. Выбор -- на каждом шаге есть параметр temperature, влияющий на "креативность" выбора

Ключевое отличие: на шаге 4 модель выбирает из распределения вероятностей, а не применяет правила. Это означает:

Один и тот же промпт может породить разный код при каждом вызове.

Практический пример

Допустим, вы просите AI: "Напиши функцию валидации email на PHP".

При первом вызове:

<?php

declare(strict_types=1);

// Attempt 1: using filter_var
function validateEmail(string $email): bool
{
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

При втором вызове с тем же промптом:

<?php

declare(strict_types=1);

// Attempt 2: using regex
function validateEmail(string $email): bool
{
    $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
    return preg_match($pattern, $email) === 1;
}

При третьем:

<?php

declare(strict_types=1);

// Attempt 3: comprehensive validation
final class EmailValidator
{
    public function validate(string $email): bool
    {
        if (empty($email) || strlen($email) > 254) {
            return false;
        }

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return false;
        }

        // Check DNS MX record
        $domain = substr($email, strrpos($email, '@') + 1);
        return checkdnsrr($domain, 'MX');
    }
}

Все три варианта -- валидные ответы на один и тот же вопрос. Но у каждого разные свойства: производительность, точность, обработка крайних случаев, архитектурный стиль. AI не скажет вам, какой вариант подходит для вашей конкретной ситуации.


Фундаментальное напряжение

AI-ассистенты создают фундаментальное напряжение в инженерной практике:

┌─────────────────────────────────────────────────────────────┐
│                 ФУНДАМЕНТАЛЬНОЕ НАПРЯЖЕНИЕ                  │
│                                                             │
│   СКОРОСТЬ                        НАДЕЖНОСТЬ                │
│   ┌──────────┐                    ┌──────────┐              │
│   │ AI может │                    │ AI может │              │
│   │ написать │                    │ написать │              │
│   │ код за   │        VS          │ код с    │              │
│   │ секунды  │                    │ багами,  │              │
│   │          │                    │ дырами,  │              │
│   │          │                    │ долгом   │              │
│   └──────────┘                    └──────────┘              │
│                                                             │
│   "Быстро и полезно"      "Уверенно и неправильно"          │
└─────────────────────────────────────────────────────────────┘

AI "уверенно неправильный"

Одна из самых опасных особенностей AI-ассистентов -- они не сигнализируют о своей неуверенности. Компилятор скажет "ошибка в строке 42". Линтер предупредит о потенциальной проблеме. AI-ассистент напишет код, который выглядит правильно, читается уверенно, но может содержать серьезные ошибки.

Примеры типичных проблем:

Категория Пример "уверенной ошибки" AI
Безопасность SQL-запрос без параметризации, потому что в обучающих данных много примеров с конкатенацией строк
Производительность N+1 запрос в цикле, потому что "это самый очевидный способ"
Крайние случаи Отсутствие проверки на null, пустой массив, слишком длинную строку
Совместимость Использование API, которое было deprecated или изменено в новой версии
Бизнес-логика Математически правильный код, который не учитывает бизнес-правило

Ключевой инсайт: AI не знает, чего он не знает. Он не скажет "я не уверен в этом решении" или "здесь может быть проблема с производительностью при 10K записей". Он просто напишет код.


Почему это важно для инженеров

1. Хрупкий код (Fragile Code)

AI-генерированный код часто работает "на первый взгляд", но ломается при нестандартных входных данных:

<?php

declare(strict_types=1);

// AI-generated: looks correct, fragile in practice
function parseUserAge(string $input): int
{
    // AI didn't consider: negative numbers, overflow, non-numeric strings,
    // leading/trailing whitespace, empty strings, "NaN", "Infinity"
    return (int) $input;
}

// Production-ready version: explicit handling
function parseUserAgeSafe(string $input): int
{
    $trimmed = trim($input);

    if ($trimmed === '') {
        throw new \InvalidArgumentException('Age cannot be empty');
    }

    if (!ctype_digit($trimmed)) {
        throw new \InvalidArgumentException(
            sprintf('Age must be a positive integer, got: "%s"', $input)
        );
    }

    $age = (int) $trimmed;

    if ($age < 0 || $age > 150) {
        throw new \InvalidArgumentException(
            sprintf('Age must be between 0 and 150, got: %d', $age)
        );
    }

    return $age;
}

2. Безопасность (Security Holes)

AI обучался на огромном количестве кода, включая небезопасный. Он может воспроизвести уязвимые паттерны:

// AI-generated: vulnerable to path traversal
func serveFile(w http.ResponseWriter, r *http.Request) {
    filename := r.URL.Query().Get("file")
    // Attacker can send: ?file=../../../etc/passwd
    data, err := os.ReadFile("/uploads/" + filename)
    if err != nil {
        http.Error(w, "Not found", 404)
        return
    }
    w.Write(data)
}

// Production-ready: sanitized path
func serveFileSafe(w http.ResponseWriter, r *http.Request) {
    filename := r.URL.Query().Get("file")
    // Clean the path to prevent traversal
    cleaned := filepath.Clean(filename)
    // Ensure it doesn't escape the uploads directory
    if strings.Contains(cleaned, "..") {
        http.Error(w, "Forbidden", 403)
        return
    }
    fullPath := filepath.Join("/uploads", cleaned)
    // Verify the resolved path is still under /uploads
    if !strings.HasPrefix(fullPath, "/uploads/") {
        http.Error(w, "Forbidden", 403)
        return
    }
    data, err := os.ReadFile(fullPath)
    if err != nil {
        http.Error(w, "Not found", 404)
        return
    }
    w.Write(data)
}

3. Перегрузка Code Review

Когда AI генерирует код в 10 раз быстрее, объем кода на review возрастает пропорционально. Но человеческая способность к review -- конечный ресурс:

Без AI:
  Разработчик пишет ~200 строк/день → Review ~200 строк → Управляемо

С AI:
  Разработчик генерирует ~2000 строк/день → Review ~2000 строк → Перегрузка

  Результат: "Выглядит нормально, approve" → Баги в production

4. Технический долг

AI-генерированный код, принятый без должной проверки, создает технический долг, который растет быстрее обычного:

  • Дублирование логики (AI не видит, что похожая функция уже существует)
  • Несогласованные паттерны (каждый промпт может породить свой стиль)
  • Отсутствие документации (AI пишет код, но не объясняет "почему")
  • Устаревшие зависимости (AI обучался на данных определенного периода)

Что меняется в инженерных практиках

Переход к недетерминированным инструментам требует пересмотра ключевых практик:

Тестирование

Раньше: тесты проверяют, что ваш код работает
Теперь: тесты проверяют, что AI-генерированный код работает
        И что он продолжает работать после рефакторинга AI

Значение тестов: ↑↑↑ (выросло многократно)

Тесты становятся не просто проверкой, а контрактом между вашими требованиями и AI-генерированным кодом. Без тестов вы не можете знать, работает ли код корректно, даже если он "выглядит правильно".

Отладка

Раньше: вы написали код → вы знаете, как он работает → вы знаете, где искать баг
Теперь: AI написал код → вы должны понять, как он работает → потом искать баг

Сложность отладки: ↑↑ (выросла)

Code Review

Раньше: проверяете код коллеги (знаете его стиль, контекст)
Теперь: проверяете код AI (может быть любым, непредсказуемый стиль)

Объем review: ↑↑↑ (AI генерирует быстро, много кода)
Нужна новая стратегия: фокус на контрактах, интерфейсах, безопасности

Архитектура

Раньше: архитектура определяет структуру кода
Теперь: архитектура ПЛЮС правила для AI определяют структуру кода
         (CLAUDE.md, .cursorrules, AGENTS.md)

Значение архитектурных решений: ↑↑ (AI без контекста нарушит архитектуру)

Тезис Энрико Папалини

Энрико Папалини в книге "AI-Assisted Software Engineering" формулирует центральный тезис:

Инженерная дисциплина при работе с AI становится БОЛЕЕ важной, а не менее.

Это контринтуитивно. Кажется, что AI упрощает разработку и нужно меньше дисциплины. На самом деле:

Без AI С AI
Пишешь код сам -- контролируешь каждую строку AI пишет код -- нужно проверять каждую строку
Скорость ограничена -- меньше кода -- меньше ошибок Скорость огромная -- больше кода -- больше потенциальных ошибок
Детерминированные инструменты -- предсказуемый результат Недетерминированные инструменты -- нужна верификация
Стандартные процессы достаточны Нужны новые процессы и guardrails

Guardrails -- ограждения для AI

Книга предлагает подход "embrace non-determinism with proper guardrails" -- принять недетерминизм, но установить ограждения:

┌────────────────────────────────────────────────────┐
│                  GUARDRAILS                         │
│                                                    │
│  ┌──────────────────────────────────────────────┐  │
│  │          AI-генерированный код               │  │
│  │                                              │  │
│  │  Быстро, продуктивно, но непредсказуемо     │  │
│  └──────────────────────────────────────────────┘  │
│                                                    │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐           │
│  │Авто-тесты│ │ Линтеры  │ │ Security │           │
│  │          │ │          │ │  Scan    │           │
│  └──────────┘ └──────────┘ └──────────┘           │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐           │
│  │Code      │ │ Type     │ │ Perf     │           │
│  │Review    │ │ Check    │ │ Bench    │           │
│  └──────────┘ └──────────┘ └──────────┘           │
│                                                    │
│  Guardrails = автоматические проверки, которые     │
│  ловят ошибки AI до того, как они попадут в prod  │
└────────────────────────────────────────────────────┘

Ключевые выводы

  1. AI-ассистенты -- недетерминированные инструменты. Один и тот же промпт может породить разный код. Это фундаментально отличает их от компиляторов, линтеров и тестов.

  2. AI "уверенно неправильный". Он не сигнализирует о неуверенности, не предупреждает о крайних случаях, не учитывает ваш контекст автоматически.

  3. Скорость генерации -- палка о двух концах. Больше кода за единицу времени означает больше потенциальных ошибок за единицу времени.

  4. Инженерная дисциплина важнее, чем когда-либо. Тесты, review, security -- все становится критически важным, когда значительная часть кода генерируется вероятностной моделью.

  5. Нужны новые процессы. Традиционные CI/CD, code review, тестирование должны быть адаптированы для мира AI-генерированного кода.

Главный тезис: AI-ассистенты -- это мощные инструменты, которые требуют новой инженерной дисциплины. Не меньше дисциплины, а БОЛЬШЕ. Не слепое доверие, а верификация. Не отказ от процессов, а их эволюция.