InquirerCaloocan LGU offers free mental health services to constituentsESPN DeportesAsí seguimos la victoria de Verstappen en el GP de SingapurESPNRanking the final four teamsRTP DesportoI Liga. Rio Ave-NacionalPunch2027 gov list: PDP engages INEC over omission of Adebutu, Pantami, othersThe Jerusalem PostHackers use security cameras to track troops, military targets in Israel, Ukraine - reportZDF heuteEntdecken Sie das ZDF-NachrichtenstudioScreen Rant7 Secondary-World Fantasy Series That Deserve More AttentionCapital FMRuto Sets April 2027 Deadline for Rironi-Mau Summit HighwayWirtualna PolskaBraun ostro o ludziach prezydenta. Wyciekły taśmyBusiness AMNoord-Korea dreigt met vergelding na Amerikaans rapport en voert nucleaire druk opRTL BoulevardADO Den Haag houdt met tien man stand bij Telstar en pakt punt
The Daily Newsstand · Free, Always
Sunday, October 11, 2026

ИИ-гид по обществознанию

Translate

Чат-бот по обществознанию: поиск по учебникам с Qwen и FAISS

В России вводятся единые государственные учебники. В моей текущей версии бота загружены два учебника по обществознанию - за 9 и 10 класс. Я хотел сделать помощника, который ищет определения и объяснения именно в этих материалах, а не в интернете и не в общих знаниях модели.

У бота нет веб-поиска. В системном промпте написано: отвечать по найденным фрагментам, и если ответа в них нет - так и сказать. Это не значит, что «модель знает только учебник»: знания из базовых весов Qwen у нее остались. Идея в том, чтобы при сборке промпта убирать все лишнее и ограничивать контекст теми параграфами, которые ученику реально нужны.

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

Это меняет архитектуру. Вместо большой модели с доступом в интернет нужна маленькая модель, которая получает короткий, узкий контекст и не уходит в общие рассуждения.

Что получилось

  • Первый сценарий: пользователь спрашивает «Что такое общество 10 класс». Бот отвечает, что по материалам 10 класса, § 23 «Трудовое право», и дает определение общества как системы взаимодействий людей, основанной на нормах, установленных государством. Ссылка на «Трудовое право» выглядит нерелевантной для такого вопроса - короткий и уверенный ответ еще не означает, что найден нужный материал. Номер страницы в ответе не указан, хотя системный промпт это требует.

  • Второй сценарий: запрос про основные теории происхождения человека. Бот перечисляет две: материалистический подход (Большой взрыв, эволюция через естественный отбор) и научно-фантастическую гипотезу (инопланетное вмешательство, с оговоркой, что она не подтверждена). Без исходного фрагмента учебника нельзя проверить, действительно ли там выделены именно эти два подхода и не пропущены ли другие. В этом ответе тоже нет номера страницы, и в тексте остались необработанные маркеры **.

Оба примера показывают, как бот выглядит в работе, но не доказывают точность его ответов относительно учебника.

Как устроен пайплайн Подготовка данных Я использую pymupdf4llm в режиме постраничного парсинга, чтобы каждый фрагмент знал свой номер страницы. Из PDF получается Markdown, где сохраняются заголовки, таблицы и списки.

Python

pages_data = pymupdf4llm.to_markdown(filename, page_chunks=True)
for page_dict in pages_data:
    page_text = page_dict["text"]
    page_num = page_dict["metadata"].get("page", 0) + 1
    md_chunks = markdown_splitter.split_text(page_text)
    safe_chunks = text_splitter.split_documents(md_chunks)
    
    for chunk in safe_chunks:
        chunk.metadata["Класс"] = grade
        chunk.metadata["Страница"] = page_num
        all_safe_chunks.append(chunk)

MarkdownHeaderTextSplitter настроен на три уровня заголовков: # - Глава, ## - Параграф, ### - Подраздел. Благодаря этому фрагменты получают метаданные, которые потом используются как фильтр поиска.

Второй проход - RecursiveCharacterTextSplitter с chunk_size=1000 и chunk_overlap=150, чтобы разрезать длинные секции.

Эмбеддинги - intfloat/multilingual-e5-large, база - FAISS (faiss-cpu, работает в оперативной памяти Colab).

Одно замечание про страницы: номер страницы файла может отличаться от напечатанного номера в учебнике. Я не проверял, корректно ли работает +1 в этой строке, поэтому пока не обещаю стопроцентно точных ссылок на страницы бумажного издания.

Поиск по запросу Когда пользователь пишет «дай ответы на вопросы 13 параграфа 10 класса», бот вытаскивает из запроса класс и номер параграфа регулярным выражением и передает в similarity_search фильтр по метаданным.

Python

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)

Здесь стоит быть точным. В моей реализации это фильтрация результатов, а не предварительное отсечение: LangChain проверяет фильтр по каждому документу, обходя результаты поиска. k=4 означает «до четырех фрагментов»: после фильтрации их может быть меньше. Если жесткий фильтр ничего не вернул, код повторяет поиск только с фильтром по классу - при этом контекст может прийти из другого параграфа.

Фильтр срабатывает, только если класс или параграф названы в запросе. Без явного «10 класс» поиск идет по всей базе; если указан только номер параграфа, поиск может выбрать этот номер в обоих учебниках.

Генерация ответа Контекст собирается из найденных фрагментов, каждому приписывается его класс, параграф и страница.

Python

meta_tag = f"[{grade} | {paragraph} | Стр. {page}]".strip(" |")
context_parts.append(f"{meta_tag}\n{doc.page_content}")
context_text = "\n\n".join(context_parts)

Дальше это уходит в системный промпт Qwen2.5-3B-Instruct вместе с указанием: отвечать строго по тексту и в конце написать, на какой странице найдена информация.

Python

messages = [
    {"role": "system", "content": f"Ты школьный ИИ-репетитор по обществознанию. Отвечай строго на основе предоставленного текста...\n\nУчебник:\n{context_text}"},
    {"role": "user", "content": message.text}
]
outputs = model.generate(**inputs, max_new_tokens=400, temperature=0.1)

Ограниченный контекст не лишает модель ее базовых знаний - она по-прежнему может что-то дописать от себя. Низкая температура помогает держаться ближе к найденному тексту, но я не проверял отдельно, как do_sample и остальные настройки генерации влияют на частоту таких вставок.

После каждого ответа я вызываю torch.cuda.empty_cache(). Это освобождает кешированную неиспользуемую память, а не память самой модели.

Что под капотом

  • Инфраструктура: Google Colab, бесплатная видеокарта T4.

  • Модель: Qwen/Qwen2.5-3B-Instruct.

  • Квантизация: 4-битная NF4 через bitsandbytes, чтобы модель поместилась в видеопамять T4.

  • Эмбеддинги: intfloat/multilingual-e5-large.

  • Векторная база: FAISS, работает в оперативной памяти.

  • Интерфейс: простой чат-бот.

Все крутится на одной бесплатной машине.

Где прототип еще сырой Честно о том, что пока не идеально:

  1. Форматирование ответов не настроено. На скриншотах видно, что модель выдает Материалистический подход как сырой текст с маркдоуном.

  2. Метка параграфа теряется на страницах без заголовка. Если параграф начинается на стр. 139 и продолжается на стр. 140 без нового заголовка, фрагменты со второй страницы могут остаться без метаданных «Параграф». Это значит, что при запросе конкретного параграфа они не попадут в выдачу.

  3. Нет проверки фактов против контекста. Модель получает ограниченный контекст, но скрипт никак не сверяет итоговую фразу в ответе с фрагментом, откуда она взялась. Это задача на будущую итерацию (дополнительный фактчекинг перед отправкой).

  4. Класс пользователя не запоминается. Если он не назван в текущем сообщении, бот не знает, какой учебник нужен. Это неудобно и требует реализации хранения состояний сессии.

Зачем это вообще нужно Школьнику нужен помощник, который отвечает по тем материалам, которые будет спрашивать учитель, а не по всей сумме знаний модели. С переходом на единые учебники это становится еще и практично: база из нескольких книг может покрыть предмет целиком, без споров «а по какому учебнику это написано».

В планах - масштабировать пайплайн на историю и биологию, где строгая фактология важна еще больше.

Что дальше Известные мне задачи на ближайшие спринты:

  • Настроить форматирование ответов в чате

  • Реализовать память сессий, чтобы запоминать класс пользователя.

  • Добавить валидацию: проверять ответ на соответствие контексту перед отправкой.

  • Решить проблему «потерянных» метаданных на страницах без заголовка параграфа.

А как вы ограничиваете LLM в узкоспециализированных проектах, чтобы она не уходила в общие знания? Буду рад обсудить в комментариях!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.