[nevr]
· 7 мин чтения

Не доверяй агенту: почему аудит каждой вехи — обязателен

AI agent audit process

«14 из 14 тестов прошли. Все миграции работают. Готово к деплою.»

Это сообщение от Builder-агента. Я прочитал его, открыл код — и за 20 минут нашёл три бага, каждый из которых мог положить продакшен. Тесты действительно проходили. Баги были в том, что тесты не проверяли.

Это не баг конкретного агента. Это паттерн. И чем больше scope задачи, тем больше скрытых дефектов прячется за зелёной галочкой.

Баг #1: миграция vector(384)→1024 без USING cast

Задача: обновить размерность эмбеддингов в Knowledge Graph с 384 до 1024. Builder написал миграцию:

change_column :kg2_nodes, :embedding, :vector, limit: 1024

Тест проверял, что после миграции колонка имеет тип vector(1024). Тест проходил — на пустой тестовой базе. В продакшене 415 нод уже имели 384-мерные эмбеддинги. PostgreSQL не может автоматически преобразовать vector(384) в vector(1024) — это не числовой каст, а изменение размерности массива.

Правильная миграция:

# Шаг 1: обнулить существующие эмбеддинги (пересчитаем)
execute "UPDATE kg2_nodes SET embedding = NULL WHERE embedding IS NOT NULL"
# Шаг 2: изменить тип колонки
change_column :kg2_nodes, :embedding, :vector, limit: 1024
# Шаг 3: запустить пересчёт
Kg2::EmbeddingJob.perform_later(recalculate_all: true)

Почему агент не поймал? Потому что тестовая база пустая. У агента нет реального контекста продакшена — 415 нод, 230 рёбер, три месяца накопленных данных. Он тестирует миграцию на чистом листе. Это как тестировать переезд квартиры в пустой комнате.

Правило: миграции, изменяющие тип данных, всегда проверяй на базе с реальным объёмом. Агент этого не сделает — у него нет продакшен-данных.

Баг #2: ActionCable broadcast на неправильный канал

Builder добавил real-time обновления для Knowledge Graph. Когда новые ноды создаются, фронтенд должен получить уведомление. Код:

ActionCable.server.broadcast(
  "kg_updates_#{project.id}",
  { type: "nodes_added", count: new_nodes.size }
)

Тест проверял, что broadcast вызывается. Тест проходил. Проблема: фронтенд подписан на канал "kg_update_#{project_id}" (без s, без _-разделения формата). Правильное имя канала определено в CompetitiveChannel и KgChannel — это "kg_update", не "kg_updates".

Результат: бэкенд кричит в пустоту. Фронтенд молчит. Пользователь не видит новые ноды, пока не обновит страницу. Функционально — всё работает. Но real-time, ради которого писали этот код, — сломан.

Почему агент не поймал? Тест проверял факт вызова broadcast, а не корректность имени канала. Агент написал тест, который подтверждает его собственную ошибку. Это классический случай: тест доказывает, что код делает то, что написано, а не то, что нужно.

Правило: при аудите ActionCable и WebSocket — всегда проверяй имена каналов на обоих концах. grep по фронтенду на имя канала из бэкенда. Если совпадений нет — broadcast мёртвый.

Баг #3: bulk ingest контроллер не обрабатывает файлы

Endpoint POST /api/kg/ingest принимает массив файлов для загрузки в Knowledge Graph. Builder написал контроллер:

def ingest
  files = params[:files]
  files.each do |file|
    Kg2::BulkIngestJob.perform_later(project_id: @project.id, file: file)
  end
  render json: { status: "queued", count: files.size }
end

Тест отправлял массив строк и проверял, что jobs ставятся в очередь. Тест проходил. Проблема: ActionDispatch::Http::UploadedFile — это не строка. Реальный multipart upload приходит как объект с tempfile, original_filename, content_type. Job сериализуется в Redis — и UploadedFile не сериализуется. В продакшене каждый вызов падал бы с ActiveJob::SerializationError.

Правильный подход: сохранить файл на диск (или в blob storage) в контроллере, передать в job только путь:

def ingest
  files = params[:files]
  paths = files.map do |file|
    saved = FileUploadValidator.validate_and_save!(file)
    saved[:path]
  end
  Kg2::BulkIngestJob.perform_later(project_id: @project.id, file_paths: paths)
  render json: { status: "queued", count: paths.size }
end

Почему агент не поймал? Тестовый фреймворк позволяет передавать fixture_file_upload, но agent использовал обычные строки. Реальный HTTP multipart и тестовый mock — разные вещи. Агент не моделировал реальный запрос.

Правило: endpoints, принимающие файлы, тестируй через fixture_file_upload или реальный multipart. Строковые моки — бессмысленны.

Паттерн: масштаб задачи × плотность дефектов

Три бага в одном билде — не случайность. Я отслеживаю метрику: количество скрытых дефектов на объём scope.

Scope задачиФайлов затронутоСкрытых дефектов (среднее)
Мелкий фикс (1-3 файла)1-30-1
Средняя фича (5-15 файлов)5-151-3
Крупная фича (20+ файлов)20+3-7
Кросс-модульная интеграция30+5-12

KG2 — кросс-модульная интеграция: модели, сервисы, jobs, контроллеры, миграции, каналы, фронтенд. 30+ файлов. Три бага — это нижняя граница ожидаемого.

Агент не врёт, когда говорит «14/14 passed». Тесты действительно проходят. Проблема в том, что агент пишет тесты на то, что он написал, а не на то, что должно работать. Это фундаментальный конфликт интересов: автор кода = автор тестов = автор отчёта о качестве.

Чеклист аудита вехи (мой рабочий)

После каждого крупного билда от агента я прохожу по списку:

  1. Миграции: есть ли change_column или remove_column? Если да — что происходит с существующими данными? Пустая тестовая база не считается
  2. ActionCable/WebSocket: grep имена каналов в бэкенде → grep те же строки во фронтенде. Несовпадение = мёртвый broadcast
  3. Jobs с файлами: передаются ли UploadedFile объекты в job? Если да — сериализация сломана. Только пути или blob-ключи
  4. N+1 и O(N): Ruby-цикл по данным, которые могут вырасти? В KG2 это критично — 415 нод сегодня, 10,000 через полгода. each по всем нодам для поиска — это O(N) вместо O(log N) через pgvector или SQL index
  5. Имена и контракты: совпадают ли имена методов, параметров, JSON-ключей между сервисами? Агент может назвать поле node_ids в одном месте и nodes в другом
  6. Edge cases: что если массив пустой? Что если project не имеет фактов? Что если пользователь — anonymous (session_id вместо user_id)?

Почему это не решается «лучшими промптами»

Соблазнительно думать: «добавлю в промпт инструкцию проверять миграции на реальных данных». Не поможет. У агента нет реальных данных. Нет продакшен-контекста. Нет понимания, что канал называется kg_update, а не kg_updates — потому что это решение было принято в другом файле три недели назад.

Агент оперирует в рамках задачи. Архитектурная целостность — ответственность человека. Или, если строишь multi-agent систему, — ответственность отдельной роли: CEO/аудитор, который не пишет код, а только читает и проверяет.

В моей Factory-архитектуре это жёсткое правило: CEO никогда не редактирует код. Только читает, аудирует, возвращает на доработку. Это не бюрократия — это единственный способ сохранить separation of concerns, когда и автор, и ревьюер — AI.

Вывод: «Done» ≠ done

Зелёные тесты — необходимое, но недостаточное условие. Каждая веха требует человеческого (или CEO-агентного) аудита. Не потому что Builder плохой — а потому что автор не может быть собственным аудитором. Это верно для людей. Это вдвойне верно для AI-агентов, которые оптимизируют на прохождение тестов, а не на корректность системы.

Три правила:

  1. Чем больше scope — тем глубже аудит. Мелкий фикс можно принять по тестам. Кросс-модульную интеграцию — никогда
  2. Аудитор не должен быть автором. В multi-agent системе: отдельная роль, отдельный контекст, отдельная задача
  3. Проверяй границы, а не центр. Агент хорошо тестирует happy path. Баги живут на стыках: миграция + данные, бэкенд + фронтенд, контроллер + job-сериализация

«14 из 14 тестов прошли» — это начало разговора, а не его конец.


Как я строю AI Factory с аудитом каждой вехи — AICPO или напишите nevr@aicpo.com