Гайд · TNWS AI

Как проверить ссылки на источники в AI-ответе по allowlist документов

6 мин—

Проверяем, что AI-ответ с цитатами ссылается только на переданные документы: ID источников, покрытие утверждений и негативный кейс.

Задача и применимость

Запретить системе отвечать «согласно документу D-999», когда в поисковой выдаче пользователю передали только D-101 и D-102. В RAG-пайплайне сохраняйте рядом с каждым найденным фрагментом стабильный source_id. После генерации требуйте структурированный ответ: список claims и для каждого список использованных source_ids. Эта проверка выявляет ссылки на отсутствующий документ, но сама по себе не доказывает, что процитированный документ действительно подтверждает утверждение; для содержательной проверки нужны текст фрагмента и независимый ревью. Здесь используется локальная тестовая фикстура: значения приведены как пример входа и ожидаемого поведения, а не как измерение реального сервиса. Если ваш проект вызывает внешнюю модель, проверьте документацию выбранного поставщика отдельно и не считайте локальную симуляцию проверкой доступности API.

Названия методов Python, синтаксис GitHub Actions и используемые версии actions сверены с официальными первоисточниками 19.09.2026. Команды в шаблонах следует запускать в собственном репозитории после проверки путей; никакие цены, тарифы и результаты производительности в статье не предполагаются.

Подготовка данных и критерий ошибки

Перед кодом сформулируйте один проверяемый инвариант из задачи: что конкретно должно пройти и какой специально нарушенный пример обязан завершиться ошибкой. Используйте синтетические обращения и стабильные ID. Данные, попавшие в репозиторий или журнал CI, могут увидеть участники проекта; не копируйте сюда реальные обращения, ключи и закрытые документы. Фикстура не должна зависеть от сегодняшнего ответа модели: ожидаемое значение задаётся после предметной проверки.

Оставьте тестовые файлы рядом с кодом. При изменении бизнес-правила обновляйте его описание и expected осознанно, отдельным ревью. Простой зелёный запуск не является доказательством: сначала проверьте, что отрицательный кейс действительно делает job красным. Ошибка должна сообщать ID или номер строки, но не раскрывать чувствительный исходный текст.

Входная фикстура, которую можно скопировать

Сохраните строки в обозначенный ниже файл; комментарии с путями — пояснение и не входят в JSONL или CSV. Если пример показывает две версии ответа, прогоните их отдельно, как описано в шагах.

{"available_sources":["D-101","D-102"],"claims":[{"text":"Возврат возможен","source_ids":["D-101"]}]}
{"available_sources":["D-101"],"claims":[{"text":"Возврат возможен","source_ids":["D-999"]}]}

Пошаговая реализация

  1. Создайте каталог scripts и файл scripts/evaluate.py; при необходимости добавьте каталог data и пример из предыдущего блока. Убедитесь, что кодировка текстовых файлов — UTF-8.
  2. Скопируйте скрипт ниже и запустите команду python scripts/evaluate.py локально. Для проекта с отдельным модулем замените демонстрационную функцию на импорт приложения. Сначала сохраните исходное проверяемое поведение.
  3. Скопируйте шаблон, затем передайте в available_sources только IDs фрагментов, действительно предоставленных модели в текущем запросе. Не используйте список всех документов базы: тогда фиктивная ссылка на невыданный, но существующий документ пройдёт проверку. Прогоните хорошие и плохие фикстуры локально, после чего добавьте скрипт в CI. При публикации ответа в интерфейсе валидируйте каждый claim до рендеринга кликабельной ссылки. Для проверки содержания попросите ревьюера открыть D-101 и подтвердить текст утверждения; membership-тест заменяет только часть этой работы.
  4. Добавьте workflow из следующего блока в .github/workflows/ai-evaluation.yml. Он запускается для pull request и не передаёт секреты коду из PR. Если сетевой интеграционный тест всё же необходим, выведите его в отдельный доверенный workflow и ограничьте доступ к ключу.
  5. Откройте успешный и намеренно неуспешный запуск. Убедитесь, что статус job соответствует коду завершения Python, а сообщение указывает на кейс, который нужно исправлять. После проверки верните корректные данные, не оставляйте искусственный сбой в ветке.
# scripts/evaluate.py
import json

def check_citations(answer):
    available = set(answer["available_sources"])
    for index, claim in enumerate(answer["claims"], 1):
        ids = claim.get("source_ids")
        if not isinstance(ids, list) or not ids:
            raise ValueError(f"claim {index}: no source")
        if not all(isinstance(item, str) for item in ids):
            raise ValueError(f"claim {index}: source ID is not string")
        missing = set(ids) - available
        if missing:
            raise ValueError(f"claim {index}: unknown source {sorted(missing)}")

good = json.loads('{"available_sources":["D-101","D-102"],"claims":[{"text":"Возврат возможен","source_ids":["D-101"]}]}')
bad = json.loads('{"available_sources":["D-101"],"claims":[{"text":"Возврат возможен","source_ids":["D-999"]}]}')
check_citations(good)
try: check_citations(bad)
except ValueError as error: print("Expected rejection:", error)
else: raise SystemExit("Unknown citation accepted")
print("Citation membership test passed")

Готовый 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

Пример входа и ожидаемого результата

При доступных D-101 и D-102 ссылка D-101 проходит. Если доступен лишь D-101, а claim ссылается на D-999, выводится unknown source ['D-999']; демонстрационный тест завершается успешно только потому, что плохой ответ корректно отвергнут. Это наблюдаемое поведение демонстрационного кода, а не отчёт о запуске на внешней модели. В production проверяйте, что конкретный контракт подходит к бизнес-процессу, и сохраняйте вход/ожидание рядом с именем кейса.

Где этот тест может обмануть

Сравнивайте точные идентификаторы, не подстроки URL и не названия похожих файлов. Если источник был обновлён, фиксируйте его версию или хеш, иначе ID может указывать на уже иной текст. Пустой список source_ids для фактического утверждения в этом контракте является ошибкой, но отказ от ответа можно моделировать отдельным типом результата. Если тест проверяет только формат, не представляйте его как оценку фактической правильности ответа. Если тест проверяет только список ID, не делайте вывода о полноте подтверждающих документов. Разделяйте сбой инфраструктуры, некорректный корпус и нарушение прикладного инварианта: у этих проблем разные действия.

Внимательно смотрите дифф при изменении ожидаемого результата. Ослабление утверждения ради зелёного статуса может скрыть ошибку. В публичных артефактах и summary показывайте только синтетический ID и минимально необходимые поля. При увеличении корпуса проверьте, что новый тестовый случай действительно добавляет неизвестную ранее ветку поведения, а не повторяет существующий текст с другой пунктуацией.

Чек-лист финальной проверки

  • У входных данных есть понятный ID и проверенное ожидаемое поведение.
  • Успешная фикстура проходит локально, испорченная падает с понятной причиной.
  • Скрипт запускается по тому же пути в CI, что и в инструкции.
  • Вход, журнал и репозиторий не содержат секретов или реальных персональных данных.
  • Сравнивается актуальная версия кода; PR не подменяет эталон без ревью.
  • Проверены границы именно этого теста: что он обнаруживает и что не проверяет.

Частые вопросы

Достаточно ли этой проверки против галлюцинаций?

Нет. Она проверяет принадлежность ID списку переданных источников, но не смысловую поддержку утверждения.

Почему нельзя сравнивать с полной базой?

Модель могла не видеть существующий документ в текущем запросе; цитата на него всё равно недостоверна.

Как поступить с общеизвестной фразой без ссылки?

Заранее определите правило: в этом шаблоне каждый claim требует источник; другой контракт нужно документировать отдельно.

Можно ли хранить URL вместо ID?

Можно, но нормализуйте и проверяйте их безопасно. Стабильный ID упрощает сравнение и отделяет проверку от отображаемой ссылки.

Как принять изменение после теста

Попросите второго участника проекта воспроизвести пример из статьи на своей ветке и оценить ожидаемый результат независимо от вывода скрипта. Особенно важно проверить, не изменили ли авторы PR эталон одновременно с функцией так, чтобы ошибка стала невидимой. Сохраните ссылку на конкретный запуск рядом с ревью и повторите тест после объединения изменений, если последующие коммиты затрагивают ту же обработку данных. При обнаружении настоящего сбоя сначала добавьте отдельную синтетическую фикстуру, которая воспроизводит проблему, и лишь потом исправляйте код. Так можно проверить, что исправление действительно удерживается, а тест не оказался формальным.

Официальная документация

Источники проверены 19.09.2026: 1, 2, 3, 4. Перед переносом на другой runtime ещё раз проверьте версии и актуальные поля документации.

Читайте также

Комментарии

Пока тихо. Скажите первое слово