Всем привет! Меня зовут Владимир Глебовец, также известный в среде юридического сообщества, как LawCoder. С 2007 года я работаю юристом, а с 2018 в свободное от работы время, программирую инструменты, которые потом использую в юридической работе. Обычно я пишу заметки на VC и в телеграме, а вот писать на Хабр не решался, т. к. ничего полезного для «трушных» программистов я написать не мог, ибо мой уровень соответствует понятию Low Coding, каламбур из которого (Low‑Law) собственно и дал название моему блогу об автоматизации юридических процессов.
Так зачем же тогда я пришел на Хабр сейчас? Научился программировать, поверил в себя и решил огрести в комментариях? Нет, за последние три года, я пользуясь ЛЛМ, программируя в режиме вайб‑кодинга, растерял и без того слабые навыки программирования. Может быть решил прорекламировать свой блог? Тоже нет, здесь нет моей целевой аудитории, да и я собственно его недавно официально даже на паузу поставил, потеряв к ведению блога интерес.
Пришел я сюда, с желанием проверить одну гипотезу, возможно Хабр поможет мне её подтвердить или опровергнуть. При этом для меня это ситуация win-win, т.к. любой результат меня устроит.
Гипотеза в следующем: я уверен,что legaltech‑инструменты могут быть открытыми для сообщества, также как это работает сейчас в сообществе разработчиков с опенсорс библиотеками и программами, и что с появлением и развитием ЛЛМ сделать много классных и полезных инструментов для юристов будет гораздо проще. Но я также и понимаю, что большинству юристов тяжело вкатиться в разработку, не имея соответствующей базы и им нужна помощь, в том числе помощь коллег и/или друзей программистов.
У меня есть небольшой проект, который я пишу на svelte + tailwind по вечерам и выходным дням, называется экспериментаторская.рф. Для себя я определил это как место где можно проверить юридические гипотезы, которые периодически появляются у меня или моих коллег. И хотя уже сейчас её можно было бы монетизировать, сделав из неё юридический сервис, я не хочу этого делать, а хочу раскрыть свой код, чтобы каждый мог повторить мой путь, допилив решение под себя. Возможно вы программист, и прочитав эту статью, вы напишете в телеге, своему коллеге «Эй, бро, я тут статью на Хабре прочитал, давай тебе за пару часов у нас в контуре развернем эту историю? По‑любому пойдет на пользу всем нам. Бумажки свои быстрее начнешь согласовывать и нам польза.». А возможно вы юрист‑энтузиаст, как и я, интересующийся современными технологиями, и сами попытаетесь повторить. В любом случае я буду рад внести свою лепту, в открытость легалтех решений.
Итак начнем. Первый раздел экспериментаторской, который я хочу показать называется «Цитирование ГК в договоре». Работает этот раздел так: вы загружаете в него договор. Затем запускаете процесс поиска цитат из ГК РФ. Код обходит каждый абзац, получает из него эмбеддинг, ищет к нему 5 ближайших соседей из базы данных, и показывает их если соответствие больше или равно заданному пользователем. В проде этот раздел находится здесь: экспериментаторская.рф/цитирование_гк_рф_в_договоре.
Использованный стек: Svelte+Tailwind для фронта, Nodejs на бэкенде, vercel serverless functions для запросов к БД и АПИ опенаи, БД развернута на Zilliz Cloud, для получения эмбеддингов используется модель опенаи «text‑embedding-3-small», остальное крутится на клиенте (это моя принципиальная позиция — делать как можно меньше запросов на сервер).
Как устроен фронтенд
1. Загрузка и распаковка DOCX-файла Для работы с DOCX-файлами используется библиотека JSZip, которая позволяет извлекать XML-контент документа прямо в браузере пользователя. async function uploadDoc() { const file = fileArray[0]; const zip = new JSZip(); const content = await zip.loadAsync(file); const docXmlFile = content.file("word/document.xml"); const docXml = await docXmlFile.async("string"); blocks = processXml(docXml); } 2. Разбор XML-документа Разбор XML-документа осуществляется рекурсивной функцией, которая обрабатывает текстовые узлы и специальные элементы форматирования. function processNode(node) { if (node.nodeType === Node.TEXT_NODE) return node.textContent; if (node.nodeType === Node.ELEMENT_NODE) { let result = ""; node.childNodes.forEach(child => { result += processNode(child); }); return result; } return ""; } 3. Формирование блоков с Tailwind-стилями Абзацы и таблицы из XML преобразуются в интерактивные блоки HTML с применением Tailwind-стилей. resultBlocks.push({ id: `block-${blockIndex++}`, type: "paragraph", text: paraText.trim(), html: `<p class="m-0">${paraText}</p>` }); 4. Интеграция с API для поиска цитат Каждый блок отправляется на сервер для поиска цитат с помощью асинхронных запросов Fetch API. async function processBlocks() { for (let block of blocks) { const response = await fetch("../duplicate_gk_rf_api", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: block.text }) }); const data = await response.json(); block.apiResults = data.results || []; } } 5. Динамическое выделение текста в зависимости от порога соответствия Svelte-реактивность позволяет пересчитывать выделение текста и комментариев при изменении ползунка. $: blocks.forEach(block => { const matches = block.apiResults.filter(r => r.distance > threshold); block.highlighted = matches.length > 0; block.comment = matches.map(r => `${r.article_number} ГК РФ (${(r.distance * 100).toFixed(0)}%)`).join("<br>"); }); 6. Копирование текста в буфер обмена Пользователь может скопировать весь видимый текст с помощью простой функции: async function copyAllTextToClipboard() { const visibleText = filteredBlocks.map(b => b.text).join("\n\n"); await navigator.clipboard.writeText(visibleText); }
Таким образом, с использованием минимального количества библиотек и подходов Svelte, любой из вас может самостоятельно повторить процесс разбора и отображения DOCX-файлов в удобной форме.
Как устроен бэкенд
Серверная часть запускается с помощью Serverless Function Vercel
1. Получение текста и валидация запроса Сервер принимает POST-запрос, проверяет его и извлекает текст запроса. export const POST = async ({ request }) => { const { query } = await request.json(); if (!query || query.trim() === "") { return new Response(JSON.stringify({ error: "Введите текст запроса." }), { status: 400 }); } } 2. Получение эмбеддинга от OpenAI Получение эмбеддинга (векторного представления) текста через API OpenAI. const openaiResponse = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }, body: JSON.stringify({ model: 'text-embedding-3-small', input: query }) }); const openaiData = await openaiResponse.json(); const embedding = openaiData.data[0].embedding; 3. Поиск похожих документов в Zilliz Сервер отправляет полученный эмбеддинг в Zilliz для поиска похожих документов в базе. const zillizResponse = await fetch('https://your-zilliz-endpoint/v2/vectordb/entities/search', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.ZILLIZ_API_TOKEN}`, 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ collectionName: "gk_rf", data: [embedding], limit: 5, outputFields: ["id", "article_number", "article_name", "point_text", "distance"] }) }); const zillizData = await zillizResponse.json(); 4. Возвращение результатов клиенту После успешного получения результатов от Zilliz сервер возвращает данные обратно клиенту. return new Response(JSON.stringify({ results: zillizData.data }), { status: 200, headers: { 'Content-Type': 'application/json' } });
Используя клиент-серверный подход, любой из вас сможет реализовать полноценный механизм для поиска и обработки информации в документах с помощью современных инструментов.
Как получил из ГК РФ набор эмбедингов
У меняуже был готовый json файл со статьями из ГК РФ. Чтобы не раздувать статью, здесь не буду останавливаться на том как я его сделал, а посто выложу его в открытый доступ на гитхаб. Далее просто расскажу как создать JSON с эмбеддингами и загрузить его в Zilliz для поиска похожих документов. Здесь у нас вход пойдет питон и его библиотеки.
Шаг 1: Установка библиотек Создаём виртуальное окружение и устанавливаем зависимости: pip install openai python-dotenv Шаг 2: Настройка API-ключей Создай файл .env и добавь свой ключ от OpenAI: OPENAI_API_KEY=your-openai-api-key Шаг 3: Подготовка текстов документов в JSON Создаем файл documents.json, где каждый документ содержит текстовые поля (например, статьи и пункты статей): { "Статья 1": { "name": "Основные положения", "points": [ "Пункт первый текста статьи", "Пункт второй текста статьи" ] }, "Статья 2": { "name": "Дополнительные положения", "points": [ "Ещё один пункт статьи" ] } } Шаг 4: Генерация эмбеддингов через OpenAI С помощью любой ЛЛМ за три минуты создаем просто скрипт для генерации эмбеддингов (в моем случае с помощью модели text-embedding-3-small): Создаем файл create_embeddings.py: import openai import json import os from dotenv import load_dotenv import time load_dotenv() openai.api_key = os.getenv("OPENAI_API_KEY") # Читаем исходные данные with open("documents.json", "r", encoding="utf-8") as f: documents = json.load(f) data_with_embeddings = {} for article_number, article_info in documents.items(): print(f"Обрабатываем {article_number}") points = [] for point in article_info["points"]: print(f"Создаём эмбеддинг для: {point[:30]}...") response = openai.embeddings.create( input=point, model="text-embedding-3-small" ) embedding = response.data[0].embedding points.append({ "text": point, "embedding": embedding }) time.sleep(1) # задержка для избежания лимита API data_with_embeddings[article_number] = { "name": article_info["name"], "points": points } # Сохраняем JSON с эмбеддингами with open("documents_with_embeddings.json", "w", encoding="utf-8") as f: json.dump(data_with_embeddings, f, ensure_ascii=False, indent=2) print("✅ Эмбеддинги готовы!") Запускаем скрипт: python create_embeddings.py Шаг 5: Подготовка JSON-файла для импорта в Zilliz Для импорта в Zilliz нужно создать плоский список записей. Делаем это также через скрипт написанный ЛЛМ: Создай файл prepare_for_zilliz.py: import json # Загружаем JSON с эмбеддингами with open("documents_with_embeddings.json", "r", encoding="utf-8") as f: data = json.load(f) records = [] for article_number, article_info in data.items(): for point in article_info["points"]: record = { "article_number": article_number, "article_name": article_info["name"], "point_text": point["text"], "embedding": point["embedding"] } records.append(record) # Сохраняем подготовленный JSON with open("zilliz_ready.json", "w", encoding="utf-8") as f: json.dump(records, f, ensure_ascii=False, indent=2) print("✅ JSON готов к загрузке в Zilliz!") Запускаем: python prepare_for_zilliz.py Теперь файл zilliz_ready.json можно загружать в Zilliz. Шаг 6: Создание коллекции в Zilliz Cloud Идём в Zilliz Cloud, создаём аккаунт и коллекцию с такой схемой: Field name Type Primary Key Description id INT64 (AutoID) ✅ Yes Автоинкрементный ID article_number VARCHAR (max 100) ❌ No Номер статьи article_name VARCHAR (max 500) ❌ No Название статьи point_text VARCHAR (max 5000) ❌ No Текст пункта embedding FLOAT_VECTOR (dim=1536) ❌ No Эмбеддинг Размерность (dim) эмбеддинга должна соответствовать модели OpenAI (1536). Обязательно нужно установить подходящий max_length для строк. В моем случае 5000 для абзацев ГК было вполне достаточно. Шаг 7: Загрузка данных в Zilliz Cloud Настройка базы данных не составит труда даже для новичка. Все опции доступны через веб-интерфейс Zilliz Cloud. Выбираете коллекцию. Нажимаете "Import Data". Загружаете файл zilliz_ready.json. Ждёте, пока данные импортируются. После чего делаете тестовый поиск в Zilliz. Проверить работу можно с помощью REST API Zilliz. Пример Python-скрипта: import requests zilliz_token = "your-zilliz-api-token" url = "https://your-zilliz-instance-url/v2/vectordb/entities/search" # Подставь сюда эмбеддинг своего запроса (получи его аналогично OpenAI) embedding = [0.12, 0.34, ...] payload = { "collectionName": "твоя-коллекция", "data": [embedding], "limit": 5, "outputFields": ["article_number", "article_name", "point_text"] } response = requests.post(url, json=payload, headers={ "Authorization": f"Bearer {zilliz_token}", "Content-Type": "application/json" }) print(response.json())
Теперь можно выполнять семантический поиск!
Нерешенные проблемы, которые возможно помогут мне решить ХАБРовчане
Я категорически не хочу использовать серверные решения для обработки docx, т.к. моя конечная цель — сохранение конфиденциальности информации. Это сильно ограничивает меня в выборе инструментов для работы с docx. Конвертация DOCX → HTML → правки → обратно в DOCX может вызвать проблемы с форматированием, особенно в сложных документах. Если нужно обеспечить качественное редактирование любого загруженного пользователем DOCX с сохранением форматирования, наиболее гибкий (но и требующий значительных усилий) подход — это разработка собственного модуля, основанного на распаковке (JSZip/PizZip) и парсинге/модификации XML (xml2js или fast‑xml‑parser). То что написано у меня сейчас решает проблему только частично. Документ нормально парсится и отображается на клиенте, но не выдает номера автоматических списков, что проблема для договорников, т.к. номера пунктов договора важны и часто они проставляются именно автоматическими списками word.
И вот тут собственно у меня вопрос к ХАБРовчанам, может быть кто‑то знает уже написанные решения, которые позволяют делать разборку и сборку docx качественно на клиенте? Или может быть у кого‑нибудь из вас есть уже написанная непубличная библиотека, которой вы готовы поделиться с юридическим сообществом?
Заключение
Вроде бы все рассказал. Если что‑то осталось непонятным, то не стесняйтесь задавать вопросы в комментариях, а лучше заюзайте какую‑нибудь доступную вам ЛЛМ, которая умеет в кодинг и она вам не только объяснит, но и напишет готовый код.
