Всем привет! Меня зовут Владимир Глебовец, также известный в среде юридического сообщества, как 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 качественно на клиенте? Или может быть у кого‑нибудь из вас есть уже написанная непубличная библиотека, которой вы готовы поделиться с юридическим сообществом?
Заключение
Вроде бы все рассказал. Если что‑то осталось непонятным, то не стесняйтесь задавать вопросы в комментариях, а лучше заюзайте какую‑нибудь доступную вам ЛЛМ, которая умеет в кодинг и она вам не только объяснит, но и напишет готовый код.