Привет! Меня зовут Дмитрий Вдовин, я техлид команды Budget Tool. Мы отвечаем за внутренний продукт, через который в банке проходят процессы планирования и контроля расходов. Это система, где формируются бюджеты, согласуются изменения и фиксируются затраты. В ней много терминов: OPEX и CAPEX, кост-центры, группы расходов, аллокация, реаллокация — всё это требует понимания и времени на освоение.
Даже опытные пользователи — продукт-оунеры, техлиды, CTO, руководители, сотрудники кост-менеджмента — тратят много времени не на работу в системе, а на поиск информации. Источники разрозненные: Excel, письма, заметки, личные переписки. Отсюда родилась идея AI-ассистента — инструмента, который отвечает на вопросы обычным языком в одном месте.
Python — стандарт для AI, но наша команда, как и большинство в банке, работает на JVM: Kotlin, Java, Spring Boot. Мы решили не переходить на другой стек, а развивать AI в привычной экосистеме. Это не просто выбор технологии — мы хотели сохранить поддержку внутри команды и не привлекать новых специалистов, которых у нас нет.
Наш опыт может быть полезен другим командам в JVM-среде, которые хотят внедрить AI без смены стека. Тем более что условия у нас типичные: небольшая команда, без ML-инженеров и большого опыта в AI, но с желанием разобраться и запустить рабочее решение.
Для разработки мы рассмотрели три основные библиотеки:
- Spring AI — проект из экосистемы Spring;
- Koog — опенсорсный фреймворк от JetBrains, ориентированный на создание AI-агентов;
- LangChain4j — реализация подходов из Python-библиотек LangChain, Haystack и LlamaIndex.
Мы остановились на Spring AI — чтобы оставаться в единой экосистеме. Да, это пока молодой проект, и не всё в нём идеально. Порой возникает желание отказаться, но он активно развивается. Недавно вышла вторая версия в превью, что добавляет уверенности в выборе.
RAG и векторная база данных
Первая задача — научить ассистента отвечать на простые вопросы и объяснять внутренние термины. Когда речь о специфичных знаниях компании, лучший путь — RAG (Retrieval-Augmented Generation): генерация ответа на основе найденного контекста.
Схема проста:
- Документы заранее разбиваются на фрагменты — чанки;
- Чанки преобразуются в векторы и сохраняются в базе;
- При запросе система ищет релевантные чанки и передаёт их модели;
- Ответ формируется только на основе этого контекста.
В качестве хранилища мы выбрали PostgreSQL с расширением pgVector. Да, это не полноценная векторная база, и в других условиях логичнее было бы взять Milvus или Qdrant. Но нам не хотелось тратить время и ресурсы на их настройку и поддержку. Нужен был быстрый PoC — и pgVector в DBaaS идеально подошёл.
Выбор был не по производительности, а по скорости запуска и возможности быстро проверить гипотезу.
Первая проблема: данные
Первый прототип собрали за десять строк кода. Векторный поиск заработал сразу. Но качество ответов оказалось низким. Причина — в данных:
- Страницы в Confluence имели разную структуру;
- Некоторые содержали только картинки и ссылки;
- Часть информации была устаревшей;
- Много знаний хранилось «в головах» сотрудников.
AI-ассистенту просто неоткуда было брать контекст. Поэтому сначала пришлось решить не техническую, а организационную задачу.
Аналитики и продукт-оунер оформили отдельные страницы по работе с системой. Мы договорились о едином формате: логические блоки с заголовками второго уровня. Бизнес начал собирать на отдельных страницах определения, термины и частые вопросы. Фактически — готовить документацию так, чтобы её мог читать не только человек, но и AI.
Подход «скормить всё, что есть, и пусть AI сам разберётся» — заведомо проигрышный.
Решение в лоб: стандартный сплиттер
Мы начали с TokenTextSplitter из Spring AI — он разбивает текст на чанки по токенам, стараясь не резать посреди предложения. Но быстро выявили проблему: определения и правила часто разрывались между чанками. В итоговую выборку попадали не все части, и модель получала неполный контекст.
Мы пробовали увеличить размер чанков — но тогда в выборку попадало много лишнего. Пришлось искать баланс: чанк должен быть мал для точного поиска, но достаточно велик, чтобы сохранять смысл. Экспериментально нашли значение около 400 символов — оно лучше всего подошло под специфику наших данных.
Перекрытие чанков
Чтобы снизить риск потери контекста, применили перекрытие — конец одного чанка частично повторяется в начале следующего. Но в Spring AI у TokenTextSplitter нет встроенной поддержки overlap.
На помощь пришёл langchain4j с его DocumentSplitter, который поддерживает перекрытие. Мы добавили его в проект, хотя ради одной функции это выглядит избыточно. Но цель была — быстро проверить гипотезу.
Метод recursive(int maxSegmentSizeInChars, int maxOverlapSizeInChars) работает так:
- Сначала пытается разбить по абзацам;
- Если абзац слишком длинный — рекурсивно делит на предложения, слова, символы;
- Добавляет перекрытие между чанками.
Качество ответов улучшилось — модель чаще получала целые определения. Но появилась новая проблема: в один чанк могли попасть фрагменты из разных логических блоков. Контекст становился «грязным» — формально релевантным, но логически смешанным.
Секции как единица смысла
Мы решили использовать внутреннюю структуру документации. В Confluence логические блоки уже выделялись заголовками h2 — это и стало нашей единицей смысла.
Теперь документ делился на секции, а если секция была большой — она дополнительно разбивалась на чанки. Это дало прирост в качестве, но проблема осталась: при поиске могли вернуться не все части одной инструкции — например, первые и последние пункты, без центральных.
Мы пробовали:
- Добавлять название секции в каждый чанк;
- Забирать соседние чанки (на один-два вверх и вниз);
- Расширять итоговый контекст.
Но в какой-то момент контекст становился перегруженным: повторы, лишние фрагменты, шум. Модель теряла фокус.
Финальное решение: поиск по чанкам, ответ по секциям
Мы пришли к более простому и эффективному подходу:
- Выполняем векторный поиск по чанкам;
- Определяем, к каким секциям относятся найденные чанки;
- Передаём в модель полный текст этих секций.
Для этого в таблицу с векторами добавили метаинформацию: id страницы и id секции. Полный текст секций храним в отдельной таблице.
Алгоритм после поиска:
- Берём id секции каждого найденного чанка;
- Оставляем уникальный набор секций;
- Забираем из базы полные тексты этих секций.
Так поиск остаётся точным на уровне чанков, а контекст — логически цельным. Это устранило разрывы инструкций и сделало поведение ассистента стабильным и предсказуемым.
Время выходить в прод
Чтобы понять, как ассистент работает в реальности, нужно было переходить к эксплуатации. Мы запустили его с минимальным промптом, включающим:
- Историю переписки;
- Найденный контекст;
- Системный промпт.
За первые полгода пользователи отправили более 1500 сообщений и создали свыше 250 чатов. Это не гигантские цифры, но важно учитывать — продукт внутренний, с ограниченной аудиторией. Пик активности был в начале, вероятно, из-за внутренней «рекламы».
Мы настроили дашборд в Grafana, где отслеживали:
- Вопросы и ответы;
- Частые темы и повторяющиеся запросы;
- Случаи, когда ассистент не находил контекст или отвечал неуверенно.
Это стал наш главный источник обратной связи. Ассистент превратился не только в инструмент для пользователей, но и в «датчик качества» документации.
Почему данные важнее модели
Я намеренно не упоминал конкретную LLM. Потому что на нашей задаче разница между, например, qwen-32b и qwen-420b не имела значения. Мы сознательно ограничили модель — она не должна использовать свои знания. Её задача — искать ответ в предоставленных данных и формулировать его по правилам.
Практика показала: качество ответов зависит не от размера модели, а от качества и структуры базы знаний.
После запуска мы регулярно анализировали, на какие вопросы ассистент отвечал плохо. Дальше — рутина: смотрим диалог, идём в документацию, дополняем или уточняем формулировки. При необходимости перерабатываем структуру раздела.
Этот процесс нельзя автоматизировать. Его может выполнить только владелец предметной области. Без участия бизнеса устойчиво улучшать качество невозможно. Но после правок ассистент начинал отвечать лучше — без изменений в коде.
Заключение
В начале казалось, что сложность будет в выборе модели, библиотек и архитектуры — особенно в нестандартном для AI стеке. Но на деле самым трудоёмким оказалось не написание кода, а подготовка данных.
AI-ассистент не создаёт знания. Он отражает реальную зрелость документации. Каждый некорректный ответ — не ошибка модели, а пробел в описании предметной области. RAG оказался не магией, а способом структурировать и зафиксировать знания.
Чем точнее описана предметная область, тем стабильнее работает ассистент. А дальше — можно усложнять архитектуру, добавлять инструменты, маршрутизацию, тестирование и обратную связь.