Практика · Тестирование AI-агентов
Синтетические данные в тестах AI-агентов: как обнаружить скрытую зависимость от продакшена
Обезличенный набор может не содержать имён и адресов, но сохранять настоящие ключи объектов, структуру связей и маршруты к рабочим системам. В результате тест проходит только потому, что агент незаметно дополняет фикстуры данными из продакшена.
Что именно может утечь
Синтетические данные должны создаваться независимо от конкретных производственных записей. Простая замена персональных полей этому требованию не соответствует. После обезличивания могут сохраниться:
- внутренние ID пользователей, заявок, документов и рабочих пространств;
- внешние ключи, UUID, номера очередей и составные ключи;
- связи между объектами и характерная форма графа;
- реальные имена хостов, URL, бакеты, индексы и названия очередей;
- шаблоны доступа: последовательность инструментов, параметры запросов и запасные маршруты;
- временные характеристики, по которым можно сопоставить тестовые и рабочие события.
Критичный признак скрытой зависимости: результат меняется при отключении сети, удалении локального кэша или замене идентификаторов на новые значения. Такой тест проверяет не только фикстуру, даже если его код утверждает обратное.
Модель проверки
Разделите доказательство на три независимые части:
- Происхождение: для каждого набора известны генератор, входы, версия схемы и допустимые преобразования.
- Независимость: идентификаторы и связи не скопированы из производственной выборки.
- Изоляция: процесс агента технически не способен обратиться к рабочим ресурсам.
Отметка «anonymized» в имени файла не доказывает ни один из этих пунктов.
Шаг 1. Зафиксируйте происхождение набора
Создайте рядом с фикстурами машиночитаемый манифест. Ниже приведён пример, а не описание реальной инфраструктуры:
{
"dataset": "agent-eval-v3",
"classification": "synthetic",
"generator": "tools/generate_eval.py",
"generator_revision": "<commit-sha>",
"schema_revision": "3",
"inputs": [
"schemas/ticket-v3.json",
"config/synthetic-distributions.json"
],
"forbidden_inputs": [
"production export",
"application cache",
"traffic replay"
],
"seed": 24017
}
Хешируйте проверяемые файлы, чтобы повторный запуск использовал тот же набор:
find ./fixtures/agent-eval-v3 -type f -print0 \
| sort -z \
| xargs -0 sha256sum \
> ./fixtures/agent-eval-v3.sha256
sha256sum --check ./fixtures/agent-eval-v3.sha256
Команды работают только с локальным каталогом ./fixtures и не обращаются к сети. В CI сохраняйте манифест и файл хешей как артефакты проверки.
Шаг 2. Найдите следы рабочей инфраструктуры
Сначала ищите не секреты, а указатели на ресурсы. Используйте только домены и шаблоны, официально утверждённые вашей командой безопасности. В примере задействованы зарезервированные имена:
rg -n --hidden \
--glob '!*.sha256' \
'(prod|production|\.internal|s3://|gs://|https?://|amqps?://|postgres(ql)?://)' \
./fixtures ./tests ./config
Затем составьте локальный список запрещённых значений без токенов и паролей:
# config/forbidden-production-markers.txt
api.prod.example.invalid
events.prod.example.invalid
production-cluster
prod-customer-id-prefix
Проверьте совпадения:
while IFS= read -r marker; do
[ -z "$marker" ] && continue
rg -n -F -- "$marker" ./fixtures ./tests ./config
done < ./config/forbidden-production-markers.txt
Файл маркеров не должен содержать действующие учётные данные. Его задача — обнаруживать имена, префиксы и адреса, раскрытие которых допустимо внутри репозитория.
Шаг 3. Проверьте идентификаторы и связи
Сканер формата UUID или номера заявки выявляет лишь форму значения. Он не доказывает совпадение с продакшеном. Сильная проверка сравнивает два локально подготовленных множества: тестовые ID и контрольный список хешей производственных ID. Исходные рабочие ID в тестовый контур переносить не нужно.
Пример формирования отпечатка внутри доверенного контура:
printf '%s' '<production-id>' \
| openssl dgst -sha256 -hmac '<comparison-key>'
Сравнение выполняйте там же, где разрешена работа с контрольным ключом. В CI передавайте только итог: количество пересечений. Норма для независимо сгенерированных идентификаторов — ноль. Сам ключ и исходные ID не должны попадать в логи или артефакты.
Отдельно проверьте структуру графа. Если тестовые сущности повторяют реальные степени узлов, редкие цепочки связей или точные размеры групп, замена ID не устраняет зависимость. Для каждого набора сравните агрегаты:
- распределение числа связей на объект;
- размеры компонент и групп;
- частоту редких типов переходов;
- совместные распределения категорий и времени.
Совпадение общей статистики допустимо, если оно задано генератором. Совпадение редкой конкретной подструктуры требует расследования происхождения.
Шаг 4. Сделайте доступ к продакшену невозможным
Пустые переменные окружения недостаточны: библиотека может подхватить профиль пользователя, метаданные среды или локальный кэш. Запускайте тест в контейнере без сети и монтируйте фикстуры только для чтения:
docker run --rm \
--network none \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop ALL \
--security-opt no-new-privileges \
--user 65532:65532 \
--mount type=bind,src="$PWD/fixtures",dst=/workspace/fixtures,readonly \
--mount type=bind,src="$PWD/tests",dst=/workspace/tests,readonly \
--env HOME=/tmp/agent-home \
--env AWS_EC2_METADATA_DISABLED=true \
<agent-test-image@sha256:digest> \
<test-command>
<agent-test-image@sha256:digest> и <test-command> — заполнители. Закрепите образ по digest, создавайте его заранее и не монтируйте домашний каталог, сокет Docker, каталоги облачных CLI или SSH.
Если агенту необходимы инструменты, поднимите локальные заглушки в отдельной тестовой сети. Разрешайте обращения только по точным именам сервисов, а неизвестный маршрут завершайте ошибкой. Ответ «пусто» опаснее явного отказа: агент может принять его за корректный результат.
Шаг 5. Добавьте отрицательные проверки
Хороший тест подтверждает не только ответ агента, но и отсутствие запрещённых действий. Журнал вызовов инструмента должен проверяться по allowlist:
{
"allowed_tools": ["fixture_search", "local_calculator"],
"allowed_hosts": ["fixture-api.test.invalid"],
"deny_unknown_tools": true,
"deny_unknown_hosts": true,
"max_tool_calls": 12
}
Добавьте три запуска:
- обычный запуск на фиксированном seed;
- запуск без сети и без пользовательских кэшей;
- метаморфный запуск с полностью заменёнными ID при сохранённой семантике.
Во всех трёх случаях смысловой результат должен оставаться одинаковым. Если после перестановки ID меняется выбранный объект, вероятна привязка к конкретному ключу, порядку записей или закэшированному ответу.
Как проверить результат
Изоляция считается подтверждённой, если одновременно выполнены следующие условия:
- манифест указывает воспроизводимый генератор и разрешённые входы;
- хеши фикстур совпадают с зафиксированной версией;
- поиск не находит адресов, префиксов и имён рабочих ресурсов;
- пересечение отпечатков тестовых и производственных ID равно нулю;
- редкие связи объясняются правилами генератора, а не копированием выборки;
- тест проходит при
--network noneи без домашних каталогов; - журнал инструментов содержит только разрешённые вызовы;
- замена всех ID не меняет смысловой результат.
Полезная финальная проверка — намеренно указать несуществующий адрес из зоны .invalid. Тест должен завершиться контролируемым отказом инструмента, а не переключиться на профиль по умолчанию или другой endpoint.
Типовые ошибки
- «Мы удалили персональные поля — набор безопасен»
- Внутренние ключи и граф связей часто идентифицируют запись не хуже имени.
- Снятие сетевого трафика вместо запрета сети
- Наблюдение полезно, но пропущенный протокол или короткий запрос оставляет риск. Сначала запретите исходящие соединения, затем анализируйте журнал.
- Передача пустого токена
- SDK может найти другой источник авторизации. Удаляйте профили и монтирования, запускайте процесс от отдельного пользователя.
- Мок, который принимает любой URL
- Такая заглушка маскирует неверный endpoint. Проверяйте схему, host, путь и допустимые параметры.
- Случайный seed при каждом запуске
- Он мешает воспроизвести дефект. Seed должен фиксироваться в манифесте, а случайные варианты — запускаться отдельной серией.
- Проверка только финального текста
- Корректный ответ не доказывает корректный маршрут. Проверяйте также вызовы инструментов и сетевые попытки.
Ограничения
Нулевое пересечение ID не доказывает, что набор полностью синтетический: реальные данные могли быть преобразованы необратимо, но сохранить структуру. Статистическая близость тоже неоднозначна — качественный генератор специально воспроизводит рабочие распределения.
Полная изоляция сети не обнаружит зависимость от данных, уже встроенных в образ, модель, индекс или кэш. Поэтому проверяйте состав образа, историю сборки и все монтирования. Для агентов, которым сеть необходима по сценарию, используйте отдельный тестовый namespace, исходящий proxy с deny-by-default и явный список адресов. Такая конфигурация слабее физического отсутствия маршрута и требует проверки правил вне процесса агента.
Практический критерий завершения
Работа закончена не тогда, когда набор назван синтетическим, а когда команда может воспроизвести его из задокументированных входов, объяснить каждую категорию связей и запустить агента в среде, где производственные ресурсы недостижимы. После этого тест начинает измерять поведение агента, а не состояние случайно доступной инфраструктуры.
Дополнительные шаблоны проверок собраны в разделе руководств, а определения терминов — в глоссарии.