Помощник страховой компании ответил, что полис покрывает аренду автомобиля на десять дней. В найденном документе было написано «до пяти дней», но этот фрагмент не попал в контекст. Ответ звучал спокойно, ссылка внизу вела на настоящий PDF, оператор его отправил. Наличие RAG и ссылки не помешало системе выдумать условие договора.
Галлюцинация возникает не только внутри модели. Поиск может не найти нужный документ, chunk потеряет оговорку, индекс окажется старым, а постобработка приклеит ссылку к неподтверждённой фразе. Управлять нужно всей цепочкой и последствиями ошибки.
Сначала разделите режимы риска
Для черновика внутренней заметки допустим свободный ответ с маркировкой. Для условий полиса, цены, медицинской рекомендации или банковской операции нужны авторитетный источник, проверяемая цитата и право отказаться. Если ошибка меняет деньги, права или здоровье, одной инструкции «не выдумывай» мало.
Практический контракт ответа:
class Claim(BaseModel):
text: str
source_id: str
quote: str
class GroundedAnswer(BaseModel):
answer: str | None
claims: list[Claim]
status: Literal["answered", "insufficient_evidence", "conflict"]После генерации сервис проверяет, что каждый source_id был выдан retrieval, цитата дословно содержится в сохранённой версии документа, а существенные утверждения покрыты claims. Проверка ссылки без проверки entailment недостаточна: настоящий документ может говорить обратное.
def validate_citations(result: GroundedAnswer, docs: dict[str, str]) -> None:
for claim in result.claims:
if claim.source_id not in docs:
raise ValueError("Источник не был выдан модели")
if normalize(claim.quote) not in normalize(docs[claim.source_id]):
raise ValueError("Цитата отсутствует в зафиксированном документе")Entailment можно проверять отдельной моделью, но на критичных ответах спорные случаи должны уходить человеку. Judge является фильтром, а не нотариусом.
Отказ является рабочим результатом
Система должна различать отсутствие доказательств, конфликт источников и технический сбой. Во всех трёх случаях красивый ответ вреднее честного статуса. Для страхового кейса retrieval без пункта о сроке должен вернуть insufficient_evidence; два действующих документа с разными сроками дают conflict; таймаут хранилища является ошибкой сервиса.
Порог отказа калибруют по данным. Слишком низкий порог даёт уверенные выдумки, слишком высокий превращает помощника в дорогую кнопку «позовите оператора». Измеряйте отдельно unsupported claim rate, корректность отказа и coverage.
Ограничивайте blast radius
Модель не должна напрямую записывать сгенерированный факт в CRM, начислять компенсацию или менять договор. Для операций используйте типизированные аргументы, policy checks и подтверждение. В интерфейсе отделяйте цитату от пересказа, показывайте дату и версию источника. В журнале сохраняйте запрос, ids и версии найденных chunks, ответ модели и результат проверок.
Для часто меняющихся данных полезен короткий TTL или запрос к системе учёта вместо RAG. Остаток на складе из вчерашнего индекса является не галлюцинацией модели, а плохой архитектурой с тем же итогом для покупателя.
Failure modes
Источник существует, утверждение не следует из него. Проверяйте claim-level entailment, а не наличие любой ссылки.
Цитата вырвана из исключения. Chunking обязан сохранять заголовок, соседние условия и идентификатор версии документа.
Модель отвечает из памяти при пустом retrieval. Пустой результат должен программно запрещать grounded answer.
Устаревший индекс. Записывайте document_version и indexed_at, задавайте freshness SLO.
Verifier видит тот же неполный контекст. Он не обнаружит пропущенный пункт. Тестируйте retrieval отдельно и добавляйте cases, где ответ лежит за границей chunk.
Пользователь принимает черновик за решение. Маркировка нужна, но критичный процесс должен блокировать отправку непроверенного ответа, а не надеяться на серый бейдж.
Правило главы
Не обещайте устранить галлюцинации. Требуйте доказательство для каждого существенного утверждения, различайте нехватку данных и конфликт, разрешайте отказ и не давайте неподтверждённому тексту выполнять дорогие действия.
Практикум
Соберите QA-сервис по десяти версиям правил возврата. В dataset включите вопросы с прямым ответом, отсутствующим ответом, конфликтом версий и оговоркой в соседнем chunk. Возвращайте GroundedAnswer и проверяйте цитаты кодом.
Измерьте unsupported claim rate, citation validity, правильность статуса отказа и coverage. Затем удалите из индекса chunk с лимитом суммы. Система должна отказаться отвечать про лимит, а не восстановить число из параметров модели. Отдельно замените действующий документ старой версией и убедитесь, что freshness gate поднимает ошибку до генерации ответа.