Как стать автором
Обновить

Как юрист с помощью вайбкодинга пилит в одного место для юридических экспериментов с ИИ

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров768

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

Заключение

Вроде бы все рассказал. Если что‑то осталось непонятным, то не стесняйтесь задавать вопросы в комментариях, а лучше заюзайте какую‑нибудь доступную вам ЛЛМ, которая умеет в кодинг и она вам не только объяснит, но и напишет готовый код.

Теги:
Хабы:
+8
Комментарии7

Публикации

Работа

Ближайшие события