Если ваш ИИ-бот отвечает по базе знаний, но в ответе появляется неподтверждённое условие или отказ при известном факте, разберите цепочку: вопрос → источник → найденный фрагмент → ответ. Так можно отделить пробел или конфликт в документах от ошибки поиска и неверной реакции на найденное. Ниже — небольшой синтетический комплект для такой диагностики: все документы и ответы вымышлены, а локальные программы проверяли только заданные тестовые случаи. Модель, внешний сервис и клиентский канал не запускались.
Учебный пример
Агентный процесс можно поручить сбор вопросов, поиску и сверке ответов по заранее заданным правилам. Эталоны ниже нужны для проверки именно этого набора; они не означают, что человек должен вручную размечать каждое будущее обращение. Человек уточняет отсутствующие бизнес-факты и решает исключения, например конфликт неупорядоченных политик.
Разделите три причины
В журнале фиксируйте не только ответ. Без источника и найденного фрагмента трудно понять, где именно возникла ошибка.
| Звено | Что записать | Какую причину искать |
|---|---|---|
| Документ | Есть ли факт, к какой версии он относится, на какой период действует, нет ли противоречия | Факт отсутствует, устарел или конфликтует с другим источником |
| Поиск | Точный вопрос и запрос, дата, найденные source_id, полный текст каждого найденного фрагмента |
Нужный документ не найден или найдена неподходящая версия |
| Ответ | Ожидаемое действие, фактический ответ, его fact_id и ссылки на источники |
Ответ противоречит найденному факту, придумывает его, отказывается без причины или не раскрывает конфликт |
Для FAQ в кейсе Epoch8/AGIMA рекомендуют добавлять к каждой статье примеры реальных пользовательских вопросов и делать статьи короткими, посвящёнными одной теме. Кейс Epoch8/AGIMA. Это рекомендация из конкретного проекта, а не доказательство, что такой способ сам по себе обеспечит точный ответ в другом боте.
Для демонстрации поиска здесь выбран SQLite FTS5. SQLite FTS5 позволяет выполнять полнотекстовый поиск оператором MATCH; результаты можно сортировать через ORDER BY. Документация SQLite FTS5. В учебном адаптере задана другая сортировка — по source_id; ограничение числа результатов не применяется. Поэтому журнал показывает найденные совпадения, а не выбранный моделью «лучший ответ».
В токенизаторе unicode61 токеном считается непрерывная последовательность одного или нескольких символов, классифицированных как token characters. Документация SQLite FTS5. Это описание разбиения символов, а не обещание распознавать русскую морфологию или любой вопрос-парафраз.
Поля source_id, valid_from и valid_until в этом индексе помечены UNINDEXED, а проверка срока действия выполняется отдельным условием. FTS5 не индексирует содержимое колонок с опцией UNINDEXED, поэтому такие поля нельзя использовать как полнотекстовые совпадения. Документация SQLite FTS5.
Адаптер проверяет период по дате вопроса: valid_from <= at и valid_until IS NULL OR valid_until >= at; конечная дата включается. Это правило учебного адаптера, а не встроенный механизм поиска по версии документа.
Значения выражения и даты передаются отдельно от фиксированного SQL-запроса. В Python sqlite3 значения для запроса можно передавать через placeholders; в qmark-стиле число значений должно совпадать с числом placeholders. Документация Python sqlite3. В адаптере также включён PRAGMA query_only: эта настройка блокирует обычные операции изменения данных, но не превращает соединение в полностью read-only. Документация SQLite о query_only.
Подготовьте эталон
Ниже весь вымышленный корпус «Планёр». У документов есть идентификатор и период действия. Реестр связывает каждый fact_id с источником, полем и значением.
source_id |
Период действия | Полный текст документа |
|---|---|---|
price-v1 |
2026-09-01 — 2026-09-30 | Учебный прайс «Планёр», версия 1. В сентябре 2026 цена — 1000 рублей в месяц. С 1 октября этот прайс не действует. |
price-v2 |
С 2026-10-01, даты окончания нет | Учебный прайс «Планёр», версия 2. С 1 октября 2026 цена — 2000 рублей в месяц. Настройка не включена. |
faq-v2 |
С 2026-10-01, даты окончания нет | Учебный FAQ «Планёр», версия 2. Для переноса задач поддерживается импорт CSV. Срок доставки и студенческие скидки здесь не определены. |
refund-a |
С 2026-10-01, даты окончания нет | Учебная политика возврата A: срок возврата 14 дней. Приоритет относительно политики B не задан. |
refund-b |
С 2026-10-01, даты окончания нет | Учебная политика возврата B: срок возврата 21 день. Приоритет относительно политики A не задан. |
fact_id |
source_id |
Поле | Значение |
|---|---|---|---|
F-price-old |
price-v1 |
Цена в месяц, рубли | 1000 |
F-price |
price-v2 |
Цена в месяц, рубли | 2000 |
F-setup |
price-v2 |
Настройка включена | false |
F-csv |
faq-v2 |
Импорт CSV поддерживается | true |
F-refund14 |
refund-a |
Срок возврата, дни | 14 |
F-refund21 |
refund-b |
Срок возврата, дни | 21 |
В этой статье answer, clarify и abstain — заданные вручную ожидаемые реакции: ответить по источнику, показать конфликт и запросить уточнение или не придумывать отсутствующее условие. Это эталон учебного набора, не ответ модели.
Ниже вопросы в CSV-совместимом виде. fact_ids и source_ids задают ручной эталон, а не вход поискового шага. Вопросы проверяют известный факт, отсутствие ответа, конфликт и устаревшую версию, парафраз и ложный отказ.
id,at,question,expected_action,fact_ids,source_ids,expected_response
Q1,2026-10-23,Сколько стоит тариф сейчас?,answer,F-price,price-v2,На 23 октября цена 2000 рублей в месяц; указать F-price и price-v2
Q2,2026-10-23,Во сколько обойдётся месяц использования?,answer,F-price,price-v2,Месяц стоит 2000 рублей; указать F-price и price-v2
Q3,2026-10-23,Можно перенести задачи из CSV?,answer,F-csv,faq-v2,Да импорт CSV поддерживается; указать F-csv и faq-v2
Q4,2026-10-23,Настройка уже входит в стоимость?,answer,F-setup,price-v2,Нет настройка не включена; указать F-setup и price-v2
Q5,2026-10-23,За сколько дней доставите?,abstain,,faq-v2,Срок доставки в базе не определён; уточнить у владельца
Q6,2026-10-23,В какой срок можно вернуть оплату?,clarify,F-refund14;F-refund21,refund-a;refund-b,Показать оба срока 14 и 21 день; приоритет не задан; уточнить действующую политику
Q7,2026-10-23,Есть скидка студентам?,abstain,,faq-v2,Скидка студентам в базе не определена; уточнить у владельца
Q8,2026-09-15,Сколько стоил тариф в сентябре?,answer,F-price-old,price-v1,В сентябре цена была 1000 рублей; указать F-price-old и price-v1
Для проверки достаточно различать три типа данных: вопрос, документы с периодами и ручной эталон. У вопроса есть уникальный id, текст, дата at, ожидаемое действие и ожидаемые fact_ids. У документа — source_id, даты начала и окончания действия, полный текст. В результате поиска сохраняются выражение, параметры, найденные идентификаторы и полный текст фрагментов. В ответе фиксируются действие, текст, заявленные fact_id со значениями и источниками, а также список source_ids.
В этом комплекте даты проверяются на формат YYYY-MM-DD и календарную корректность, идентификаторы — на уникальность, а периоды — на непротиворечивость. Это правила учебного контракта, а не универсальные свойства библиотек. Документация Python описывает date.fromisoformat() как разбор даты из строки ISO 8601 с указанными исключениями. Документация Python datetime. Модуль json преобразует JSON-данные в объекты Python при десериализации. Документация Python json. Модуль csv возвращает каждую прочитанную строку файла как список строк. Документация Python csv.
Для проверки агенту можно передать такое правило. Это предлагаемый текст задания, а не запуск модели в описанном учебном примере:
Используй только переданные документы, их версии, сроки действия и найденные фрагменты. Сначала определи, есть ли в найденных действующих источниках факт, нужный для ответа. Если факт подтверждён, ответь по нему и укажи его
fact_idиsource_id. Если факта нет, не додумывай его: воздержись от ответа и обозначь, что именно нужно уточнить. Если действующие источники противоречат друг другу и не задают приоритет, покажи оба значения и запроси решение владельца. Не считай само наличие ссылки доказательством корректности ответа. Отдельно сообщи, если подходящий источник не был найден.
Полный заполненный случай Q1
Ниже пример одного структурированного случая целиком. mock_context здесь задан вручную: это не журнал SQLite. В нём показана подмена старой версии прайса на актуальную для даты вопроса. Ответы также mock, не вывод модели.
| Поле | Значение |
|---|---|
question.id |
Q1 |
question.question |
Сколько стоит тариф сейчас? |
question.at |
2026-10-23 |
question.expected_action |
answer |
question.fact_ids |
F-price |
mock_context.before |
price-v1: Учебный прайс «Планёр», версия 1. В сентябре 2026 цена — 1000 рублей в месяц. С 1 октября этот прайс не действует. |
mock_context.after |
price-v2: Учебный прайс «Планёр», версия 2. С 1 октября 2026 цена — 2000 рублей в месяц. Настройка не включена. |
before_answer |
action=answer; текст: «1000 рублей в месяц»; claims: F-price-old, значение 1000, source_id=price-v1; source_ids: price-v1 |
after_answer |
action=answer; текст: «На 23 октября — 2000 рублей в месяц [F-price, price-v2]»; claims: F-price, значение 2000, source_id=price-v2; source_ids: price-v2 |
before.result |
pass=false; ошибки: expected_fact_missing, wrong_source_version_for_question_date; input_errors=[]; retrieval_missing_source_ids=[price-v2]; замечаний к документам нет; источник разметки документа — manual_golden_annotation |
after.result |
pass=true; errors=[]; input_errors=[]; retrieval_missing_source_ids=[]; замечаний к документам нет; источник разметки документа — manual_golden_annotation |
Указанная проверка смотрит на соответствие эталону и ссылок. Сам текст ответа всё равно нужно сопоставить с фрагментом: корректные fact_id и source_id ещё не доказывают, что факт использован верно.
Прогоните и разберите
Для учебного поиска использованы SQLite FTS5 и следующий контракт: поля идентификатора и срока действия не индексируются; поиск получает выражение и дату вопроса; срок проверяется отдельным условием; найденные строки сортируются по source_id; ограничение числа результатов не задано. В документации FTS5 выражение со звёздочкой после строки помечает последний извлечённый токен как префиксный; звёздочка внутри кавычек может работать не так, как ожидается. Документация SQLite FTS5.
| ID | Выражение до → после | Найденные source_id до → после |
Полный найденный фрагмент после прогона |
|---|---|---|---|
| Q1 | цена → цена |
price-v2 → price-v2 |
price-v2: Учебный прайс «Планёр», версия 2. С 1 октября 2026 цена — 2000 рублей в месяц. Настройка не включена. |
| Q2 | использование → цена |
пусто → price-v2 |
price-v2: Учебный прайс «Планёр», версия 2. С 1 октября 2026 цена — 2000 рублей в месяц. Настройка не включена. |
| Q3 | CSV → CSV |
faq-v2 → faq-v2 |
faq-v2: Учебный FAQ «Планёр», версия 2. Для переноса задач поддерживается импорт CSV. Срок доставки и студенческие скидки здесь не определены. |
| Q4 | настройка → настройка |
price-v2 → price-v2 |
price-v2: Учебный прайс «Планёр», версия 2. С 1 октября 2026 цена — 2000 рублей в месяц. Настройка не включена. |
| Q5 | "доставк"* → "доставк"* |
faq-v2 → faq-v2 |
faq-v2: Учебный FAQ «Планёр», версия 2. Для переноса задач поддерживается импорт CSV. Срок доставки и студенческие скидки здесь не определены. |
| Q6 | возврата → возврата |
refund-a; refund-b → refund-a; refund-b |
refund-a: Учебная политика возврата A: срок возврата 14 дней. Приоритет относительно политики B не задан. refund-b: Учебная политика возврата B: срок возврата 21 день. Приоритет относительно политики A не задан. |
| Q7 | "скидк"* → "скидк"* |
faq-v2 → faq-v2 |
faq-v2: Учебный FAQ «Планёр», версия 2. Для переноса задач поддерживается импорт CSV. Срок доставки и студенческие скидки здесь не определены. |
| Q8 | цена → цена |
price-v1 → price-v1 |
price-v1: Учебный прайс «Планёр», версия 1. В сентябре 2026 цена — 1000 рублей в месяц. С 1 октября этот прайс не действует. |
Q2 проверяет парафраз только в ограниченном смысле. По выражению использование поиск вернул пустой результат; после явно заданной замены выражения на цена он нашёл price-v2. Это выполненный лексический тест, а не свидетельство, что система сама распознала перефразирование.
Q5 и Q7 находят FAQ, хотя в нём нет ответа на вопрос о сроке доставки или скидке. Это полезное различие: документ может содержать релевантное упоминание темы и одновременно сообщать, что нужное условие не определено. В Q6 поиск вернул обе политики. Какую считать приоритетной, документы не устанавливают; ожидаемое clarify задано вручную.
Q8 проверяет версию на историческую дату: вопрос задан на 2026-09-15, поэтому поиск возвращает price-v1, а не более поздний прайс. Дата вопроса не заменяется текущей датой запуска.
Ниже — mock-примеры ответов для восьми вопросов. Они показывают ошибки реакции на найденный контекст и ожидаемый вариант; это не генерации модели.
| ID | Ошибочный mock-ответ | Ожидаемый mock-ответ | Что выявляет |
|---|---|---|---|
| Q1 | 1000 рублей в месяц; ссылка на F-price-old, price-v1 |
На 23 октября — 2000 рублей в месяц; F-price, price-v2 |
Использована не та версия на дату вопроса |
| Q2 | 3000 рублей в месяц; ссылка на F-price, price-v2 |
Месяц стоит 2000 рублей; F-price, price-v2 |
Значение не подтверждается фактом |
| Q3 | Не знаю, в базе нет ответа | Да, импорт CSV поддерживается; F-csv, faq-v2 |
Ложный отказ при известном факте; отдельно проверяется ниже на одинаковом контексте |
| Q4 | Да, настройка включена | Нет, настройка не включена; F-setup, price-v2 |
Ответ противоречит найденному факту |
| Q5 | Доставим завтра | Срок доставки в базе не определён; уточнить у владельца | Выдуманное отсутствующее условие |
| Q6 | 14 дней | Показать оба срока — 14 и 21 день; приоритет не задан; уточнить у владельца | Выбран один из противоречащих источников без основания |
| Q7 | В базе скидка студентам не определена; уточнить у владельца | В базе скидка студентам не определена; уточнить у владельца | Корректное воздержание при пробеле в данных |
| Q8 | В сентябре — 2000 рублей | В сентябре цена была 1000 рублей; F-price-old, price-v1 |
Использована не та версия для исторической даты |
В старом mock-примере Q3 одновременно менялись контекст и ответ, поэтому по нему одному нельзя было установить причину отказа. Для проверки ложного отказа отдельно выполнен replay: SQLite заново нашёл faq-v2, после чего два ответа проверили на одном и том же полном найденном фрагменте.
| Поле replay Q3 | Значение |
|---|---|
| Вопрос и эталон | id=Q3; «Можно перенести задачи из CSV?»; at=2026-10-23; ожидаемое действие answer; ожидаемый факт F-csv |
| Запрос и параметры поиска | Выражение CSV; параметры: CSV, 2026-10-23, 2026-10-23 |
| Один и тот же найденный контекст для обоих ответов | faq-v2, действует с 2026-10-01, даты окончания нет. Полный текст: «Учебный FAQ „Планёр“, версия 2. Для переноса задач поддерживается импорт CSV. Срок доставки и студенческие скидки здесь не определены.» |
| Проверяемый факт | F-csv; source_id=faq-v2; поле csv_import; значение true |
| Ответ до | action=abstain; «Не знаю, в базе нет ответа»; claims=[]; source_ids=[] |
| Результат до | pass=false; ошибки expected_fact_missing, wrong_action_or_false_refusal; input_errors=[]; retrieval_missing_source_ids=[]; замечаний к документам нет |
| Ответ после | action=answer; «Да, импорт CSV поддерживается [F-csv, faq-v2]»; claims: F-csv, значение true, источник faq-v2; source_ids=[faq-v2] |
| Результат после | pass=true; errors=[]; input_errors=[]; retrieval_missing_source_ids=[]; замечаний к документам нет |
В этом replay поиск не менялся и в обоих результатах retrieval_missing_source_ids=[]: документ уже был найден. До изменения ошибкой было действие ответа — отказ при доступном факте. После изменения на том же контексте ожидаемая структура и ссылки прошли проверку. Этот учебный replay не проверяет качество модели: ответы были составлены вручную.
Что именно проверяет валидатор
Структурная проверка может сверить уникальность идентификаторов, даты, соответствие fact_id реестру, значение и тип факта, его источник, найденный документ и срок его действия. Она также сравнивает действие с эталоном: например, для Q5 ожидается abstain, а для Q6 — clarify с двумя источниками. retrieval_missing_source_ids показывает, каких ожидаемых источников не было в результате поиска; это не оценка смысла ответа.
Даже правильная ссылка не подтверждает, что предложение рядом с ней верно пересказывает документ. Поэтому агенту в предлагаемом процессе нужно сверять каждое утверждение ответа с найденным фрагментом и отмечать неподтверждённые или противоречащие ему формулировки. Ссылка на факт не доказывает, что факт верно использован. При отсутствии бизнес-условия или неразрешённом конфликте источников решение передаётся владельцу факта.
Правки и повтор
После изменения документа, поиска или правила ответа повторите тот же комплект. Сопоставьте дату вопроса, выражение, параметры, найденные source_id, полный текст фрагмента, ожидаемую реакцию и ответ. Старые входы и результаты сохраняйте: иначе нельзя понять, какая именно правка изменила результат.
В комплекте уже выполнены проверки целостности. Это результаты конкретного учебного адаптера, не универсальная проверка произвольного кода.
| Проверка подмены | Зафиксированный результат |
|---|---|
Подменён source_id в строке журнала |
true — повторный поиск выявил расхождение |
| Подменён полный текст найденного фрагмента | true — проверка выявила несоответствие документа |
| Дата вопроса заменена текущей датой запуска | true — повторный запрос выявил расхождение с исходной датой и периодом документа |
| В выражение поиска передана строка, похожая на SQL-инъекцию | true — в этом адаптере она обработана как параметр выражения FTS, а не как часть SQL-запроса |
В адаптере используется фиксированный SQL-запрос, а выражение и обе даты передаются отдельными qmark-параметрами. Проверка строки, похожей на SQL-инъекцию, не обещает защитить произвольный код или чужой компонент от всех видов атак.
Повторно были выполнены 27 проверок поиска и 24 проверки целостности; четыре существенных результата приведены в таблице. Отдельно повторён тест Q3 с известным контекстом. Это подтверждает воспроизводимость указанных учебных входов и проверок, а не качество всех ботов или модели.
Для повторного запуска в комплекте предусмотрены команды:
python3 false-refusal-replay.py
sha256sum -c SHA256SUMS
Первая команда запускает replay ложного отказа и включает повтор остальных проверок. Вторая сверяет файлы с контрольными суммами. После изменения корпуса или адаптера повторите те же случаи и добавьте новые строки для изменённых условий.
Для новых обращений можно предложить такой рабочий процесс: агент собирает вопросы из доступных логов, отбирает новые формулировки, выполняет поиск без доступа к эталонным ответам, сохраняет полный контекст, а затем сверяет факты, ссылки и ожидаемые действия с документами. Для типовых случаев он готовит проверку по правилам; человеку передаются отсутствующие бизнес-факты и исключения вроде неразрешённого конфликта политик. Это предлагаемая схема, а не измеренный результат и не гарантия безошибочной работы любого агента. Обязательная ручная вычитка каждой обычной строки в таком процессе не требуется.
Совпавший повтор подтверждает только воспроизводимость этого синтетического набора на заданных входах. Он не измеряет качество всех ботов и не гарантирует отсутствия галлюцинаций.
Полный учебный корпус, вопросы, настройки, журналы и скрипты повторного запуска: скачать комплект.
Для соседних задач пригодятся материалы FAQ для сайта с нейросетью и нейросети для маркетинга.

