Гайд · TNWS AI
Как тестировать отказ AI-ассистента от ответа без подмены результата
Проверяем контракт abstain для AI-ассистента: отдельный статус, причина, отсутствие выдуманного ответа и отрицательный сценарий.
Задача и применимость
Ассистент по внутренним правилам компании должен честно отказаться отвечать, если переданные документы не содержат нужной информации. Не закрепляйте в тестах единственную дословную фразу отказа; задайте машиночитаемый контракт status: abstain, непустую reason и пустой answer. При наличии подтверждённого источника допускается status: answered, непустой ответ и список source_ids. Тест ниже валидирует структуру и разделяет ветки, но не определяет самостоятельно, достаточно ли документов для ответа: это отдельная оценка на размеченном корпусе. Здесь используется локальная тестовая фикстура: значения приведены как пример входа и ожидаемого поведения, а не как измерение реального сервиса. Если ваш проект вызывает внешнюю модель, проверьте документацию выбранного поставщика отдельно и не считайте локальную симуляцию проверкой доступности API.
Названия методов Python, синтаксис GitHub Actions и используемые версии actions сверены с официальными первоисточниками 19.09.2026. Команды в шаблонах следует запускать в собственном репозитории после проверки путей; никакие цены, тарифы и результаты производительности в статье не предполагаются.
Подготовка данных и критерий ошибки
Перед кодом сформулируйте один проверяемый инвариант из задачи: что конкретно должно пройти и какой специально нарушенный пример обязан завершиться ошибкой. Используйте синтетические обращения и стабильные ID. Данные, попавшие в репозиторий или журнал CI, могут увидеть участники проекта; не копируйте сюда реальные обращения, ключи и закрытые документы. Фикстура не должна зависеть от сегодняшнего ответа модели: ожидаемое значение задаётся после предметной проверки.
Оставьте тестовые файлы рядом с кодом. При изменении бизнес-правила обновляйте его описание и expected осознанно, отдельным ревью. Простой зелёный запуск не является доказательством: сначала проверьте, что отрицательный кейс действительно делает job красным. Ошибка должна сообщать ID или номер строки, но не раскрывать чувствительный исходный текст.
Входная фикстура, которую можно скопировать
Сохраните строки в обозначенный ниже файл; комментарии с путями — пояснение и не входят в JSONL или CSV. Если пример показывает две версии ответа, прогоните их отдельно, как описано в шагах.
{"status":"abstain","reason":"Нет данных о сроке","answer":"","source_ids":[]}
{"status":"answered","reason":"","answer":"Срок 3 дня","source_ids":["POLICY-1"]}
Пошаговая реализация
- Создайте каталог
scriptsи файлscripts/evaluate.py; при необходимости добавьте каталогdataи пример из предыдущего блока. Убедитесь, что кодировка текстовых файлов — UTF-8. - Скопируйте скрипт ниже и запустите команду
python scripts/evaluate.pyлокально. Для проекта с отдельным модулем замените демонстрационную функцию на импорт приложения. Сначала сохраните исходное проверяемое поведение. - Сохраните шаблон и выполните его локально. Интегрируйте validate после разбора ответа и перед его отображением пользователю. В промпте явно попросите модель возвращать одну из двух веток, но не полагайтесь только на формулировку промпта: валидатор должен независимо отклонять противоречивое сочетание полей. Добавьте в корпус вопрос, на который документы не отвечают, и отдельно кейс с подтверждённым ответом. Ревьюер должен проверить основание метки
abstain; автоматическая проверка структуры не выяснит, содержит ли POLICY-1 нужный факт. - Добавьте workflow из следующего блока в
.github/workflows/ai-evaluation.yml. Он запускается для pull request и не передаёт секреты коду из PR. Если сетевой интеграционный тест всё же необходим, выведите его в отдельный доверенный workflow и ограничьте доступ к ключу. - Откройте успешный и намеренно неуспешный запуск. Убедитесь, что статус job соответствует коду завершения Python, а сообщение указывает на кейс, который нужно исправлять. После проверки верните корректные данные, не оставляйте искусственный сбой в ветке.
# scripts/evaluate.py
import json
def validate(result):
status = result.get("status")
if status == "abstain":
if not isinstance(result.get("reason"), str) or not result["reason"].strip():
raise ValueError("abstain requires reason")
if result.get("answer") or result.get("source_ids"):
raise ValueError("abstain must not fabricate answer or citations")
elif status == "answered":
if not isinstance(result.get("answer"), str) or not result["answer"].strip():
raise ValueError("answered requires nonempty answer")
if not isinstance(result.get("source_ids"), list) or not result["source_ids"]:
raise ValueError("answered requires citations")
else:
raise ValueError("unknown status")
good = {"status":"abstain","reason":"Нет данных о сроке","answer":"","source_ids":[]}
bad = {"status":"abstain","reason":"Нет данных","answer":"Срок 3 дня","source_ids":[]}
validate(good)
try: validate(bad)
except ValueError as error: print("Expected rejection:", error)
else: raise SystemExit("Fabricated answer was accepted")
print("Abstain contract verified")
Готовый workflow для проверки в pull request
Файл ниже запускает скрипт через Python. permissions: contents: read оставляет встроенному токену только чтение кода. Если вы уже используете другой CI, сохраняйте тот же негативный тест и ненулевой код выхода, а файл workflow адаптируйте к своей инфраструктуре.
name: AI evaluation
on:
pull_request:
permissions:
contents: read
jobs:
evaluate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.13'
- run: python scripts/evaluate.py
Пример входа и ожидаемого результата
Если вопрос «Каков срок возврата?» задан без документа о сроках, ожидается abstain с причиной и пустым answer. Вредный результат status: abstain вместе с answer: Срок 3 дня отвергается: он одновременно заявляет отказ и придумывает конкретный срок. Это наблюдаемое поведение демонстрационного кода, а не отчёт о запуске на внешней модели. В production проверяйте, что конкретный контракт подходит к бизнес-процессу, и сохраняйте вход/ожидание рядом с именем кейса.
Где этот тест может обмануть
Никогда не интерпретируйте пустой текст как доказательство корректного отказа: может быть сбой API. Разделяйте транспортную ошибку и явный status abstain. Источники для answered проверяйте по текущему набору выданных документов; список цитат без проверки membership может содержать выдуманный ID. Если тест проверяет только формат, не представляйте его как оценку фактической правильности ответа. Если тест проверяет только список ID, не делайте вывода о полноте подтверждающих документов. Разделяйте сбой инфраструктуры, некорректный корпус и нарушение прикладного инварианта: у этих проблем разные действия.
Внимательно смотрите дифф при изменении ожидаемого результата. Ослабление утверждения ради зелёного статуса может скрыть ошибку. В публичных артефактах и summary показывайте только синтетический ID и минимально необходимые поля. При увеличении корпуса проверьте, что новый тестовый случай действительно добавляет неизвестную ранее ветку поведения, а не повторяет существующий текст с другой пунктуацией.
Чек-лист финальной проверки
- У входных данных есть понятный ID и проверенное ожидаемое поведение.
- Успешная фикстура проходит локально, испорченная падает с понятной причиной.
- Скрипт запускается по тому же пути в CI, что и в инструкции.
- Вход, журнал и репозиторий не содержат секретов или реальных персональных данных.
- Сравнивается актуальная версия кода; PR не подменяет эталон без ревью.
- Проверены границы именно этого теста: что он обнаруживает и что не проверяет.
Частые вопросы
Можно ли разрешить причину в обычном ответе?
Можно изменить контракт, но определите семантику поля reason; здесь оно нужно только для отказа.
Как измерить качество отказов?
Нужен размеченный набор вопросов с и без ответа в документах и ручная проверка спорных случаев. Не выводите показатель по одному примеру.
Почему проверять статус отдельно от текста?
Машиночитаемая ветка снижает риск принять пустой сбойный ответ за осознанный отказ.
Что если модель ответила без source_ids?
По этому контракту результат отклоняется; либо дайте источники, либо явно измените правило для другого типа ответа.
Как принять изменение после теста
Попросите второго участника проекта воспроизвести пример из статьи на своей ветке и оценить ожидаемый результат независимо от вывода скрипта. Особенно важно проверить, не изменили ли авторы PR эталон одновременно с функцией так, чтобы ошибка стала невидимой. Сохраните ссылку на конкретный запуск рядом с ревью и повторите тест после объединения изменений, если последующие коммиты затрагивают ту же обработку данных. При обнаружении настоящего сбоя сначала добавьте отдельную синтетическую фикстуру, которая воспроизводит проблему, и лишь потом исправляйте код. Так можно проверить, что исправление действительно удерживается, а тест не оказался формальным.
Официальная документация
Источники проверены 19.09.2026: 1, 2, 3, 4. Перед переносом на другой runtime ещё раз проверьте версии и актуальные поля документации.
Читайте также
Как исключить bias из weight decay через Optax
Optax 0.2.6: маска add_decayed_weights применяет decay к kernel и оставляет bias без изменения.
Как использовать Huber loss в Optax для регрессии с выбросами
Optax 0.2.6: Huber loss квадратичен около нуля и линеен после delta; проверяем ошибки 0,5 и 2.
Как использовать sigmoid focal loss в Optax
Optax 0.2.6: focal loss для logits 0 и labels 0/1 при gamma=2; проверка симметричного базового значения.
Комментарии
Пока тихо. Скажите первое слово