Почему юридический AI‑сервис нельзя строить как обычный чат: проектируем case‑first SaaS на Go
- суббота, 19 сентября 2026 г. в 00:00:06
Несколько месяцев назад мне понадобилось заключить договор с водоканалом. Казалось бы, обычная бюрократическая задача: отправил документы, дождался ответа, подписал договор. На практике всё оказалось значительно веселее — ответа долго не было, сроки шли, а что делать дальше, было не совсем понятно.
Юриста у меня не было, зато был AI. Я загрузил документы, описал ситуацию и начал разбираться, какие вообще есть варианты дальнейших действий. В итоге с помощью AI подготовил несколько обращений и жалоб, после чего ситуация сдвинулась с места и договор со мной всё‑таки заключили.
Самый очевидный вывод из этой истории мог бы звучать как «нейросеть помогла написать юридическую жалобу». Но мне, как backend‑разработчику, гораздо интереснее оказался другой вопрос: а как выглядел бы такой сервис, если превратить этот сценарий в полноценный продукт?
Когда я начал думать над архитектурой, довольно быстро пришёл к выводу, что обычный чат — плохая основа для подобной системы.
Представим обычную ситуацию. Сегодня пользователь загружает заявление, через две недели получает ответ организации, ещё через месяц пишет жалобу в контролирующий орган, а потом добавляет новые документы. За это время меняются обстоятельства, появляются новые факты, истекают сроки, а старые документы продолжают влиять на последующие действия.
Если хранить всё это только как последовательность сообщений:
User └── Chat └── Messages
то довольно быстро возникает проблема. Для каждого нового вопроса приходится снова передавать модели огромный контекст и надеяться, что она правильно восстановит историю, не перепутает даты и не потеряет важную деталь где‑нибудь между двадцатью сообщениями.
По сути, пользователь ведёт уже не разговор, а дело, состояние которого постепенно изменяется. Поэтому основной сущностью приложения мне кажется логичнее сделать Case, а чат оставить только одним из способов взаимодействия с ним.
Архитектурно это может выглядеть примерно так:
User └── Case ├── Participants ├── Documents ├── Facts ├── Timeline ├── Legal Sources ├── Actions ├── Drafts └── Messages
Такое изменение кажется небольшим, но на самом деле сильно влияет на остальную архитектуру. Новый документ становится не очередным вложением в сообщении, а событием, которое может изменить состояние дела.
Например, пользователь загружает ответ организации. Система должна определить тип документа, дату, отправителя, связать его с предыдущими событиями, извлечь новые факты и понять, изменился ли возможный следующий шаг. И только после этого имеет смысл обращаться к LLM за интерпретацией.
Допустим, из загруженных документов система уже знает, что 12 января пользователь направил заявление, а 15 января организация его получила.
Нет никакого смысла при каждом последующем запросе снова просить модель искать эти даты во всех документах. После первого извлечения такие сведения должны стать обычными данными приложения.
Например:
{ "type": "application_received", "occurred_at": "2026-01-15", "organization": "ORG_1", "source_document_id": "doc_42" }
Здесь для меня особенно важен source_document_id. Система должна хранить не только сам факт, но и его происхождение.
Если приложение утверждает, что организация получила заявление 15 января, я хочу иметь возможность открыть документ и увидеть, откуда именно взялась эта дата. Это позволяет провести важную границу между двумя вещами: что содержится в документах и как модель эти документы интерпретирует.
В обычном AI‑чате эти два слоя легко смешиваются. В прикладной системе я бы, наоборот, старался максимально их разделять.
Современная LLM уже достаточно хорошо умеет написать убедительно выглядящую претензию, заявление или жалобу. Сам по себе красивый текст поэтому не кажется мне большим конкурентным преимуществом.
Гораздо сложнее ответить на другой вопрос: почему система вообще решила, что пользователю сейчас нужно писать именно этот документ?
Наивная схема выглядит примерно так:
User question ↓ LLM ↓ Legal recommendation
Фактически мы в таком случае используем веса модели как юридическую базу знаний. В памяти модели могут находиться нужные нормы, старые редакции документов, комментарии из интернета, а иногда и нормы, которых вообще не существует.
Для приложения, которое работает с юридическими вопросами, такой подход выглядит слишком рискованно. Поэтому я хочу строить цепочку иначе:
Case ↓ Facts ↓ Legal retrieval ↓ Verified sources ↓ LLM ↓ Recommendation
Сначала система понимает факты конкретного дела, затем ищет подходящие нормативные источники и только после этого передаёт их модели для анализа.
Отдельная проблема — цитирование нормативных актов. Мне не хочется просить LLM самой написать что‑нибудь вроде «согласно статье X федерального закона Y».
Вместо этого модель должна получать уже найденные системой источники:
SOURCE_12 Федеральный закон ... Статья ... Текст нормы ... SOURCE_19 Постановление ... Пункт ... Текст нормы ...
После анализа модель возвращает структурированный ответ:
{ "recommendation": "Подготовить обращение в ...", "sources": [ "SOURCE_12", "SOURCE_19" ] }
А уже backend преобразует SOURCE_12 в название нормативного акта, номер статьи и ссылку на источник.
Это не делает вывод автоматически правильным. Ошибки всё равно возможны: можно подобрать нерелевантную норму, неверно интерпретировать её или упустить особые обстоятельства конкретной ситуации. Но по крайней мере система перестаёт позволять модели свободно изобретать нормативные основания.
Если модель ссылается на SOURCE_42, а такого источника в контексте нет, ответ можно отклонить ещё до показа пользователю.
Во многих примерах RAG предлагается взять документ и нарезать его на фрагменты примерно одинакового размера:
chunk_size = 1000 tokens
Для нормативных документов такой подход кажется мне странным, потому что у них уже существует собственная структура. Есть главы, статьи, части, пункты и подпункты.
Вместо уничтожения этой структуры произвольными границами токенов я бы предпочёл сохранить её в базе.
Например:
legal_sources ------------- id number title revision_date valid_from valid_to source_url
И отдельно:
legal_sections -------------- id source_id article part paragraph subparagraph text
Тогда поиск можно выполнять по отдельным структурным элементам, но при этом всегда понимать, из какого документа и места взят найденный фрагмент.
Причём для первой версии я пока даже не уверен, что здесь необходима отдельная vector database. Если MVP работает с ограниченным набором заранее проверенных нормативных источников, может оказаться достаточно PostgreSQL Full Text Search.
Если позже данных станет много, можно добавить pgvector и семантический поиск. Но делать это только потому, что «у нас AI‑проект, значит нужен vector DB», мне не хочется.
Основной язык у меня Go, поэтому backend первой версии я собираю как обычный модульный монолит.
Примерно так:
Web UI │ ▼ Go Backend │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ PostgreSQL MinIO LLM Gateway │ │ ▼ ▼ Legal Knowledge LLM Provider Base
Плюс будет отдельный worker, использующий ту же кодовую базу и ту же доменную модель.
Структура репозитория получается примерно такой:
cmd/ api/ worker/ internal/ auth/ case/ document/ legal/ ai/ draft/ jobs/ storage/
Я специально не хочу начинать с микросервисов, Kafka и Kubernetes. Возможно, когда‑нибудь они действительно понадобятся, но сначала хотелось бы получить хотя бы первых пользователей, которым вообще нужен продукт.
Для MVP гораздо полезнее потратить время на качество обработки документов, восстановление фактов и работу с нормативными источниками, чем заранее проектировать инфраструктуру для нагрузки, которой пока не существует.
Ещё одна вещь, от которой хочется отказаться, — один огромный запрос в духе:
Вот двадцать документов. Ты опытный юрист. Проанализируй их и скажи, что делать.
Мне гораздо больше нравится pipeline из небольших этапов:
Upload ↓ Extract text ↓ Extract facts ↓ Update timeline ↓ Classify case ↓ Find missing information ↓ Retrieve legal sources ↓ Generate possible actions ↓ Generate draft ↓ Verify
Например, первый вызов модели может вообще ничего не знать о праве. Его задача — только извлечь структурированные факты.
Пример результата:
{ "organizations": [ "ORG_1" ], "events": [ { "date": "2026-01-12", "type": "application_sent" }, { "date": "2026-01-15", "type": "application_received" } ], "unknown_facts": [] }
Следующий этап определяет категорию проблемы. Ещё один занимается поиском нормативных источников. И только после этого отдельный вызов LLM формирует возможные действия.
Такой подход требует больше кода, но зато каждый этап проще наблюдать, тестировать и заменять.
После генерации документа я хочу добавить ещё один этап проверки.
Verifier получает сформированный документ, известные факты дела и нормативные источники. Его задача — найти утверждения, которые ничем не подтверждаются, неверные ссылки, противоречия и отсутствующие данные.
Например:
{ "unsupported_claims": [], "invalid_sources": [], "contradictions": [], "missing_information": [] }
Конечно, LLM, проверяющая результат другой LLM, не превращает систему в формально верифицированную. Вторая модель тоже может ошибаться.
Но здесь важен сам принцип: первый сгенерированный текст не считается истиной по умолчанию.
Причём часть проверок вообще можно делать без LLM. Например, существование source_id, даты документов, ФИО участников и другие структурированные значения гораздо надёжнее проверять обычным кодом.
Чем дальше я думал про такой сервис, тем очевиднее становилось, что юридические документы — довольно неприятный тип данных для внешних AI API.
В них могут находиться ФИО, адреса, номера договоров, лицевые счета, телефоны, паспортные данные и подписи. Поэтому идея просто отправлять каждый загруженный PDF целиком во внешний API мне не нравится.
Архитектурно хочется иметь промежуточный слой:
Original document ↓ Local processing ↓ PII detection ↓ Иван Иванов → PERSON_1 Адрес → ADDRESS_1 Лицевой счёт → ACCOUNT_1 ↓ External LLM ↓ Restore values
Mapping между PERSON_1 и реальным человеком при этом остаётся внутри инфраструктуры приложения.
Понятно, что качественная анонимизация — отдельная большая задача и в MVP она наверняка будет реализована не идеально. Но саму архитектурную границу между исходными документами и внешним LLM‑провайдером хочется заложить сразу.
Это главный вопрос, который я задавал себе ещё до написания первой строчки кода.
Если продукт выглядит так:
textarea → LLM API → textarea
то, скорее всего, такой продукт действительно не нужен. Пользователь просто откроет ChatGPT, Claude или другую модель и получит примерно тот же результат.
Ценность специализированного сервиса должна находиться выше уровня самой LLM.
Например, система может помнить историю одного дела в течение нескольких месяцев, связывать факты с исходными документами и отслеживать изменение ситуации после появления новых материалов. Она может показывать, на каком именно нормативном источнике основан вывод, и не давать модели свободно придумывать статьи законов.
Но самое важное — сервис должен отвечать не столько на вопрос пользователя, сколько на вопрос «что мне теперь делать?»
То есть результатом работы становится не очередное сообщение в чате, а изменение состояния дела и понятный следующий шаг: дождаться ответа, запросить дополнительный документ, подготовить претензию, сформировать обращение или загрузить новый ответ организации.
Наверное, это пока главный инженерный вывод из этого pet‑проекта.
Сначала AI‑приложение очень легко представить так:
LLM ├── память ├── база данных ├── поиск ├── business logic ├── domain expert └── source of truth
Это очень удобно для прототипа. Можно сделать впечатляющую демку за вечер.
Но по мере появления требований мне всё больше нравится другая схема:
Database Search Rules LLM Verifier
Здесь модель становится одним из компонентов системы. Она отлично работает там, где обычный код работает плохо: понимает неструктурированный текст, извлекает сущности, классифицирует документы, обобщает информацию и помогает сформировать человеческий текст.
При этом даты, документы, участники дела, нормативные источники и другие проверяемые данные остаются обычными сущностями backend‑приложения.
Сейчас я собираю первую версию проекта как pet project с возможностью позже превратить его в небольшой SaaS.
Первый сценарий специально беру очень узким: бытовые споры и официальная переписка физического лица с организациями. В качестве одного из тестовых кейсов как раз буду использовать ту историю с водоканалом, с которой всё началось.
Технически проект даёт возможность поработать с Go, PostgreSQL, object storage, document processing, structured output, RAG и orchestration нескольких LLM‑вызовов. В дальнейшем сюда естественно добавляются очереди обработки, observability, versioning нормативной базы и более сложная работа с приватностью.
Но мне интереснее проверить не технологии. Главный вопрос гораздо проще:
можно ли сделать AI‑продукт, в котором пользователь получает пользу, но при этом ему не приходится слепо верить модели на слово?
Если проект дойдёт до рабочего MVP, в следующих материалах хочу отдельно разобрать реализацию хранения Case, pipeline обработки документов, RAG по нормативным источникам и то, насколько жизнеспособной окажется идея использовать PostgreSQL вместо отдельной vector database на первом этапе.