От каши к структуре: гибридная AI-система для обработки свободного текста

От каши к структуре: гибридная AI-система для обработки свободного текста

Я разрабатываю систему под названием Svyazi — инструмент для структурирования и поиска по профилям участников сообщества. Основная задача — из свободных текстов извлекать структурированные данные и делать это непрерывно, по мере поступления. За несколько месяцев я перепробовал регулярные выражения, чистый LLM и пришёл к гибридному решению. Ниже — архитектура, промпты, трудности и неочевидные решения.

Стек: Python 3.12, Ollama + Qwen 2.5 (всё локально), YAML для хранения, SHA256 для дедупликации, Jinja2 для шаблонизации промптов.

Откуда взялась задача

В моём сообществе участники часто ищут коллег: «нужен фронтендер», «кто работал с Kubernetes?», «ищу партнёра в стартап». При этом представления — от двух предложений до развёрнутых автобиографий. Со временем накопилось более двухсот таких текстов. Искать по ним стало невозможно.

Привет! Я Алексей, занимаюсь бэкендом уже лет 7. Основной стек — Go и Python, последние два года плотно сижу на k8s. До этого работал в Яндексе и одном финтех-стартапе (NDA, не могу называть). Есть опыт с ML — делал рекомендательную систему, но это скорее хобби-проект. Ищу интересные проекты, можно парт-тайм. Английский — upper intermediate.

Дизайнер, 10 лет. Figma, немного code. Spark AR.

Попробуйте найти всех Go-разработчиков с Kubernetes и базовым ML. Руками — часы. А если профилей тысяча?

Ключевая идея решения

  • Глубокий профиль, а не визитка. Храним не просто «Иван, Python-разработчик», а стек с уровнями, реальные проекты, выступления, мягкие навыки.
  • Гибрид LLM + детерминированный код. LLM извлекает смысл, код нормализует результат. Каждый делает своё.
  • Двухэтапный скоринг. Быстрый фильтр по индексу → LLM только для шорт-листа. Экономит время и ресурсы.
  • Privacy by design. Персональные данные фильтруются на входе и не попадают в обработку.

Почему глубокий профиль важнее каталога

Разница между «Иван, Python-разработчик» и человеком, которого действительно знаешь, — огромная. Глубокая структура позволяет находить неочевидные связи.

Например, система случайно обнаружила, что участница с 15-летним опытом в Wi-Fi-инженерии и разработчик из Петербурга могут сотрудничать. Был сгенерирован open-source проект по радиопланированию Wi-Fi — с описанием ролей и дорожной картой. До этого они не знали друг о друге.

Путь был тернист: регулярки и их провал

Сначала я пробовал регулярные выражения. Проблема — люди пишут по-разному: «5 лет на Python», «работаю с питоном с 2019», «делал кучу всего на пайтоне, потом перешёл на Go».

Регулярки пропускали слишком много. Сейчас, в эпоху LLM, такой подход бессмысленен.

Чистый LLM: три главные проблемы

  • Неконсистентность: одна технология возвращается как golang, Go, Golang, Go (Golang).
  • Галлюцинации: «делал проект на Spark» → Apache Spark, хотя имелся в виду Spark AR.
  • Нестабильный формат: ответы с комментариями, текстом до и после, markdown-обёртками — всё это ломает парсер.

Вывод: LLM извлекает смысл, но детерминированный код должен приводить результат к единому виду.

Архитектура: 6 слоёв

Система разделена на независимые модули. Каждый отвечает за одну задачу. Если ломается один — остальные не затрагиваются.

YAML → [Import] → [AI Processing] → [Normalization] → [Indexing] → [Пред-скоринг] → [Семантический поиск]

  • 1. Import: парсинг YAML, вырезание персданных, дедупликация по SHA256, создание карточки в статусе pending.
  • 2. AI Processing: отправка текста в LLM, получение JSON, retry при ошибках, dead letter queue.
  • 3. Normalization: синонимы → канон, классификация по достоверности, стандартизация форматов.
  • 4. Indexing: раскладка по индексам — навыки, роли, общий.
  • 5. Пред-скоринг: детерминированная фильтрация, формирование шорт-листа.
  • 6. Семантический поиск: LLM-скоринг по шорт-листу, ранжирование, выдача результата.

Сквозной пример обработки

Слой 1: текст Алексея попадает в систему. Вырезаются персональные данные — Telegram, email, ссылки. Считается SHA256. Если хеш уже есть — профиль пропускается. Это предотвращает дубли.

Слой 2: обезличенный текст отправляется в Qwen 2.5. Модель возвращает JSON. Пример проблемы: указывает k8s вместо kubernetes, а 7 лет опыта на Go — потому что человек 7 лет в бэкенде, но не сказано, что именно на Go.

Слой 3: детерминированный код нормализует данные:

  • k8skubernetes (по справочнику синонимов)
  • Gogo (канонизация в lowercase)
  • years: 7null (помечено как inferred)
  • ML levelhobby (по контексту «хобби-проект»)

CardIndex — сердце системы

CardIndex — единый источник правды. Он отслеживает состояние всех карточек: статус, дедупликацию, очередь, историю изменений. Без него система начинает плодить дубли и терять согласованность.

Промпт-инжиниринг

Промпт не статичен. Он менялся более десяти раз и будет меняться дальше. Хранится в .md-файлах, подставляется через Jinja2. Это код — его нужно версионировать и тестировать.

Три основные проблемы с промптами:

  • «Додумывание»: модель ставит уровень senior или middle без явных оснований.
  • Путаница «упомянул» и «работает»: «коллеги использовали Terraform, но я в это не лез» → навык Terraform с уровнем junior.
  • JSON-мусор: комментарии, текст вокруг JSON, markdown. Требуется отдельный слой очистки.

Нормализация: три детерминированных этапа

  • Синонимы → канон: справочник skills_synonyms.yml вырос с 30 до почти 100 строк. AI-агент периодически дополняет его.
  • Классификация по достоверности:
    • verified — навык точно подтверждён
    • claimed — заявлен, но нет в справочнике
    • inferred — додуман моделью, требует модерации
  • Стандартизация форматов: «Yandex», «Яндекс» → единый канон. Даты — в ISO.

Новые навыки не удаляются, а накапливаются. Когда частота ростёт — это сигнал: возможно, появился тренд.

Двухэтапный поиск

Запрос: «Go-разработчик, senior, Kubernetes, удалёнка».

Этап 1 — пре-фильтр: детерминированная проверка по индексу. Быстро, дёшево. Алексей попадает в шорт-лист.

Этап 2 — семантический скоринг: LLM оценивает нюансы. Например, «хобби-ML» может быть плюсом для ML-вакансии, но не нужен чистому бэкендеру.

Зачем два этапа? Зачем прогонять 200 профилей через LLM, если 170 отсеиваются по базовым критериям?

Грабли, на которые наступал

  • Недетерминированность: даже при temperature=0 результаты могут отличаться. Решение: few-shot примеры, валидация по JSON-схеме, retry до трёх раз. После — ручной разбор.
  • Галлюцинации: «руководил волонтёрским проектом» → team_lead. Решение: поле confidence, модерация всех inferred-значений.
  • Скорость: Qwen 2.5 обрабатывает профиль за 120–200 секунд. Помогают:
    • Кэширование по хешу
    • Инкрементальная обработка
    • Оптимизация промптов (сократил с полутора страниц)
    • Очередь — можно идти пить чай, система сама дойдёт до всех
  • Приватность: всё локально. Персданные вырезаются на этапе импорта. Модель видит только обезличенный текст.

Итоги и выводы

  1. AI + детерминированность: LLM понимает текст, но требует нормализации после себя.
  2. Промпт — это код: версионировать, тестировать, итерировать.
  3. CardIndex обязателен: без единого источника правды система теряет согласованность.
  4. Discovery — обратная связь от данных: не выкидывайте неизвестное, анализируйте накопленное.
  5. Контроль остаётся за человеком: модерация inferred-навыков и пополнение справочников — ручная работа, но она предотвращает мусор в данных.

Qwen 2.5 справляется достойно, но уступает GPT-4o по точности. Компенсирую это более жёсткой нормализацией.

Что дальше

Планирую расширять справочники и углублять профили. Недавно внедрил экспериментальную функцию — персональные медиа-отчёты для участников: темы выступлений, форматы мастер-классов, карточки коллабораций. Первые результаты уже есть — включая историю с Wi-Fi-инженером и питерским разработчиком.

Читать оригинал