Перейти к содержанию

Отказ при недостатке информации

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

В lesson-09 функция extract вызывается с strict=True. До выбора предложений она смотрит на наибольшее coverage среди упакованных кандидатов. Если значение меньше 0.5, ответ получает статус insufficient, пустой список sources и понятное сообщение. Поиск, база и документы не меняются. Так можно разобрать влияние одного нового правила и не смешивать его с изменением корпуса или формулы BM25.

Порог не является вероятностью

Coverage — доля уникальных нормализованных слов вопроса, которые присутствуют во фрагменте. Число 0.5 означает половину таких слов. Оно не означает «пятьдесят процентов уверенности модели» или «ответ верен с вероятностью половина». В цепочке ещё нет модели, а метрика вычисляется обычными множествами Python. Этот смысл полезно сохранять в названии и пояснениях. Иначе точное арифметическое значение начнёт восприниматься как статистическая оценка истинности, которой код никогда не вычислял.

Порог выбран для небольшого авторского корпуса и демонстрации механизма. Универсального значения для русского сайта нет. Вопрос «сохранение старого адреса» может иметь низкое покрытие, если документ использует «сохраняйте старые URL». Проблема здесь частично в морфологии. Увеличить порог значит сделать отказы чаще, но не обязательно сделать помощника полезнее. Перед реальным изменением правила нужно составить вопросы с известными основаниями, посмотреть их термы и отдельно измерить лишние отказы и неподдержанные ответы.

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

Отсутствие информации и отказ доступа

В публичном наборе нет текста editor-notes. Вопрос о вымышленной редакционной дате не должен раскрывать его существование подробным сообщением «документ найден, но закрыт». Текущий публичный ответ говорит об отсутствии достаточных сведений в выбранных документах. Это соответствует фактическому разрешённому контексту. Для редактора позже будет другой набор источников, но статус доступа должен определяться до поиска. Не следует использовать отказ генерации как способ скрыть текст, который уже отправили внешнему рантайму.

Ошибка базы, отсутствующий манифест и сетевой таймаут модели — другие состояния. Они не доказывают, что документ не содержит ответа. Если смешать их с insufficient, оператор не увидит сбой, а посетитель примет техническую неисправность за содержательное отсутствие. В текущем CLI ошибки чтения не перехватываются и остаются ошибками программы. В модельном шаге будет отдельное поле model_error с возвратом извлекающего основания. Так система сохранит полезную информацию и не притворится, что получила успешный сгенерированный ответ.

Где применяется решение

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

Если ни одна часть не поместилась, список пуст и отказ происходит сразу. При этом причина может быть бюджетной, а не содержательной. В расширенной версии полезно сохранить диагностический reason: no_hits, context_budget или low_coverage. Посетителю можно дать краткое сообщение, а оператору точную причину. Мы пока не добавляем такой enum, чтобы новый урок оставался одним изменением. Но уже сейчас можно проследить, какой слой вернул пустой список, и не пытаться исправлять упаковку увеличением поискового score.

Порог применяется к максимальному покрытию одного фрагмента, а не к объединению слов всех частей. Это осознанное ограничение. Два источника могут вместе отвечать на вопрос, хотя каждый покрывает меньше половины термов. У объединённого покрытия есть своя опасность: термы могут находиться в несвязанных темах и создать ложное ощущение полного ответа. Чтобы расширять правило, нужно оценивать конкретные составные вопросы. Не стоит механически выбирать большее число, если оно перестаёт соответствовать смыслу найденных оснований.

Проверяемые варианты вопроса

Полный app.py ниже показывает только новый вызов strict. При самостоятельном запуске задайте сначала sitemap опубликованных страниц: подходящий документ содержит несколько важных совпадений, поэтому ожидается supported с цитатой. Затем задайте sitemap стоимость тарифа рублей: совпадение лишь одного слова должно привести к insufficient. Эти сценарии здесь не выполнялись. Отдельно сопоставьте термы через функцию terms, чтобы видеть, какие слова считаются существенными и почему результат изменился, а не угадывать поведение по длинной исходной фразе.

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

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

Полный файл app.py из состояния lesson-09:

import json
import sys
from pathlib import Path
from loader import load_documents
from chunks import flatten
from ranking import rank
from storage import sync, read_rows
from context import pack
from answer import extract

def ask(question, role="reader"):
    if len(question) > 500:
        raise ValueError("Question is too long")
    rows = read_rows("reader")
    candidates = rank(question, rows)
    candidates = pack(candidates)
    def build():
        baseline = extract(question, candidates, strict=True)
        return baseline
    return build()

def main():
    changes = sync(load_documents(), incremental=False)
    question = sys.argv[1] if len(sys.argv) > 1 else "сохранить старых URL"
    role = sys.argv[2] if len(sys.argv) > 2 else "reader"
    print(json.dumps(ask(question, role), ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

Оглавление серии · Исходники всех шагов · Учебные маршруты

Пример остаётся локальным черновиком. При подготовке он не выполнялся; все описанные эффекты являются ожидаемыми, а не результатами запущенных проверок.