Прежде чем мы продолжим, давайте разберём два слова, которые будут встречаться на каждом шагу. Если вы опытный разработчик - можете пропустить, но новичкам это сэкономит часы гугления.
Термин №1: Детерминированность
Детерминированная система - это система, которая при одинаковых входных данных всегда выдаёт одинаковый результат.
Функция sum(2,2) всегда вернёт 4. Детерминированно.
Запрос к базе данных SELECT name FROM users WHERE id=1 всегда вернёт одно и то же (пока данные не изменились). Детерминированно.
Компилятор из одного и того же исходного кода соберёт один и тот же бинарник. Детерминированно.
Почему это удобно
потому что тестирование становится предсказуемым. Вы написали тест, он зелёный - значит, код работает. Он красный - значит, что-то сломалось. Вы можете положиться на тест как на индикатор истины.
Недетерминированная система (а LLM - именно такая) при одинаковом входе может выдавать разные выходы. Один и тот же промпт «Сколько будет 2+2?» сегодня может вернуть «4», завтра - «2+2=4», а послезавтра - «Четыре, если вы про арифметику». И это не баг, а фундаментальное свойство генеративных моделей: они выбирают следующий токен случайным образом из распределения вероятностей, а не по жёсткому правилу.
Метафора
Детерминированная система - это автомат с газировкой: нажал кнопку «Кола» - получил колу. Всегда. Недетерминированная - это бариста в кофейне: вы заказали «капучино», а он спросил «покрепче?», добавил корицу «вам под настроение» и нарисовал сердечко пенкой. Вкусно, но предсказать точный рисунок невозможно.
Что это означает для тестирования
Вы не можете написать assert response == "4" - модель не виновата, но тест упадёт.
Вы не можете быть уверены, что если сегодня тесты прошли, то завтра они не упадут на том же коде.
Вам приходится проверять не точное равенство, а свойства ответа: есть ли там смысл? правильный ли формат? не противоречит ли фактам?
Термин №2: Ассертить
«Ассертить» (от английского assert - утверждать, проверять) - это действие, которое делает тест: он берёт какое-то утверждение и проверяет, истинно оно или ложно.
В коде это выглядит так:
python
# Это и есть "ассерт" - проверка утверждения
assert sum(2, 2) == 4 # "Утверждаю, что 2+2 равно 4"
# Если утверждение истинно → тест зелёный, идём дальше
# Если утверждение ложно → тест красный, программа падает с ошибкой
В жизни разработчика «ассертить» означает зафиксировать в коде теста ожидание: «вот это значение должно быть равно вот тому», «вот эта строка должна содержать вот это слово», «вот этот список не должен быть пустым».
Это фундаментальный строительный блок всех тестов в классической разработке. И именно он ломается при работе с LLM, потому что LLM не даёт вам «равенства» - она даёт «похожесть», «вероятность», «семантическую близость».
Запомните
В этой главе мы будем учиться ассертить без ассертов - проверять свойства ответов, не требуя точного совпадения.
Давайте посмотрим на один и тот же запрос к модели в трёх разных условиях и увидим, как ведут себя разные подходы к assert.

Что мы видим?
Жёсткий assert response == "4" убивает почти все правильные ответы. Он годится только для абсолютно контролируемых сценариев.
Мягкий assert "4" in response пропускает галлюцинацию - худший вид ошибки. Модель сказала «5», но тест зелёный.
Оба подхода не отличают «правильный ответ, но в другой формулировке» от «полностью неправильного ответа».
Что мы пытались проверить:
«Модель знает, что 2+2=4».
Что мы на самом деле проверили:
В первом случае: «строка ответа в точности равна "4"».
Во втором случае: «в строке ответа есть символ "4"».
Это совсем не одно и то же.
Напоминание: в классическом тестировании есть пирамида (постулированная Майком Коном для agile-команд):

В этой пирамиде:
· Unit-тесты проверяют маленькие кусочки кода изолированно.
· Интеграционные тесты проверяют, как кусочки работают вместе.
· End-to-end тесты проверяют весь сценарий целиком.
Теперь давайте попробуем применить эту пирамиду к LLM-приложению.

Проблема в том, что unit-тесты для LLM почти ничего не проверяют. Вы можете написать 1000 unit-тестов на промпт, и все они будут зелёными, но модель всё равно будет галлюцинировать на реальных данных. Потому что поведение LLM - это не функция от одного входа, а распределение вероятностей.
Инженерный вывод
В LLM-приложениях вы не можете построить надёжную систему тестирования, полагаясь только на классическую пирамиду. Вам нужны вероятностные методы оценки (о которых мы поговорим в Е.3) и специфические метрики надёжности (Е.4).
Прежде чем мы перейдём к решениям, давайте посмотрим, как НЕ надо делать. Эти ошибки я видел в каждом втором проекте, где команда впервые бралась за LLM.
Антипаттерн №1: «Жёсткий assert на полное совпадение»
python
def test_answer():
response = agent.ask("Кто написал 'Войну и мир'?")
assert response == "Лев Толстой"
Падает на «Лев Николаевич Толстой», «Толстой», «Л.Н. Толстой». Команда тратит дни на то, чтобы «подкрутить» промпт, заставляя модель отвечать строго определённой строкой. В итоге промпт превращается в монстра на 2000 токенов с запретами и ограничениями. Модель начинает галлюцинировать на других вопросах. Тесты зелёные, продукт сломан.
Антипаттерн №2: «Золотая лихорадка регулярок»
python
def test_paris():
response = agent.ask("Найди билеты в Париж")
assert re.search(r"Париж|Paris|столица Франции", response, re.IGNORECASE)
Сначала работает. Потом агент начинает отвечать: «Я не могу найти билеты в Париж, но могу предложить билеты в Лион». Регулярка проходит - слово «Париж» есть. Но ответ - провал. Команда добавляет новые регулярки, потом ещё, потом условия. Через месяц папка tests/agents/regex весит 500 строк. Никто не помнит, зачем каждая регулярка. Тесты зелёные, продукт всё равно сломан.
Антипаттерн №3: «Ручное тестирование вместо автоматизации»
- Мы не можем автоматизировать тесты, потому что LLM недетерминирована. Мы тестируем руками.
- И как часто?
- Перед каждым релизом. Раз в две недели. Два инженера тратят по полдня.
- А сколько кейсов вы проверяете?
- 20–30.
- А в проде у вас 10 000 пользователей в день?
Этот антипаттерн убивает масштабирование. Ручное тестирование не ловит регрессии на краевых случаях. Оно не даёт измерить, улучшилась ли модель или ухудшилась на 3%. Оно создаёт иллюзию контроля, но не даёт реальной уверенности.
Классические тесты не умирают полностью. Они просто отступают на второй план, проверяя самое базовое:
· Валидность JSON/XML (структура ответа)
· Наличие обязательных полей (не вернул ли модель null вместо строки)
· Отсутствие токсичности (простая проверка по чёрному списку)
· Корректность вызова инструментов (правильные имена и типы параметров)
А смысл, точность, отсутствие галлюцинаций - это уже не assert, а метрики качества. Их измеряют на выборке, а не на одном примере. И для этого нужны другие инструменты.
Вы не сделаете LLM детерминированной. Но вы можете сделать её предсказуемой в вероятностном смысле. И для этого нужны новые инструменты, а не отчаянные попытки засунуть квадратный assert в круглую нейросеть.