Разработчик под ником X_ray_ml опубликовал на Habr кейс: чат-бот по обществознанию, который ищет ответы не в интернете и не в общих знаниях модели, а строго в двух конкретных учебниках — за 9 и 10 класс. Инструмент построен на Qwen2.5-3B-Instruct, векторной базе FAISS и эмбеддингах multilingual-e5-large. Это не академический эксперимент — за ним стоит вполне прикладная задача: в России вводятся единые государственные учебники, и ответ «из интернета» на уроке не засчитывается.
Почему «умная модель» здесь мешает, а не помогает
Стандартный LLM отвечает на вопрос «Что такое общество?» развёрнуто и грамотно — но не обязательно словами из нужного параграфа. Для ЕГЭ или устного ответа по конкретному учебнику это проблема: формулировка важна не меньше смысла.
Автор перевернул логику: вместо большой модели с широким контекстом — маленькая модель (3B параметров) с узким, заранее отфильтрованным контекстом из нужных страниц. Задача бота не «понять обществознание», а «найти нужный фрагмент и передать его модели в чистом виде».
Как устроен пайплайн изнутри
Парсинг PDF. Учебники загружаются через pymupdf4llm в режиме постраничного парсинга — каждый фрагмент сразу получает номер страницы. На выходе Markdown с сохранёнными заголовками, таблицами и списками.
Нарезка на чанки. Сначала MarkdownHeaderTextSplitter делит текст по трём уровням заголовков: глава, параграф, подраздел — это даёт метаданные для последующей фильтрации. Затем RecursiveCharacterTextSplitter с chunk_size=1000 и chunk_overlap=150 дробит длинные секции.
Векторная база. Эмбеддинги считаются моделью intfloat/multilingual-e5-large, индекс хранится в FAISS (faiss-cpu) прямо в оперативной памяти Colab.
Поиск с фильтрацией. Когда пользователь пишет «дай ответы на вопросы 13 параграфа 10 класса», регулярное выражение вытаскивает класс и номер параграфа, и similarity_search получает функцию-фильтр по метаданным. Важный нюанс от автора: это не предварительное отсечение, а проверка по каждому документу из результатов поиска. Параметр k=4 означает «до четырёх фрагментов» — после фильтрации их может быть меньше.
def smart_filter(metadata):
if target_class and metadata.get("Класс") != target_class:
return False
if target_paragraphs:
par_name = metadata.get("Параграф", "")
found = False
for num in target_paragraphs:
if re.search(rf'\b{num}\b', par_name):
found = True
break
if not found:
return False
return True
docs = real_vectorstore.similarity_search(
f"query: {message.text}", k=4, filter=smart_filter
)Если жёсткий фильтр ничего не нашёл, код повторяет поиск только с фильтром по классу — контекст может прийти из другого параграфа.
Генерация. Найденные фрагменты собираются в контекст с тегами класса, параграфа и страницы, затем уходят в системный промпт Qwen2.5-3B-Instruct с инструкцией отвечать строго по тексту. Температура выставлена в 0.1, max_new_tokens=400.
Где это уже ломается
Автор честно приводит два примера из реальной работы бота — и оба неидеальны.
На вопрос «Что такое общество, 10 класс» бот ответил уверенно, но сослался на § 23 «Трудовое право» — явно нерелевантный параграф для такого вопроса. Номер страницы в ответе отсутствовал, хотя системный промпт его требует.
На вопрос про теории происхождения человека бот назвал два подхода: материалистический (эволюция) и научно-фантастический (инопланетное вмешательство). Без исходного фрагмента учебника невозможно проверить, действительно ли там выделены именно эти два подхода и не пропущены ли другие. В тексте ответа остались необработанные маркеры **.
Ещё одно ограничение: если в запросе не указан класс явно, поиск идёт по всей базе сразу. Если указан только номер параграфа — бот может выбрать его в обоих учебниках одновременно.
Что это значит для тех, кто строит похожие инструменты
Кейс показывает рабочую схему RAG для узкой предметной области с минимальными ресурсами: маленькая модель, локальный векторный индекс, метаданные как фильтр. Для школьного или корпоративного ассистента «только по нашим документам» — это разумная отправная точка.
Главный урок здесь не в коде, а в честности оценки: уверенный и короткий ответ бота не означает, что найден нужный материал. Метрик точности поиска в кейсе нет, сравнения с ground truth нет — есть два живых примера, один из которых явно промахнулся. Для продакшн-инструмента, где ошибка влияет на оценку школьника, это критично.
Следующий логичный шаг — добавить reranker поверх FAISS-поиска и автоматическую проверку, что найденный параграф действительно содержит ответ на вопрос, прежде чем отдавать его модели.
