# Нельзя юнит-тестить бросок кости

Разработчик встраивает LLM в систему, пишет вокруг обычный тест «прошёл/не прошёл», видит, что он флакает, и дальше либо удаляет тест, либо мокает модель до бессмысленности. Оба варианта неверны. Вероятностный компонент не сломан, когда варьируется, — но «оно варьируется» не даёт права перестать его тестировать. Просто надо тестировать распределение, а не единичный сэмпл: оценивать золотой набор с допуском, ставить порог на процент прохождений, проверять инварианты, которые обязаны держаться каждый раз, и держать жёсткую границу между стохастическим ядром и детерминированной оболочкой вокруг него.

- **Author:** Фёдор Чищин
- **Published:** 2026-07-03
- **Tags:** methodology, eval
- **Source:** https://fedorthinks.com/ru/blog/you-cant-unit-test-a-dice-roll

---

Вот баг, который на моих глазах умные инженеры выкатывают снова и снова. Они встраивают LLM в фичу,
дальше делают правильную вещь — пишут тест: подаём вход, проверяем, что на выходе ровно ожидаемая
строка. На их машине проходит. В CI падает. При перезапуске снова проходит. И тогда они делают одно из
двух — удаляют тест или мокают модель, чтобы та возвращала заготовленный ответ, — и теперь та часть
системы, которая вероятнее всего сглючит, оказывается единственной без теста вообще.

Ошибка не в флакающем тесте. Ошибка в рамке. **Ты пытаешься юнит-тестить бросок кости.**

## Проверка одного прогона — неправильный инструмент

Юнит-тест задаёт вопрос «да/нет» о детерминированной функции: дано `x` — получаю ли я ровно `y`? Это
правильный вопрос к парсеру и бессмысленный — к модели, которой _по замыслу_ позволено сказать одну и
ту же верную вещь пятью разными способами. Прогони достаточно раз — и один и тот же промпт даёт тебе
распределение, а не значение. Проверять один вытянутый из этого распределения образец почти ничего не
говорит; ты просто переносишь подбрасывание монетки в свой CI.

Вот почему цифры по продуктивности ИИ-кодинга такие мутные. По ощущениям люди быстрее, но
[опытных разработчиков на реальных задачах замеряли как _более медленных_](https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/),
а команды видят [больше пул-реквестов, но более долгие ревью и растущий churn](https://arxiv.org/pdf/2601.18341).
Немалая часть этого — люди, пытающиеся валидировать недетерминированный вывод детерминированными
привычками, по одной хрупкой проверке за раз, и тонущие в этом.

> «Оно недетерминированное» — верное утверждение про модель и ленивая отмазка про систему. Кость не
> пришпилить. Но задать рамки, в которых ей позволено выпадать, — вполне.

## Тестируй распределение, а не сэмпл

Ход в том, чтобы перестать проверять один правильный ответ и начать проверять форму поведения:

- **Оценивай золотой набор, а не сверяй строку.** Держи 30–100 реальных пар «вход/выход» и оценивай
  новые прогоны по похожести, рубрике или «модели-судье» — не по точному равенству. Ты меряешь
  качество, у которого есть диапазон, а не тождество, у которого его нет.
- **Порог на процент прохождений, а не единственный зелёный.** Прогони кейс _N_ раз и требуй, скажем,
  95% успеха. Один провал из двадцати — не сломанная сборка, а кость, ведущая себя как кость. А вот
  процент прохождений, ушедший за черту, — это уже регрессия, которую ты ловишь.
- **Проверяй инварианты, которые обязаны держаться каждый раз.** Содержимое варьируется; контракт — нет.
  Всегда валидный JSON. Никогда не сливает данные другого пользователя. Всегда внутри бюджета токенов.
  Отказывает в тех трёх промптах, в которых обязан отказать. Это _детерминированно_, и тестируется как
  всё остальное.
- **Отдели стохастическое ядро от детерминированной оболочки.** Роутинг, парсинг, валидация, ретраи и
  фолбэки вокруг модели — обычный код: юнит-тесть его жёстко, с замоканной моделью. А вероятностное
  тестирование прибереги для той единственной границы, которая на самом деле вероятностная.

## Итог

Недетерминированность — не повод не тестировать, а повод тестировать иначе. Модель, которую не
пришпилить, — ровно та часть системы, которой нужен измеряемый поводок, потому что
[«обычно работает» — это то, как ненадёжный софт ощущается изнутри ровно до момента, когда перестаёт](/ru/blog/smarter-but-not-more-reliable).

**Хватит проверять один удачный бросок. Оценивай золотой набор с допуском, ставь порог на процент
прохождений, запирай инварианты и юнит-тесть детерминированную оболочку так, будто от неё зависит
жизнь, — потому что в продакшне именно эта оболочка держит кости.**
