TECH 6 хв

Server-side evidence extraction: як ми винесли аналіз доказів на бекенд

Фронтенд парсив докази з тексту відповіді regex-ами — мобільний Safari зависав на секунду. Ми перенесли витяг доказів на бекенд, додали SSE-подію evidence, і тепер клієнт просто рендерить готові об\

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-шар робить міграцію безпечною: навіть якщо серверна екстракція тимчасово недоступна, користувач побачить докази. Просто трохи повільніше.