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

от автора

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

Заключение

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


ссылка на оригинал статьи https://habr.com/ru/articles/913086/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *