LEX — AI Legal Platform for Law Firms

AI-powered legal analysis platform for law firms and corporate counsel.

Features

Resources

Blog Articles

Technology

Built on AWS (EC2, Bedrock Claude AI, ALB, WAF, S3, ACM, KMS). PostgreSQL, Redis, Qdrant vector database. TypeScript, React, Node.js.

Start free — 50 credits on registration. Sign up

TECH 6 мин

Server-side evidence extraction: как мы вынесли анализ доказательств на бэкенд

Фронтенд парсил доказательства из текста ответа regex-ами — мобильный Safari зависал на секунду. Мы перенесли извлечение доказательств на бэкенд, добавили SSE-событие evidence, и теперь клиент просто рендерит готовые объекты. Время до первого доказательства: с 2.1с до 0.8с.

Server-side evidence extraction: как мы вынесли анализ доказательств на бэкенд

Когда парсинг на клиенте перестал справляться — мы перенесли разбор доказательств туда, где ему место.


Проблема

LEX AI возвращает пользователю не просто текст. Каждый ответ содержит доказательства: фрагменты судебных решений, статьи законодательства, выдержки из документов. Раньше весь этот поток приходил как единый текстовый блок, и фронтенд должен был самостоятельно разбирать его на структурированные карточки.

На десктопе это работало приемлемо. На мобильных устройствах — нет.

Симптомы, которые мы наблюдали:

Проблема Причина
UI freezes на 300-800 мс Парсинг больших ответов блокировал main thread
Неправильное выделение доказательств Regex-эвристики не покрывали все форматы
Дублирование логики Каждый клиент (веб, мобайл, MCP) писал свой парсер
Ухудшение при масштабировании Чем больше доказательств — тем медленнее рендер

Когда ответ содержал 15-20 доказательств (типичная ситуация для анализа судебной практики), мобильный Safari просто зависал на секунду. Пользователи это замечали.

Архитектурное решение

Вместо того, чтобы оптимизировать клиентский парсер, мы поставили вопрос иначе: зачем вообще парсить на клиенте то, что бэкенд уже знает?

Когда ChatService вызывает инструменты (search_court_decisions, get_legislation_section, vault_search), он получает структурированные данные. Потом LLM генерирует текстовый ответ, а клиент пытается из текста извлечь обратно ту же структуру. Это лишний цикл.

Решение: бэкенд извлекает доказательства во время генерации ответа и отправляет их отдельными SSE-событиями.

Поток данных: до и после

Раньше:

Backend: LLM генерирует текст с доказательствами вперемешку
   -> SSE: answer (один большой блок)
   -> Frontend: regex-парсинг, построение карточек
   -> Рендер

Теперь:

Backend: LLM генерирует текст
   -> EvidenceExtractor классифицирует tool_result
   -> SSE: evidence { type, title, source, content, relevance_score }
   -> SSE: answer (чистый текст без встроенных доказательств)
   -> Frontend: рендер готовых объектов

SSE-протокол

Мы расширили существующий SSE-поток новым событием evidence. Полный набор событий теперь выглядит так:

Событие Назначение Payload
thinking Индикатор обработки { stage: string }
tool_result Результат вызова инструмента { tool, result, cost }
evidence Структурированное доказательство { type, title, source, content, relevance_score }
answer Текстовый фрагмент ответа { delta: string }
complete Завершение потока { total_cost, evidence_count }

Объект evidence имеет чёткую типизацию:

interface EvidenceBlock {
  type: 'court_decision' | 'legislation' | 'document' | 'legal_position';
  title: string;
  source: string;
  content: string;
  relevance_score: number;
}

Поле relevance_score (0-1) позволяет фронтенду сортировать доказательства по релевантности и сворачивать менее важные по умолчанию.

Извлечение доказательств на бэкенде

EvidenceExtractor работает на этапе обработки tool_result. Когда ChatService получает результат от инструмента, он передаёт его в экстрактор до того, как LLM начнёт генерировать финальный ответ.

Для классификации (court_decision vs legislation vs document) мы используем LLM на уровне quick-модели (gpt-4o-mini). Это добавляет 50-100 мс на доказательство, но экономит значительно больше на клиенте и гарантирует корректную классификацию.

Критический момент: экстракция происходит параллельно с генерацией ответа. Пока LLM пишет текст, доказательства уже летят к клиенту. Пользователь видит карточки в EvidencePanel ещё до завершения текстового ответа.

Fallback-механизм

Мы не удалили клиентский парсер. Он остался как fallback:

if (receivedEvidenceEvents.length > 0) {
  // Используем серверные доказательства
  renderStructuredEvidence(receivedEvidenceEvents);
} else {
  // Fallback: парсим из текста ответа
  const extracted = parseEvidenceFromText(fullAnswer);
  renderStructuredEvidence(extracted);
}

Это защищает от трёх сценариев: бэкенд ещё не обновлён (постепенный деплой), экстрактор упал с ошибкой, соединение разорвалось посреди потока и evidence-события потерялись.

Результаты

Метрика До После
Время до первого доказательства в UI 2.1 сек 0.8 сек
Main thread blocking (мобайл) 300-800 мс < 50 мс
Корректность классификации ~82% ~96%
Размер клиентского бандла baseline -4 KB (удалённые regex-паттерны)

Наибольший выигрыш — на мобильных. UI jank практически исчез, потому что фронтенд больше не занимается тяжёлым парсингом. EvidencePanel просто рендерит готовые объекты.

Выводы

Эта миграция подтвердила принцип, которого мы придерживаемся в LEX AI: данные должны структурироваться как можно ближе к источнику. Бэкенд знает, что он вернул из инструмента. Заставлять клиент догадываться об этом из текста — это архитектурный долг, который мы наконец закрыли.

Fallback-слой делает миграцию безопасной: даже если серверная экстракция временно недоступна, пользователь увидит доказательства. Просто немного медленнее.