MidТеория8 min

Проблема 70%: почему AI-код не дотягивает

Почему AI генерирует 70% решения и как закрыть оставшиеся 30%

Суть проблемы

AI-инструменты могут сгенерировать примерно 70% рабочего решения за считанные минуты. Код компилируется, базовые сценарии работают, визуально все выглядит готовым. Но оставшиеся 30% -- обработка крайних случаев, безопасность, производительность под нагрузкой, соответствие бизнес-правилам -- это именно то, что отличает прототип от production-ready кода.

┌───────────────────────────────────────────────────────────────┐
│                    СТРУКТУРА РЕШЕНИЯ                          │
│                                                               │
│  ████████████████████████████████████████████░░░░░░░░░░░░░░░  │
│  ◄──────── 70% AI генерирует ────────►◄─── 30% инженер ───►  │
│                                                               │
│  70% = Happy path:                    30% = Production:       │
│  • Базовая логика                     • Edge cases            │
│  • Стандартные CRUD                   • Error handling         │
│  • Типовые паттерны                   • Security              │
│  • "Это работает на моей машине"      • Performance           │
│                                       • Observability         │
│                                       • Business rules        │
│                                       • Backward compat       │
└───────────────────────────────────────────────────────────────┘

Почему именно 70%?

Это не точная метрика, а наблюдение из практики. AI отлично справляется с:

AI делает хорошо (70%) AI делает плохо (30%)
Шаблонный код Специфичная бизнес-логика
CRUD-операции Сложная валидация
Стандартные паттерны Нестандартные требования
Типовые API-эндпоинты Обработка граничных случаев
Базовые структуры данных Оптимизация под нагрузку
Документация по шаблону Контекстно-зависимая документация
Типовые тесты Тесты на граничные случаи

Почему последние 30% самые сложные

1. AI не знает ваш контекст

AI обучен на миллионах репозиториев, но он не знает:

  • Вашу инфраструктуру: какие базы данных, какие лимиты, какие SLA
  • Ваши бизнес-правила: "заказ можно отменить только в течение 24 часов после оплаты, кроме выходных"
  • Вашу нагрузку: 100 запросов в секунду или 100,000
  • Ваши compliance-требования: GDPR, PCI DSS, 152-ФЗ
  • Вашу историю: "мы уже пробовали этот подход, и он не работает из-за X"
<?php

declare(strict_types=1);

// AI-generated: generic order cancellation
// Looks correct, but misses ALL business rules
final class OrderService
{
    public function cancel(Order $order): void
    {
        // AI thinks this is enough
        $order->setStatus(OrderStatus::CANCELLED);
        $this->orderRepository->save($order);
    }
}

// Production reality: business rules the AI didn't know about
final class OrderServiceProduction
{
    public function cancel(Order $order, User $user): void
    {
        // Rule 1: only cancellable statuses
        if (!$order->isCancellable()) {
            throw new OrderNotCancellableException($order->getId());
        }

        // Rule 2: 24-hour window (excluding weekends/holidays)
        $businessHours = $this->businessCalendar->getBusinessHoursSince(
            $order->getPaidAt()
        );
        if ($businessHours > 24) {
            throw new CancellationWindowExpiredException($order->getId());
        }

        // Rule 3: admin can override time limit
        if ($businessHours > 24 && !$user->hasRole('ROLE_ADMIN')) {
            throw new CancellationWindowExpiredException($order->getId());
        }

        // Rule 4: partial refund after 12 business hours
        $refundAmount = $businessHours <= 12
            ? $order->getTotal()
            : $order->getTotal()->multiply(0.8); // 80% refund

        // Rule 5: notify payment provider
        $this->paymentGateway->refund($order->getPaymentId(), $refundAmount);

        // Rule 6: restore inventory
        foreach ($order->getItems() as $item) {
            $this->inventoryService->restore($item->getSku(), $item->getQuantity());
        }

        // Rule 7: audit trail
        $this->auditLog->record(
            AuditAction::ORDER_CANCELLED,
            $user->getId(),
            $order->getId(),
            ['refund_amount' => $refundAmount->getAmount()]
        );

        // Rule 8: notify customer
        $this->notificationService->send(
            new OrderCancelledNotification($order, $refundAmount)
        );

        $order->cancel($refundAmount);
        $this->orderRepository->save($order);
    }
}

AI сгенерировал 5 строк. Production-версия -- 50 строк. И это еще без учета обработки ошибок в каждом вызове внешних сервисов.

2. AI не понимает системные ограничения

AI генерирует код, который работает изолированно, но не учитывает взаимодействие с реальной системой:

// AI-generated: fetch all users and process
// Works with 100 users, crashes with 1,000,000
func ProcessAllUsers(db *sql.DB) error {
    rows, err := db.Query("SELECT * FROM users")
    if err != nil {
        return fmt.Errorf("query users: %w", err)
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
            return fmt.Errorf("scan user: %w", err)
        }
        users = append(users, u) // OOM with millions of rows
    }

    for _, u := range users {
        if err := processUser(u); err != nil {
            return err // Stops on first error, loses progress
        }
    }
    return nil
}

// Production-ready: batched processing with resilience
func ProcessAllUsersBatched(ctx context.Context, db *sql.DB) error {
    const batchSize = 1000
    var lastID int64

    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }

        users, err := fetchUserBatch(ctx, db, lastID, batchSize)
        if err != nil {
            return fmt.Errorf("fetch batch after id=%d: %w", lastID, err)
        }

        if len(users) == 0 {
            break // No more users
        }

        var errs []error
        for _, u := range users {
            if err := processUser(u); err != nil {
                errs = append(errs, fmt.Errorf("user %d: %w", u.ID, err))
                // Continue processing other users
            }
        }

        if len(errs) > 0 {
            slog.Warn("batch had errors",
                "last_id", lastID,
                "error_count", len(errs),
            )
        }

        lastID = users[len(users)-1].ID
    }

    return nil
}

func fetchUserBatch(ctx context.Context, db *sql.DB, afterID int64, limit int) ([]User, error) {
    rows, err := db.QueryContext(ctx,
        "SELECT id, name, email FROM users WHERE id > $1 ORDER BY id LIMIT $2",
        afterID, limit,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
            return nil, err
        }
        users = append(users, u)
    }
    return users, rows.Err()
}

3. AI не учитывает безопасность по умолчанию

AI оптимизирует на "работает", а не на "безопасно":

<?php

declare(strict_types=1);

// AI-generated: file upload handler
// Works, but has critical security issues
final class FileUploadController
{
    #[Route('/upload', methods: ['POST'])]
    public function upload(Request $request): JsonResponse
    {
        $file = $request->files->get('file');

        // SECURITY ISSUE 1: no file type validation
        // SECURITY ISSUE 2: no file size limit
        // SECURITY ISSUE 3: original filename used (path traversal risk)
        // SECURITY ISSUE 4: uploaded to web-accessible directory
        // SECURITY ISSUE 5: no virus scan
        // SECURITY ISSUE 6: no rate limiting

        $file->move('/var/www/uploads', $file->getClientOriginalName());

        return new JsonResponse(['status' => 'ok']);
    }
}

// Production-ready: secure file upload
final class SecureFileUploadController
{
    private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
    private const ALLOWED_MIME_TYPES = [
        'image/jpeg',
        'image/png',
        'image/webp',
        'application/pdf',
    ];
    private const UPLOAD_DIR = '/var/data/uploads'; // NOT web-accessible

    public function __construct(
        private readonly VirusScannerInterface $virusScanner,
        private readonly RateLimiterFactory $rateLimiter,
        private readonly LoggerInterface $logger,
    ) {}

    #[Route('/upload', methods: ['POST'])]
    public function upload(Request $request): JsonResponse
    {
        // Rate limiting
        $limiter = $this->rateLimiter->create('file_upload');
        if (!$limiter->consume(1)->isAccepted()) {
            return new JsonResponse(
                ['error' => 'Too many uploads'],
                Response::HTTP_TOO_MANY_REQUESTS
            );
        }

        $file = $request->files->get('file');

        if (!$file instanceof UploadedFile || !$file->isValid()) {
            return new JsonResponse(
                ['error' => 'Invalid file'],
                Response::HTTP_BAD_REQUEST
            );
        }

        // Size check
        if ($file->getSize() > self::MAX_FILE_SIZE) {
            return new JsonResponse(
                ['error' => 'File too large'],
                Response::HTTP_BAD_REQUEST
            );
        }

        // MIME type validation (check actual content, not extension)
        $mimeType = $file->getMimeType();
        if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) {
            return new JsonResponse(
                ['error' => 'File type not allowed'],
                Response::HTTP_BAD_REQUEST
            );
        }

        // Virus scan
        if (!$this->virusScanner->isClean($file->getPathname())) {
            $this->logger->critical('Malware detected in upload', [
                'original_name' => $file->getClientOriginalName(),
                'ip' => $request->getClientIp(),
            ]);
            return new JsonResponse(
                ['error' => 'File rejected'],
                Response::HTTP_BAD_REQUEST
            );
        }

        // Generate safe filename
        $extension = $file->guessExtension() ?? 'bin';
        $safeFilename = bin2hex(random_bytes(16)) . '.' . $extension;

        $file->move(self::UPLOAD_DIR, $safeFilename);

        $this->logger->info('File uploaded', [
            'filename' => $safeFilename,
            'original' => $file->getClientOriginalName(),
            'size' => $file->getSize(),
        ]);

        return new JsonResponse([
            'id' => $safeFilename,
            'status' => 'ok',
        ]);
    }
}

Типичные паттерны провалов

Отсутствие обработки ошибок

Самый частый "пропуск" AI -- неполная обработка ошибок:

// AI loves the "happy path"
func GetUserProfile(ctx context.Context, userID string) (*Profile, error) {
    user, err := userRepo.FindByID(ctx, userID)
    if err != nil {
        return nil, err // Generic error, no context
    }

    // What if user is nil but no error? (soft delete, eventual consistency)
    // What if user is banned/suspended?
    // What if profile data is partially corrupted?

    return &Profile{
        Name:  user.Name,
        Email: user.Email,
    }, nil
}

// Production: defensive programming
func GetUserProfileSafe(ctx context.Context, userID string) (*Profile, error) {
    if userID == "" {
        return nil, fmt.Errorf("user id cannot be empty")
    }

    user, err := userRepo.FindByID(ctx, userID)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return nil, fmt.Errorf("user %s not found: %w", userID, ErrNotFound)
        }
        return nil, fmt.Errorf("fetch user %s: %w", userID, err)
    }

    if user == nil {
        return nil, fmt.Errorf("user %s: unexpected nil without error", userID)
    }

    if user.Status == StatusBanned {
        return nil, fmt.Errorf("user %s: %w", userID, ErrUserBanned)
    }

    if user.Name == "" || user.Email == "" {
        slog.WarnContext(ctx, "user has incomplete profile",
            "user_id", userID,
            "has_name", user.Name != "",
            "has_email", user.Email != "",
        )
    }

    return &Profile{
        Name:  user.Name,
        Email: user.Email,
    }, nil
}

Неверные допущения о данных

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

<?php

declare(strict_types=1);

// AI assumes: all prices are in the same currency
// AI assumes: prices fit in float (they don't for financial calculations)
// AI assumes: tax is a simple percentage
function calculateOrderTotal(array $items): float
{
    $total = 0.0;
    foreach ($items as $item) {
        $total += $item['price'] * $item['quantity'];
    }
    $tax = $total * 0.20; // AI hardcoded 20% VAT
    return $total + $tax;
}

// Reality: financial calculations need precision and context
final readonly class OrderCalculator
{
    public function __construct(
        private TaxServiceInterface $taxService,
        private CurrencyConverterInterface $currencyConverter,
    ) {}

    public function calculateTotal(array $items, Address $shippingAddress): Money
    {
        $lineItems = [];
        $currency = null;

        foreach ($items as $item) {
            $price = Money::fromMinorUnits(
                $item->getPriceMinorUnits(),
                $item->getCurrency()
            );

            // Ensure consistent currency
            if ($currency === null) {
                $currency = $price->getCurrency();
            } elseif (!$price->getCurrency()->equals($currency)) {
                $price = $this->currencyConverter->convert($price, $currency);
            }

            $lineItems[] = new LineItem(
                price: $price,
                quantity: $item->getQuantity(),
                taxCategory: $item->getTaxCategory()
            );
        }

        $subtotal = Money::zero($currency);
        foreach ($lineItems as $lineItem) {
            $subtotal = $subtotal->add(
                $lineItem->price->multiply($lineItem->quantity)
            );
        }

        // Tax depends on: product category, shipping address, customer type
        $tax = $this->taxService->calculate(
            $lineItems,
            $shippingAddress
        );

        return $subtotal->add($tax);
    }
}

Ловушка "Vibe Coding"

"Vibe coding" -- это принятие AI-вывода без критического анализа, потому что код "выглядит правильно". Это самый быстрый способ накопить технический долг.

Признаки того, что вы попали в ловушку

Красные флаги vibe coding:

□ Вы не читаете сгенерированный код целиком
□ Вы нажимаете "Accept" потому что "выглядит нормально"
□ Вы не пишете тесты на AI-генерированный код
□ Вы не понимаете, почему AI выбрал именно такое решение
□ Вы не проверяете крайние случаи
□ Вы используете AI для языка/фреймворка, который плохо знаете
□ Вы не можете объяснить, что делает сгенерированный код

Правило: Если вы не можете объяснить каждую строку AI-генерированного кода -- вы не готовы принять его в production.


Стоимость пропуска 30%

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

Скорость накопления технического долга:

Ручной код:
  ████░░░░░░░░░░░░  Медленно: разработчик думает над каждой строкой

AI-код без review:
  ████████████████  Быстро: 10x кода за то же время, без проверок

AI-код с review:
  ██████░░░░░░░░░░  Умеренно: быстрая генерация + проверка

Реальные последствия

Стадия Без проверки С проверкой
День 1 Прототип готов за 2 часа Прототип готов за 4 часа
Неделя 1 5 багов в production 0 багов
Месяц 1 Серьезная уязвимость Код stable
Квартал 1 Рефакторинг 3 недели Постепенное развитие
Год 1 "Проще переписать с нуля" Maintainable codebase

Начальная экономия в 2 часа превращается в потерю недель на исправление. Принцип "быстро сейчас -- дорого потом" усиливается многократно с AI.


Как закрыть оставшиеся 30%

Стратегия 1: Структурированный review

Чек-лист для каждого блока AI-генерированного кода:

## AI Code Review Checklist

### Корректность
- [ ] Код решает поставленную задачу
- [ ] Все edge cases обработаны
- [ ] Нет скрытых предположений о данных

### Безопасность
- [ ] Входные данные валидируются
- [ ] SQL-запросы параметризированы
- [ ] Нет path traversal
- [ ] Нет утечки чувствительных данных в логах

### Производительность
- [ ] Нет N+1 запросов
- [ ] Нет загрузки всех данных в память
- [ ] Индексы для частых запросов
- [ ] Пагинация для списков

### Обработка ошибок
- [ ] Все ошибки обработаны (не проглочены)
- [ ] Ошибки содержат контекст
- [ ] Graceful degradation при недоступности зависимостей

### Совместимость
- [ ] Не сломает существующие API/контракты
- [ ] Миграции обратно совместимы
- [ ] Нет deprecated API

Стратегия 2: Автоматизированные проверки

# .github/workflows/ai-code-quality.yml
name: AI Code Quality Gates
on: [pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      # Static analysis catches AI mistakes
      - name: PHPStan (level 9)
        run: vendor/bin/phpstan analyse src/ --level=9

      # Security scanning
      - name: Security Audit
        run: |
          composer audit
          vendor/bin/psalm --taint-analysis

      # Test coverage must not decrease
      - name: Test Coverage
        run: |
          vendor/bin/phpunit --coverage-clover=coverage.xml
          # Fail if coverage drops below 80%
          php check-coverage.php coverage.xml 80

      # Performance regression detection
      - name: Performance Tests
        run: vendor/bin/phpunit --group=performance

      # Complexity check
      - name: Complexity Analysis
        run: vendor/bin/phpmd src/ text codesize

Стратегия 3: AI как первый черновик

Относитесь к AI-генерированному коду как к первому черновику, а не к финальной версии:

Workflow "AI-черновик":

1. Опишите задачу AI максимально точно
2. Получите генерацию (70% решения)
3. Прочитайте КАЖДУЮ строку
4. Задайте вопросы:
   - Какие edge cases я вижу?
   - Что произойдет при ошибке X?
   - Как это будет работать при нагрузке Y?
   - Какие бизнес-правила не учтены?
5. Допишите оставшиеся 30%
6. Напишите тесты (включая edge cases)
7. Запустите security scan
8. Проверьте производительность

Стратегия 4: Тесты как спецификация

Пишите тесты до генерации кода AI. Тесты становятся спецификацией, которую AI должен выполнить:

<?php

declare(strict_types=1);

// Write tests FIRST -- they are the specification
final class OrderCancellationTest extends TestCase
{
    public function testCannotCancelAlreadyShippedOrder(): void
    {
        $order = OrderFactory::create(status: OrderStatus::SHIPPED);
        $this->expectException(OrderNotCancellableException::class);
        $this->service->cancel($order, $this->adminUser);
    }

    public function testCannotCancelAfter24BusinessHours(): void
    {
        $order = OrderFactory::create(
            paidAt: new \DateTimeImmutable('-3 days')
        );
        $this->expectException(CancellationWindowExpiredException::class);
        $this->service->cancel($order, $this->regularUser);
    }

    public function testAdminCanCancelAfter24Hours(): void
    {
        $order = OrderFactory::create(
            paidAt: new \DateTimeImmutable('-3 days')
        );
        // Admin override -- should work
        $this->service->cancel($order, $this->adminUser);
        $this->assertEquals(OrderStatus::CANCELLED, $order->getStatus());
    }

    public function testPartialRefundAfter12BusinessHours(): void
    {
        $order = OrderFactory::create(
            total: Money::EUR(10000), // 100.00 EUR
            paidAt: new \DateTimeImmutable('-20 hours')
        );
        $this->service->cancel($order, $this->regularUser);
        // 80% refund after 12 business hours
        $this->assertEquals(Money::EUR(8000), $order->getRefundAmount());
    }

    public function testInventoryRestoredOnCancellation(): void
    {
        $order = OrderFactory::create(items: [
            ['sku' => 'ABC-123', 'quantity' => 2],
            ['sku' => 'DEF-456', 'quantity' => 1],
        ]);
        $this->service->cancel($order, $this->regularUser);
        $this->inventoryService
            ->shouldHaveReceived('restore')
            ->with('ABC-123', 2)
            ->once();
    }
}

// NOW ask AI to implement OrderService.cancel()
// that passes ALL these tests

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

  1. 70% -- это только начало. AI дает быстрый старт, но production-ready код требует человеческой экспертизы для оставшихся 30%.

  2. 30% -- это самое ценное. Именно в edge cases, безопасности и бизнес-логике заключается ценность инженера.

  3. Vibe coding -- это ловушка. Принятие AI-кода без проверки создает технический долг, который растет экспоненциально.

  4. Тесты -- ваш лучший инструмент. Пишите тесты до генерации, используйте их как спецификацию для AI.

  5. AI-код = первый черновик. Никогда не рассматривайте AI-генерированный код как финальную версию.

Главная мысль: Задача инженера -- не генерировать код (это AI делает быстрее), а обеспечивать качество, безопасность и соответствие требованиям. Оставшиеся 30% -- это то, за что платят инженерам.