783 lines
52 KiB
Python
783 lines
52 KiB
Python
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()
|