Почему юридический AI‑сервис нельзя строить как обычный чат: проектируем case‑first SaaS на Go

Несколько месяцев назад мне понадобилось заключить договор с водоканалом. Казалось бы, обычная бюрократическая задача: отправил документы, дождался ответа, подписал договор. На практике всё оказалось значительно веселее — ответа долго не было, сроки шли, а что делать дальше, было не совсем понятно.
Юриста у меня не было, зато был AI. Я загрузил документы, описал ситуацию и начал разбираться, какие вообще есть варианты дальнейших действий. В итоге с помощью AI подготовил несколько обращений и жалоб, после чего ситуация сдвинулась с места и договор со мной всё‑таки заключили.
Самый очевидный вывод из этой истории мог бы звучать как «нейросеть помогла написать юридическую жалобу». Но мне, как backend‑разработчику, гораздо интереснее оказался другой вопрос: а как выглядел бы такой сервис, если превратить этот сценарий в полноценный продукт?
Когда я начал думать над архитектурой, довольно быстро пришёл к выводу, что обычный чат — плохая основа для подобной системы.
Юридическая проблема — это не история сообщений
Представим обычную ситуацию. Сегодня пользователь загружает заявление, через две недели получает ответ организации, ещё через месяц пишет жалобу в контролирующий орган, а потом добавляет новые документы. За это время меняются обстоятельства, появляются новые факты, истекают сроки, а старые документы продолжают влиять на последующие действия.
Если хранить всё это только как последовательность сообщений:
User
└── Chat
└── Messagesто довольно быстро возникает проблема. Для каждого нового вопроса приходится снова передавать модели огромный контекст и надеяться, что она правильно восстановит историю, не перепутает даты и не потеряет важную деталь где‑нибудь между двадцатью сообщениями.
По сути, пользователь ведёт уже не разговор, а дело, состояние которого постепенно изменяется. Поэтому основной сущностью приложения мне кажется логичнее сделать Case, а чат оставить только одним из способов взаимодействия с ним.
Архитектурно это может выглядеть примерно так:
User
└── Case
├── Participants
├── Documents
├── Facts
├── Timeline
├── Legal Sources
├── Actions
├── Drafts
└── MessagesТакое изменение кажется небольшим, но на самом деле сильно влияет на остальную архитектуру. Новый документ становится не очередным вложением в сообщении, а событием, которое может изменить состояние дела.
Например, пользователь загружает ответ организации. Система должна определить тип документа, дату, отправителя, связать его с предыдущими событиями, извлечь новые факты и понять, изменился ли возможный следующий шаг. И только после этого имеет смысл обращаться к LLM за интерпретацией.
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 для законов тоже хочется делать не совсем стандартно
Во многих примерах 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», мне не хочется.
Архитектуру MVP хочу сделать максимально скучной
Основной язык у меня 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, а не один prompt
Ещё одна вещь, от которой хочется отказаться, — один огромный запрос в духе:
Вот двадцать документов. Ты опытный юрист. Проанализируй их и скажи, что делать.
Мне гораздо больше нравится 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 valuesMapping между PERSON_1 и реальным человеком при этом остаётся внутри инфраструктуры приложения.
Понятно, что качественная анонимизация — отдельная большая задача и в MVP она наверняка будет реализована не идеально. Но саму архитектурную границу между исходными документами и внешним LLM‑провайдером хочется заложить сразу.
Зачем вообще такой сервис, если уже есть ChatGPT?
Это главный вопрос, который я задавал себе ещё до написания первой строчки кода.
Если продукт выглядит так:
textarea → LLM API → textareaто, скорее всего, такой продукт действительно не нужен. Пользователь просто откроет ChatGPT, Claude или другую модель и получит примерно тот же результат.
Ценность специализированного сервиса должна находиться выше уровня самой LLM.
Например, система может помнить историю одного дела в течение нескольких месяцев, связывать факты с исходными документами и отслеживать изменение ситуации после появления новых материалов. Она может показывать, на каком именно нормативном источнике основан вывод, и не давать модели свободно придумывать статьи законов.
Но самое важное — сервис должен отвечать не столько на вопрос пользователя, сколько на вопрос «что мне теперь делать?»
То есть результатом работы становится не очередное сообщение в чате, а изменение состояния дела и понятный следующий шаг: дождаться ответа, запросить дополнительный документ, подготовить претензию, сформировать обращение или загрузить новый ответ организации.
Чем больше работаю с 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 на первом этапе.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.