Не доверяй агенту: почему аудит каждой вехи — обязателен
«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-3 | 0-1 |
| Средняя фича (5-15 файлов) | 5-15 | 1-3 |
| Крупная фича (20+ файлов) | 20+ | 3-7 |
| Кросс-модульная интеграция | 30+ | 5-12 |
KG2 — кросс-модульная интеграция: модели, сервисы, jobs, контроллеры, миграции, каналы, фронтенд. 30+ файлов. Три бага — это нижняя граница ожидаемого.
Агент не врёт, когда говорит «14/14 passed». Тесты действительно проходят. Проблема в том, что агент пишет тесты на то, что он написал, а не на то, что должно работать. Это фундаментальный конфликт интересов: автор кода = автор тестов = автор отчёта о качестве.
Чеклист аудита вехи (мой рабочий)
После каждого крупного билда от агента я прохожу по списку:
- Миграции: есть ли
change_columnилиremove_column? Если да — что происходит с существующими данными? Пустая тестовая база не считается - ActionCable/WebSocket: grep имена каналов в бэкенде → grep те же строки во фронтенде. Несовпадение = мёртвый broadcast
- Jobs с файлами: передаются ли
UploadedFileобъекты в job? Если да — сериализация сломана. Только пути или blob-ключи - N+1 и O(N): Ruby-цикл по данным, которые могут вырасти? В KG2 это критично — 415 нод сегодня, 10,000 через полгода.
eachпо всем нодам для поиска — это O(N) вместо O(log N) через pgvector или SQL index - Имена и контракты: совпадают ли имена методов, параметров, JSON-ключей между сервисами? Агент может назвать поле
node_idsв одном месте иnodesв другом - 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-агентов, которые оптимизируют на прохождение тестов, а не на корректность системы.
Три правила:
- Чем больше scope — тем глубже аудит. Мелкий фикс можно принять по тестам. Кросс-модульную интеграцию — никогда
- Аудитор не должен быть автором. В multi-agent системе: отдельная роль, отдельный контекст, отдельная задача
- Проверяй границы, а не центр. Агент хорошо тестирует happy path. Баги живут на стыках: миграция + данные, бэкенд + фронтенд, контроллер + job-сериализация
«14 из 14 тестов прошли» — это начало разговора, а не его конец.
Как я строю AI Factory с аудитом каждой вехи — AICPO или напишите nevr@aicpo.com