from __future__ import annotations from docx import Document from docx.enum.section import WD_SECTION from docx.enum.table import WD_TABLE_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Cm, Pt, RGBColor from pathlib import Path OUTPUT_PATH = Path("protocol/Протокол_курсовой_работы_LawBot.docx") BLACK = RGBColor(0, 0, 0) def apply_run_format(run, *, bold: bool | None = None, font_size: int | None = None) -> None: if bold is not None: run.bold = bold if font_size is not None: run.font.size = Pt(font_size) run.font.name = "Times New Roman" run._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman") run.font.color.rgb = BLACK run.font.underline = False def set_cell_text(cell, text: str, align=WD_ALIGN_PARAGRAPH.CENTER, bold: bool = False, font_size=12): cell.text = "" paragraph = cell.paragraphs[0] paragraph.alignment = align run = paragraph.add_run(text) apply_run_format(run, bold=bold, font_size=font_size) def configure_document(document: Document) -> None: section = document.sections[0] section.page_width = Cm(21) section.page_height = Cm(29.7) section.top_margin = Cm(2) section.bottom_margin = Cm(2) section.left_margin = Cm(3) section.right_margin = Cm(1.5) section.different_first_page_header_footer = True normal = document.styles["Normal"] normal.font.name = "Times New Roman" normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman") normal.font.size = Pt(14) normal.font.color.rgb = BLACK normal.paragraph_format.first_line_indent = Cm(1.25) normal.paragraph_format.line_spacing = 1.5 normal.paragraph_format.space_after = Pt(0) normal.paragraph_format.space_before = Pt(0) for style_name in ["Heading 1", "Heading 2", "Heading 3"]: style = document.styles[style_name] style.font.name = "Times New Roman" style._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman") style.font.size = Pt(14) style.font.bold = True style.font.color.rgb = BLACK style.font.underline = False style.paragraph_format.first_line_indent = Cm(0) style.paragraph_format.line_spacing = 1.5 style.paragraph_format.space_before = Pt(12) style.paragraph_format.space_after = Pt(6) footer = section.footer paragraph = footer.paragraphs[0] paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER add_page_number_field(paragraph) def add_page_number_field(paragraph) -> None: run = paragraph.add_run() fld_begin = OxmlElement("w:fldChar") fld_begin.set(qn("w:fldCharType"), "begin") instr_text = OxmlElement("w:instrText") instr_text.set(qn("xml:space"), "preserve") instr_text.text = " PAGE " fld_separate = OxmlElement("w:fldChar") fld_separate.set(qn("w:fldCharType"), "separate") fld_end = OxmlElement("w:fldChar") fld_end.set(qn("w:fldCharType"), "end") run._r.append(fld_begin) run._r.append(instr_text) run._r.append(fld_separate) run._r.append(fld_end) def add_toc(paragraph) -> None: run = paragraph.add_run() fld_begin = OxmlElement("w:fldChar") fld_begin.set(qn("w:fldCharType"), "begin") instr_text = OxmlElement("w:instrText") instr_text.set(qn("xml:space"), "preserve") instr_text.text = ' TOC \\o "1-3" \\h \\z \\u ' fld_separate = OxmlElement("w:fldChar") fld_separate.set(qn("w:fldCharType"), "separate") placeholder = OxmlElement("w:t") placeholder.text = "Обновите поле содержания в Microsoft Word (ПКМ -> Обновить поле)." fld_end = OxmlElement("w:fldChar") fld_end.set(qn("w:fldCharType"), "end") run._r.append(fld_begin) run._r.append(instr_text) run._r.append(fld_separate) run._r.append(placeholder) run._r.append(fld_end) def add_title_page(document: Document) -> None: p = document.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER apply_run_format(p.add_run("МИНОБРНАУКИ РОССИИ"), bold=True, font_size=14) for line in [ "Федеральное государственное бюджетное образовательное учреждение", "высшего образования", "_______________________________", "_______________________________", ]: paragraph = document.add_paragraph() paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER run = paragraph.add_run(line) apply_run_format(run, font_size=14) document.add_paragraph() document.add_paragraph() document.add_paragraph() p = document.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run("ПРОТОКОЛ КУРСОВОЙ РАБОТЫ") apply_run_format(run, bold=True, font_size=16) p = document.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run( "на тему «Разработка Telegram-бота для юридических консультаций\n" "на основе технологии Retrieval-Augmented Generation»" ) apply_run_format(run, font_size=14) document.add_paragraph() document.add_paragraph() table = document.add_table(rows=4, cols=2) table.alignment = WD_TABLE_ALIGNMENT.RIGHT table.columns[0].width = Cm(5.5) table.columns[1].width = Cm(7.5) rows = [ ("Выполнил:", "студент группы ____________"), ("Ф.И.О.:", "__________________________"), ("Руководитель:", "__________________________"), ("Дата:", "__________________________"), ] for row, (left, right) in zip(table.rows, rows): set_cell_text(row.cells[0], left, align=WD_ALIGN_PARAGRAPH.LEFT, bold=False, font_size=14) set_cell_text(row.cells[1], right, align=WD_ALIGN_PARAGRAPH.LEFT, bold=False, font_size=14) document.add_paragraph() document.add_paragraph() document.add_paragraph() document.add_paragraph() document.add_paragraph() p = document.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER apply_run_format(p.add_run("Волгоград, 2026"), font_size=14) document.add_page_break() def add_heading(document: Document, text: str, level: int = 1) -> None: paragraph = document.add_heading(text, level=level) paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT for run in paragraph.runs: apply_run_format(run, bold=True, font_size=14) def add_paragraph(document: Document, text: str, align=WD_ALIGN_PARAGRAPH.JUSTIFY) -> None: paragraph = document.add_paragraph(text) paragraph.alignment = align def add_list(document: Document, items: list[str]) -> None: for index, item in enumerate(items, start=1): paragraph = document.add_paragraph(f"{index}. {item}") paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY paragraph.paragraph_format.first_line_indent = Cm(0) paragraph.paragraph_format.left_indent = Cm(1.25) def add_figure_placeholder(document: Document, number: int, title: str, marker: str) -> None: p = document.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run(f"[МЕСТО ДЛЯ {marker.upper()}]") apply_run_format(run, bold=True, font_size=14) caption = document.add_paragraph() caption.alignment = WD_ALIGN_PARAGRAPH.CENTER run = caption.add_run(f"Рисунок {number} – {title}") apply_run_format(run, font_size=14) def add_table_with_caption( document: Document, number: int, title: str, headers: list[str], rows: list[list[str]], ) -> None: caption = document.add_paragraph() caption.alignment = WD_ALIGN_PARAGRAPH.CENTER run = caption.add_run(f"Таблица {number} – {title}") apply_run_format(run, font_size=14) table = document.add_table(rows=len(rows) + 1, cols=len(headers)) table.style = "Table Grid" table.alignment = WD_TABLE_ALIGNMENT.CENTER for cell, header in zip(table.rows[0].cells, headers): set_cell_text(cell, header, bold=True) for row_obj, row_data in zip(table.rows[1:], rows): for cell, value in zip(row_obj.cells, row_data): set_cell_text(cell, value, align=WD_ALIGN_PARAGRAPH.LEFT) document.add_paragraph() def build_document() -> None: document = Document() configure_document(document) add_title_page(document) add_heading(document, "СОДЕРЖАНИЕ", level=1) toc_paragraph = document.add_paragraph() toc_paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT add_toc(toc_paragraph) document.add_page_break() add_heading(document, "ВВЕДЕНИЕ", level=1) add_paragraph( document, "Развитие цифровых сервисов и широкое распространение генеративных моделей " "искусственного интеллекта создают предпосылки для появления специализированных " "информационных систем, способных предоставлять пользователю понятные и быстрые " "консультации в узких предметных областях. Одной из таких областей является " "юриспруденция, где особенно важны актуальность источников, проверяемость ответа " "и возможность сослаться на конкретную норму права.", ) add_paragraph( document, "В рамках курсовой работы разработан Telegram-бот LawBot, предназначенный для " "консультирования пользователей по бытовым юридическим вопросам в правовом поле " "Российской Федерации. Система реализует подход Retrieval-Augmented Generation: " "пользовательский запрос классифицируется, затем по локальной базе нормативных " "документов выполняется гибридный поиск, после чего большая языковая модель " "формирует ответ только по найденным источникам. Такой подход позволяет объединить " "преимущества генеративных моделей и контролируемого поиска по локальному корпусу.", ) add_paragraph( document, "Объектом исследования является процесс автоматизации первичной юридической " "консультации в Telegram. Предметом исследования являются методы проектирования " "чат-ботов, механизмы гибридного поиска по правовым документам и способы " "организации RAG-контура для прикладной юридической системы.", ) add_paragraph( document, "Цель работы состоит в повышении доступности правовой информации для пользователя " "за счет разработки Telegram-бота, который принимает вопрос, подбирает релевантные " "нормы закона и объясняет их простым языком с сохранением ссылок на источники.", ) add_paragraph(document, "Для достижения поставленной цели решены следующие задачи:") add_list( document, [ "проведен анализ предметной области и существующих решений в области юридических чат-ботов;", "спроектированы сценарии использования, функциональные требования и архитектура системы;", "реализованы Telegram-бот, парсер нормативных документов, общий слой доступа к данным и RAG API;", "выполнено тестирование разработанного программного продукта.", ], ) add_paragraph( document, "Работа состоит из четырех глав, заключения и списка использованных источников. " "В документе содержатся 10 рисунков и 4 таблицы. Первая глава посвящена анализу " "предметной области и подходов к разработке чат-ботов, во второй главе рассмотрены " "вопросы проектирования, в третьей главе описана реализация, а в четвертой приведены " "результаты тестирования.", ) add_heading(document, "1 АНАЛИЗ ПРЕДМЕТНОЙ ОБЛАСТИ", level=1) add_heading(document, "1.1 Характеристика предметной области", level=2) add_paragraph( document, "Предметной областью проекта является автоматизированная первичная юридическая " "консультация по законодательству Российской Федерации. В отличие от развлекательных " "или сервисных чат-ботов, юридический бот должен обеспечивать не только корректный " "диалог, но и высокую точность подбора правовой нормы, прозрачность происхождения " "ответа и сохранение истории взаимодействия. Ошибочный ответ в данной области может " "привести пользователя к неверным действиям, поэтому в системе сделан акцент на " "локальный корпус документов и обязательную ссылку на найденный источник.", ) add_paragraph( document, "Для MVP-версии были выбраны наиболее частые бытовые категории вопросов: трудовые " "отношения, защита прав потребителей, жилье и аренда, семейные споры, долги и займы, " "договорные отношения, вопросы суда и процесса, а также обобщенная категория " "«Другое». В качестве документной базы используются федеральные нормативные акты: " "Конституция Российской Федерации, Гражданский кодекс РФ (части 1–4), Гражданский " "процессуальный кодекс РФ, Жилищный кодекс РФ, Семейный кодекс РФ, Трудовой кодекс РФ, " "Закон РФ «О защите прав потребителей», Федеральный закон «Об исполнительном " "производстве», Федеральный закон «Об ипотеке» и КоАП РФ. Источником для первичного " "получения текстов выбран раздел популярных документов на сайте КонсультантПлюс [1].", ) add_paragraph( document, "Основная проблема пользователя в такой системе заключается в том, что ему сложно " "самостоятельно определить нужный нормативный акт, статью и актуальную редакцию " "документа. Кроме того, нормативный язык зачастую сложен для восприятия. Следовательно, " "бот должен решать сразу две задачи: находить релевантные правовые фрагменты и " "объяснять их доступным языком, не выходя за пределы найденных источников.", ) add_heading(document, "1.2 Анализ существующих решений", level=2) add_paragraph( document, "В области юридических информационных сервисов можно выделить несколько классов " "решений: генеративные универсальные ИИ-ассистенты, rule-based FAQ-боты, " "retrieval-ориентированные справочные системы и гибридные системы, совмещающие " "поиск и генерацию. На практике каждое решение имеет собственные преимущества и " "ограничения.", ) add_table_with_caption( document, 1, "Сравнение существующих решений и подходов", ["Решение", "Подход", "Преимущества", "Ограничения"], [ [ "ChatGPT, GigaChat и аналогичные LLM-ассистенты", "generation-based", "Естественный диалог, гибкость формулировок, хорошие пояснения", "Без внешнего поиска возможны галлюцинации и отсутствие точных ссылок на нормы", ], [ "FAQ-боты государственных и коммерческих сервисов", "rule-based", "Предсказуемость ответов, простая проверяемость сценариев", "Ограниченный охват, слабая работа со свободным текстом", ], [ "Справочно-правовые системы", "retrieval-based", "Высокая полнота поиска по нормативным актам, удобная работа с документами", "Требуют самостоятельного анализа текста пользователем, не ориентированы на диалог в Telegram", ], [ "Гибридные RAG-системы", "hybrid", "Сочетают поиск по корпусу и генерацию понятного ответа", "Сложнее в проектировании, требуют отдельного контура индексации и контроля качества", ], ], ) add_paragraph( document, "Для юридической области pure generation-подход является недостаточно надежным, " "поскольку модель может предложить правдоподобный, но не подтвержденный нормой " "закона ответ. Чисто rule-based подход, напротив, слишком ограничен и плохо подходит " "для свободно сформулированных пользовательских ситуаций. Справочно-правовые системы " "эффективны как поиск по документам, однако требуют от пользователя самостоятельного " "изучения и интерпретации результатов. По этим причинам наиболее рациональным для " "поставленной задачи является гибридный RAG-подход.", ) add_heading(document, "1.3 Обоснование выбранного подхода", level=2) add_paragraph( document, "В проекте выбран гибридный сценарий, в котором генеративная модель используется для " "классификации запроса, генерации поисковых фраз и финального формулирования ответа, " "а поиск выполняется по локальной базе нормативных актов. На первом этапе пользовательский " "вопрос дополняется контекстом категории, региона и типа пользователя. На втором этапе " "LLM формирует несколько поисковых фраз и предварительные фильтры. На третьем этапе " "система выполняет full-text поиск в PostgreSQL и vector search в ChromaDB, объединяет " "результаты и отбирает наиболее релевантные фрагменты. Финальный ответ формируется " "исключительно по этим фрагментам.", ) add_paragraph( document, "Выбранная схема обеспечивает компромисс между качеством диалога и контролем над " "источниками. Она хорошо масштабируется: можно добавлять новые документы, повторно " "индексировать корпус, улучшать embedding-модель, не меняя пользовательский интерфейс. " "Именно поэтому данный подход выбран как основа архитектуры LawBot.", ) add_heading(document, "2 ПРОЕКТИРОВАНИЕ ЧАТ-БОТА", level=1) add_heading(document, "2.1 Сценарии использования системы", level=2) add_paragraph( document, "Основным актором системы является пользователь Telegram, который может начать новую " "консультацию, выбрать категорию вопроса, описать ситуацию, указать регион, получить " "ответ с найденными источниками и сохранить консультацию в историю. Дополнительным " "актором является администратор, имеющий доступ к административным функциям управления. " "Также в архитектуре присутствуют внешние сервисы: источник нормативных документов, " "LLM-провайдер и сервисы хранения.", ) add_figure_placeholder( document, 1, "Диаграмма вариантов использования Telegram-бота LawBot", "диаграммы use case", ) add_paragraph( document, "Ключевой пользовательский сценарий включает последовательность «выбор категории – " "описание ситуации – указание региона – поиск по базе – выдача ответа – сохранение " "в историю». Отдельно поддерживается сценарий продолжения ранее начатой консультации, " "в котором система учитывает краткую историю сообщений и использует ее как дополнительный " "контекст для генерации ответа.", ) add_heading(document, "2.2 Функциональные требования", level=2) add_table_with_caption( document, 2, "Ключевые функциональные требования к системе", ["Группа требований", "Содержание"], [ [ "Пользовательский интерфейс", "Система должна поддерживать команды /start, главное меню, категории вопросов, профиль и историю консультаций.", ], [ "Юридический поиск", "Система должна выполнять гибридный поиск по локальному корпусу правовых документов и учитывать категорию, регион и тип пользователя.", ], [ "Формирование ответа", "Система должна формировать ответ простым языком только по найденным источникам и отображать ссылки на используемые нормы.", ], [ "Управление данными", "Система должна хранить пользователей, консультации, сообщения, список документов, чанки и историю RAG-запросов.", ], [ "Подготовка корпуса", "Система должна скачивать, нормализовать и загружать тексты законов с сохранением версионности и исходного HTML.", ], ], ) add_figure_placeholder( document, 2, "Диаграмма требований к системе LawBot", "диаграммы requirements", ) add_paragraph( document, "Сценарии и требования были согласованы с задачами курсовой работы. Особое внимание " "уделено функции контроля источников: бот не должен ссылаться на правовые нормы, которых " "нет среди найденных RAG-фрагментов. Это требование является критически важным для " "юридической предметной области.", ) add_heading(document, "2.3 Архитектура решения", level=2) add_paragraph( document, "Архитектура системы состоит из пяти логических подсистем: Telegram-бота, парсера, " "общего слоя доступа к базе данных, RAG API и подсистем хранения. Telegram-бот " "реализован на aiogram [2] и отвечает за пользовательский диалог. Парсер выделен в " "отдельный CLI-модуль, который подготавливает нормативный корпус и сохраняет его в " "PostgreSQL [5]. FastAPI-сервис [3] выполняет индексацию в ChromaDB [6], гибридный " "поиск, классификацию запроса и формирование ответа. Общие модели и репозитории " "вынесены в пакет shared, чтобы разные сервисы работали с единым описанием данных.", ) add_figure_placeholder( document, 3, "Компонентная диаграмма системы LawBot", "компонентной диаграммы", ) add_paragraph( document, "Физическое развертывание выполняется в Docker Compose [10]. В составе стенда работают " "контейнеры tgbot, api, postgredb, redisdb и chromadb. Для каждого сервиса задан healthcheck, " "а для логов используется драйвер json-file с ротацией. Для embedding-модели дополнительно " "прокинут volume, чтобы модель не загружалась заново после каждого перезапуска.", ) add_figure_placeholder( document, 4, "Схема развертывания контейнеров в Docker Compose", "схемы развертывания", ) add_heading(document, "2.4 Проектирование хранилища данных", level=2) add_paragraph( document, "Для хранения структурированных данных выбрана реляционная СУБД PostgreSQL [5]. " "В системе используются таблицы users, consultations, messages, law_sources, law_chunks " "и rag_queries. Таблица users хранит профиль пользователя Telegram, consultations и messages " "описывают историю взаимодействия, law_sources и law_chunks содержат нормативные документы " "и их фрагменты, а rag_queries фиксирует сгенерированные поисковые фразы и найденные чанки. " "Для полнотекстового поиска в PostgreSQL используется поле tsv с русской морфологией.", ) add_paragraph( document, "Векторный индекс вынесен в ChromaDB [6]. Такое разделение позволяет хранить канонический " "текст документов в PostgreSQL и независимо перестраивать embedding-индекс, не дублируя " "основную бизнес-логику. Для поддержки будущих сценариев поиска по истории консультаций " "и аудита источников все сообщения и RAG-запросы также сохраняются в основной базе данных.", ) add_figure_placeholder( document, 5, "ER-диаграмма и логическая модель хранения данных LawBot", "er-диаграммы", ) add_heading(document, "3 РЕАЛИЗАЦИЯ ЧАТ-БОТА", level=1) add_heading(document, "3.1 Используемые технологии и среда разработки", level=2) add_table_with_caption( document, 3, "Основные технологии проекта", ["Технология", "Назначение"], [ ["Python 3.12/3.13", "Основной язык разработки сервисов"], ["aiogram 3.x [2]", "Telegram-бот и обработчики пользовательских состояний"], ["FastAPI [3]", "REST API для RAG-контура и индексации"], ["SQLAlchemy 2.x [4]", "ORM и общий слой доступа к базе данных"], ["PostgreSQL 16 [5]", "Хранение пользователей, истории консультаций и правовых документов"], ["ChromaDB [6]", "Векторная база данных для семантического поиска по чанкам"], ["Sentence Transformers [7]", "Локальная CPU-модель эмбеддингов deepvk/USER2-small [9]"], ["OpenRouter [8]", "OpenAI-compatible доступ к LLM openai/gpt-5.2 для классификации и генерации ответа"], ["Docker Compose [10]", "Контейнеризация и оркестрация сервисов"], ["Redis [11]", "Хранилище состояний FSM и служебных данных бота"], ], ) add_paragraph( document, "Разработка выполнялась в виде многомодульного проекта. Для бота, API и парсера выбрана " "единая рабочая директория с общим пакетом shared. Такое решение позволило использовать " "одни и те же ORM-модели и репозитории как из Telegram-сервиса, так и из API и парсера, " "не дублируя схему данных.", ) add_heading(document, "3.2 Реализация парсера нормативных документов", level=2) add_paragraph( document, "Парсер реализован как самостоятельный CLI-модуль parser. Он поддерживает команды " "discover, fetch, normalize, ingest и run. Команда discover анализирует страницу " "популярных правовых подборок на сайте КонсультантПлюс [1], формирует manifest и " "определяет набор документов для загрузки. Команда fetch скачивает HTML-документы и " "сохраняет их в raw-кеш. Команда normalize выделяет метаданные, статьи и внутреннюю " "структуру документа, после чего сохраняет нормализованный JSON. Команда ingest " "загружает полученные данные в PostgreSQL.", ) add_paragraph( document, "Для каждого документа рассчитывается version_hash, что позволяет повторно запускать " "пайплайн без создания лишних дублей. Документы сохраняются как сущности law_sources, " "а статьи и их фрагменты – как law_chunks. В рамках MVP был успешно загружен набор из " "13 правовых источников и 5304 чанков, что покрывает ключевые бытовые юридические " "сценарии проекта.", ) add_paragraph( document, "Сохранение исходного HTML и normalized JSON делает парсер пригодным для последующей " "диагностики, повторной индексации и возможной смены механизма chunking без повторной " "загрузки всего корпуса.", ) add_heading(document, "3.3 Реализация Telegram-бота", level=2) add_paragraph( document, "Пользовательский интерфейс реализован в сервисе tgbot на базе aiogram [2]. Бот " "поддерживает главное меню, разделы «Задать вопрос», «Мои консультации», «Профиль» " "и «Помощь», а также административные разделы. Логика диалога построена на FSM, " "где отдельные состояния отвечают за выбор категории, ввод вопроса, ввод региона, " "продолжение консультации и редактирование профиля. Для хранения состояния используется " "Redis [11].", ) add_paragraph( document, "В момент получения вопроса бот обращается к RAG API через отдельный модуль на базе " "httpx. Запрос включает идентификатор пользователя, категорию вопроса, регион, тип " "пользователя и при необходимости краткую историю предыдущих сообщений. После получения " "ответа бот отображает его пользователю и сохраняет возможность открыть или продолжить " "консультацию. Также реализованы суточные лимиты на число консультаций и ограничение " "на количество пользовательских сообщений в одной ветке консультации.", ) add_figure_placeholder( document, 6, "Главное меню Telegram-бота LawBot", "скриншота интерфейса", ) add_figure_placeholder( document, 7, "Пример ответа бота с найденными источниками", "скриншота диалога", ) add_heading(document, "3.4 Реализация RAG API и гибридного поиска", level=2) add_paragraph( document, "RAG API реализован как отдельный FastAPI-сервис [3]. Он содержит эндпоинты /health, " "/api/v1/index/rebuild, /api/v1/rag/search и /api/v1/rag/answer. Во время старта сервиса " "инициализируется схема базы данных и запускается фоновая задача автоиндексации: если " "коллекция ChromaDB пуста или рассинхронизирована с PostgreSQL, сервис автоматически " "запускает rebuild индекса в фоне и детально логирует все этапы процесса.", ) add_paragraph( document, "Алгоритм retrieval включает несколько фаз. Сначала LLM классифицирует вопрос и генерирует " "набор search_queries. Затем для каждой поисковой фразы выполняется полнотекстовый поиск " "по PostgreSQL и векторный поиск по ChromaDB. Результаты объединяются в общую таблицу " "оценок, сортируются и отбираются по верхнему порогу top_k. Такой механизм позволяет " "учитывать как точные совпадения ключевых слов, так и семантическую близость запроса " "и текста нормы.", ) add_paragraph( document, "Векторные представления создаются локальной CPU-моделью deepvk/USER2-small [9] через " "библиотеку Sentence Transformers [7]. Для LLM-интеграции используется OpenRouter [8], " "через который подключена модель openai/gpt-5.2 по OpenAI-compatible интерфейсу. " "В промпте генерации ответа жёстко задано правило: использовать только те источники, " "которые были возвращены retrieval-контуром.", ) add_figure_placeholder( document, 8, "Фоновая индексация чанков и продовые логи RAG API", "скриншота логов", ) add_heading(document, "3.5 Надежность и эксплуатационные особенности", level=2) add_paragraph( document, "Для повышения надежности проект контейнеризирован. В compose-конфигурации настроены " "healthcheck-проверки для PostgreSQL, Redis и API, а также ротация логов через driver " "json-file. Для embedding-модели настроен отдельный volume, благодаря которому модель " "не скачивается повторно при каждом перезапуске. Для Telegram Bot API предусмотрена " "поддержка proxy, параметризуемая через переменную окружения.", ) add_paragraph( document, "Сервис API ведет расширенные продовые логи: фиксируются старт и завершение HTTP-запросов, " "загрузка embedding-модели, шаги индексации, число найденных полнотекстовых и векторных " "совпадений, а также параметры классификации и генерации ответа. Такая детализация " "позволяет анализировать производительность и находить проблемные участки в retrieval-пайплайне.", ) add_heading(document, "4 ТЕСТИРОВАНИЕ ЧАТ-БОТА", level=1) add_heading(document, "4.1 Методика тестирования", level=2) add_paragraph( document, "Тестирование проекта выполнялось как на уровне отдельных сервисов, так и на уровне " "сквозного пользовательского сценария. Проверялись корректность парсинга документов, " "загрузка данных в PostgreSQL, построение индекса в ChromaDB, маршрутизация запросов " "через RAG API и реакция Telegram-бота на действия пользователя. Дополнительно были " "проверены фоновые механизмы: автоиндексация при старте API, ротация логов и сохранение " "кеша embedding-модели на volume.", ) add_table_with_caption( document, 4, "Основные тестовые сценарии", ["№", "Сценарий", "Ожидаемый результат", "Фактический результат"], [ ["1", "Команда /start", "Открывается главное меню бота", "Успешно"], ["2", "Новая консультация", "Пользователь проходит шаги категория -> вопрос -> регион -> ответ", "Успешно"], ["3", "RAG answer", "API возвращает ответ со ссылками на найденные источники", "Успешно"], ["4", "История консультаций", "Пользователь может открыть, продолжить и удалить консультацию", "Успешно"], ["5", "Перестроение индекса", "API индексирует чанки в ChromaDB без ручного вмешательства", "Успешно"], ["6", "Повторный запуск API", "Модель эмбеддингов берется из локального volume, скачивание не повторяется", "Успешно"], ], ) add_heading(document, "4.2 Результаты тестирования", level=2) add_paragraph( document, "В ходе функционального тестирования был выполнен полный пайплайн parser -> PostgreSQL -> " "ChromaDB -> RAG API -> Telegram-бот. После запуска parser run в базе данных присутствовали " "13 активных источников и 5304 чанка, что подтвердило корректность загрузки нормативного корпуса. " "Затем был выполнен rebuild индекса и проверены поисковые запросы по темам трудового права, " "защиты прав потребителей, жилья и договоров. Система корректно возвращала подборку правовых " "норм и формировала итоговый ответ по найденным фрагментам.", ) add_paragraph( document, "При пользовательском тестировании проверялись сценарии работы с главным меню, " "оформление новой консультации, сохранение истории, изменение региона и типа пользователя, " "а также повторное продолжение старой консультации. Все ключевые сценарии были отработаны " "успешно, критических ошибок, приводящих к потере данных или невозможности получить ответ, " "не обнаружено.", ) add_figure_placeholder( document, 9, "Тестовый сценарий получения ответа по трудовому вопросу", "скриншота тестирования", ) add_figure_placeholder( document, 10, "Тестовый сценарий работы с историей консультаций", "скриншота тестирования", ) add_heading(document, "ЗАКЛЮЧЕНИЕ", level=1) add_paragraph( document, "В результате выполнения курсовой работы был разработан Telegram-бот LawBot для " "юридических консультаций по законодательству Российской Федерации. В ходе работы были " "решены все поставленные задачи: проведен анализ предметной области, спроектированы " "сценарии и архитектура системы, реализованы пользовательский бот, модуль подготовки " "правового корпуса, общий слой работы с данными и RAG API, а также выполнено " "тестирование полученного решения.", ) add_paragraph( document, "Практическая значимость результата заключается в том, что пользователь получает " "доступный интерфейс для первичной юридической консультации с опорой на локальный " "корпус нормативных документов. В отличие от обычных генеративных ассистентов, " "разработанная система контролирует происхождение ответа и ограничивает генерацию " "только найденными источниками.", ) add_paragraph( document, "В качестве направлений дальнейшего развития можно выделить расширение корпуса " "документов, добавление автоматического обновления базы законов, подключение более " "сильных reranking-моделей, поддержку загрузки пользовательских документов и развитие " "аналитики по качеству retrieval и генерации ответов.", ) add_heading(document, "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ", level=1) references = [ "КонсультантПлюс. Популярные документы и кодексы [Электронный ресурс]. URL: https://www.consultant.ru/popular/ (дата обращения: 24.05.2026).", "aiogram 3 documentation [Электронный ресурс]. URL: https://docs.aiogram.dev/ (дата обращения: 24.05.2026).", "FastAPI Documentation [Электронный ресурс]. URL: https://fastapi.tiangolo.com/ (дата обращения: 24.05.2026).", "SQLAlchemy 2.0 Documentation [Электронный ресурс]. URL: https://docs.sqlalchemy.org/ (дата обращения: 24.05.2026).", "PostgreSQL Documentation [Электронный ресурс]. URL: https://www.postgresql.org/docs/ (дата обращения: 24.05.2026).", "Chroma Documentation [Электронный ресурс]. URL: https://docs.trychroma.com/ (дата обращения: 24.05.2026).", "Sentence Transformers Documentation [Электронный ресурс]. URL: https://www.sbert.net/ (дата обращения: 24.05.2026).", "OpenRouter Documentation [Электронный ресурс]. URL: https://openrouter.ai/docs (дата обращения: 24.05.2026).", "Модель deepvk/USER2-small на Hugging Face [Электронный ресурс]. URL: https://huggingface.co/deepvk/USER2-small (дата обращения: 24.05.2026).", "Docker Compose file reference [Электронный ресурс]. URL: https://docs.docker.com/reference/compose-file/ (дата обращения: 24.05.2026).", "Redis Documentation [Электронный ресурс]. URL: https://redis.io/docs/ (дата обращения: 24.05.2026).", "PlantUML language reference guide [Электронный ресурс]. URL: https://plantuml.com/ru/guide (дата обращения: 24.05.2026).", ] for index, item in enumerate(references, start=1): paragraph = document.add_paragraph() paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY paragraph.paragraph_format.first_line_indent = Cm(0) paragraph.paragraph_format.left_indent = Cm(0) run = paragraph.add_run(f"{index}. {item}") run.font.name = "Times New Roman" run._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman") run.font.size = Pt(14) OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) document.save(OUTPUT_PATH) if __name__ == "__main__": build_document()