Files

783 lines
52 KiB
Python
Raw Permalink Normal View History

2026-05-25 01:12:43 +03:00
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()