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

Ответ с проверяемыми цитатами

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

В lesson-08 появляется answer.py. Функция extract получает вопрос и уже разрешённые фрагменты. Возвращаемый объект имеет status, answer, sources и mode. Режим называется extractive. Список sources содержит id, название, URL и точную выбранную строку quote. Ответ можно показать человеку, не раскрывая внутреннюю оценку BM25 и не заставляя его интерпретировать числовое покрытие.

Что считается цитатой

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

Выбранное предложение не перефразируется. Оно берётся из row["text"], уже сохранённого в индексе. Значит, quote можно сопоставить с конкретной частью корпуса. Для нашей лаборатории это сильнее свободного текста вроде «сайт нужно сохранить полностью», который похож на вывод, но не показывает, какое правило найдено. При этом точность цитирования ещё не доказывает полноту: одно предложение может зависеть от предыдущего условия. Мы должны смотреть содержание выбранной части, а не считать сам факт буквального совпадения автоматическим признаком правильного ответа.

От вопроса к нескольким основаниям

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

В функцию попадают до четырёх частей, но она рассматривает первые три. Этот предел задан отдельно от top_k поиска и бюджета контекста. Так можно показать, что количество найденных кандидатов и количество отображённых оснований не обязаны совпадать. Однако лишнее усечение тоже может потерять полезную информацию. В собственной системе лучше сделать такие параметры одной понятной конфигурацией и включить её версию в диагностику. Сейчас явные числа позволяют проследить поведение без скрытой настройки внешнего SDK.

Если выбранное предложение не содержит ни одного терма вопроса, оно пропускается. Такое бывает, когда совпадение произошло только в заголовке. Например, заголовок «Карта сайта» помогает найти раздел, но не любое предложение раздела отвечает на вопрос о гарантии индексации. Мы не выбираем первое предложение просто потому, что документ оказался наверху. Отсутствие подходящей цитаты приводит к статусу insufficient. Он появится и при пустом контексте: у программы нет основания для извлечения ответа.

Ссылка приходит из корпуса

URL берётся из разрешённых метаданных фрагмента. Код ответа не строит его из заголовка и не просит модель воспроизвести путь. Поэтому старый PHP-адрес сохраняется ровно в том виде, в котором задан в манифесте. Это важная связь с опытом обновления ProfessorWeb: современный слой поиска может работать с Markdown, не требуя новых адресов исторических материалов. Название и внутренний id помогают человеку различать источники, а публичная ссылка остаётся отдельным устойчивым свойством.

Предположим, в манифесте ошибочно поменяли адрес на другую разрешённую страницу того же сайта. Цитата останется точной, загрузчик пропустит URL по имени узла, а переход окажется неправильным. Значит, есть отдельный редакционный контроль соответствия текста и адреса. Нельзя заменить его проверкой, что ссылка начинается с https://professorweb.ru. В реальном корпусе полезно сохранять исходное место импорта или собственный канонический permalink, чтобы каждый source был проверяемым отношением текста и страницы, а не произвольной красивой ссылкой.

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

Состояния ответа

При наличии основания возвращается supported. В этом уроке слово означает лишь извлечённое предложение из выбранного источника. Оно не должно превращаться в обещание абсолютной истинности: корпус может содержать ошибку, устаревшее правило или неполный контекст. При отсутствии оснований возвращается insufficient с ясной строкой об отсутствии сведений. Это нормальное состояние, а не исключение. Ошибка чтения базы остаётся ошибкой выполнения и не маскируется под «в документах нет ответа».

Полный answer.py ниже можно сопоставить с архивом. При самостоятельном запуске python3 app.py "sitemap опубликованных страниц" ожидается объект с цитатами и ссылками. Обратите внимание на точное происхождение каждой quote: она должна находиться в подготовленном тексте соответствующего фрагмента. Сценарий не запускался при подготовке. Чтобы разобраться в выборе предложения, вручную посчитайте совпадающие термы в двух предложениях одного раздела. При равенстве max выбирает первое встретившееся; это ещё одно явное ограничение простого алгоритма.

Проверьте также вопрос о цене, которой в корпусе нет. Ожидается insufficient, а не приблизительная стоимость из общих знаний. В следующем уроке добавим порог покрытия, чтобы одного случайного совпадения не хватало для уверенного извлечения. Например, вопрос о цене sitemap содержит знакомый термин, но остальные существенные слова не поддержаны источником. Разбор такого случая покажет, почему корректная цитата и корректный ответ на весь вопрос являются разными требованиями.

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

import re
from ranking import terms

def extract(question, rows, strict=False):
    if not rows or (strict and max(row["coverage"] for row in rows) < .5):
        return {"status": "insufficient", "answer": "В выбранных документах недостаточно сведений.",
                "sources": [], "mode": "extractive"}
    wanted = set(terms(question))
    sources = []
    for row in rows[:3]:
        sentences = re.split(r"(?<=[.!?])\s+", row["text"])
        quote = max(sentences, key=lambda sentence: len(wanted.intersection(terms(sentence))))
        if not wanted.intersection(terms(quote)):
            continue
        sources.append({"id": row["id"], "title": row["title"], "url": row["url"], "quote": quote})
    if not sources:
        return {"status": "insufficient", "answer": "Подходящая цитата не найдена.", "sources": [], "mode": "extractive"}
    return {"status": "supported", "answer": "\n".join(source["quote"] for source in sources),
            "sources": sources, "mode": "extractive"}

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

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