Суть проблемы
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
Ключевые выводы
-
70% -- это только начало. AI дает быстрый старт, но production-ready код требует человеческой экспертизы для оставшихся 30%.
-
30% -- это самое ценное. Именно в edge cases, безопасности и бизнес-логике заключается ценность инженера.
-
Vibe coding -- это ловушка. Принятие AI-кода без проверки создает технический долг, который растет экспоненциально.
-
Тесты -- ваш лучший инструмент. Пишите тесты до генерации, используйте их как спецификацию для AI.
-
AI-код = первый черновик. Никогда не рассматривайте AI-генерированный код как финальную версию.
Главная мысль: Задача инженера -- не генерировать код (это AI делает быстрее), а обеспечивать качество, безопасность и соответствие требованиям. Оставшиеся 30% -- это то, за что платят инженерам.