Цель статьи — показать не абcтрактный пример как можно извлечь пользу из собственного RAG локально (если вдруг кто‑то ещё этого не сделал), и заодно посмотреть, какие основные параметры тут вообще доступны. Часто сидя на звонках я слышал от коллег: «вот хорошо бы сделать собственный RAG для Confluence», «вот бы развернуть у себя». И как будто бы навайбкодить такое решение — дело пары вечеров, но часто дальше разговоров дело не двигается.
В общем, закатав рукава, сделал свой собственный туллинг для поиска по Confluence. Какие были альтернативы: унылый поиск по Confluence через API — это сразу мимо. Можно было потыкаться с чем‑то типа mcp‑atlassian, но это тот же поиск по API, который ещё и токены ест — с него я стартовал, но по факту только смог проверить, что мой токен для подключения к Confluence работает, а результаты выдачи были не очень, в браузер лезть не надо — на этом всё, дальше пользы не увидел.
Сама работа с RAG включает следующие шаги:
Сканируем документ, разбиваем его на кусочки (чанки), строим эмбеддинги — для каждого чанка свой вектор, массив чисел — и сохраняем их в каком‑нибудь сторе.
Когда приходит запрос от пользователя, из запроса точно так же строится эмбеддинг.
Кандидатов ищем по косинусной близости между вектором вопроса и векторами чанков cosine similarity — у каждого кандидата есть
minScore, порог, ниже которого чанк в выдачу вообще не попадает. Из отобранных кандидатов через MMR‑переранжирование (балансирует релевантность и разнообразие, чтобы topK результатов не забился дублями одной длинной страницы) выбираются финальныеtopKчанков, которые пойдут в промпт.Дальше подставляем в промпт найденные чанки, а затем — запрос пользователя.
Итого на выходе получаем ответ именно по нашей базе знаний, с референсами, откуда он был собран.
Confluence: не тащим всё подряд
С точки зрения работы с Confluence важно сказать, что нам не нужен весь Confluence как таковой — достаточно тех пространств, с которыми мы реально работаем. Далее по мере необходимости — например, коллега скинул ссылку или открыли доступ в новый раздел — добавляем это пространство в список и переиндексируем. Технически это полная пересборка индекса по всему текущему списку пространств, а не инкрементальное добавление одного нового — но на масштабе в десятки‑сотни страниц это секунды, так что на практике ощущается как точечное добавление.
Проблема разметки
Из проблем, которые возникали по пути, можно выделить разметку документов. Confluence REST API отдаёт содержимое страницы в собственном XHTML‑формате («storage format») с макросами вида <ac:structured-macro ac:name="code">. Наивный подход — взять как есть, просто удалив стркутрные элементы, страница превращается в один сплошной абзац без всякой структуры. Вот что происходит с реальной страницей о работе с Kafka:
Инструкции по работе с Kafka.Общая информацияВ компании используют Managed Kafka в облаке.Есть кластера развернутые в - скорее всего с ними лучше не работать.Конфигурацию кластеров можно найти здесь: dev и production. Чтение данныхДля чтения данных из топиков можно воспользоваться инструментом kafkacat.Пример команды для чтения последнего сообщения из топика:bashkcat -C \ -b ${BROKER_ADDRESS}:9091 \ -t ${TOPIC} \ -X security.protocol=SASL_SSL...
Заголовки, ссылки и код слиплись в один поток — команда kcat буквально приклеилась к слову «bash» из соседнего абзаца. Для чанкинга (который режет по границам предложений/абзацев) и для модели (которая должна понимать, что перед ней команда терминала, а не связный текст) это прямой урон качеству.
Решается это написанием собственного конвертера, который делает обход DOM‑дерева и следующие преобразования: заголовки h1—h6 → #, ul/ol → списки, table → markdown‑таблица, pre/ac:structured-macro[name=code] → тройные бэктики, a[href] → [текст](ссылка).
Confluence‑специфичные макросы (ac:*, ri:*) разворачиваются в свой видимый текст, а не выбрасываются. Тот же конвертер переиспользуется и для HTML‑экспорта — только парсится не XML storage‑формат, а обычный отрендеренный HTML. В свой тул я добавил ещё поддержку HTML и markdown дополнительно — часть документации, те же архитектурные схемы и описания интеграций, хранится в git в md‑формате, а вытянуть какой‑то паттерн или описание сервиса из сотни доков стало до безобразия просто.
С какими параметрами можно поэксперементировать:
chunk-size,chunk-overlap— размер чанка и перекрытие (количество символов, которое определяет, насколько один чанк налезает на другой).min-score— порог косинусной близости: чанки хуже этого score в выдачу не попадают (отсекает нерелевантный шум).top-k— сколько ближайших чанков отдавать в LLM как контекст для ответа.mmr-lambda— баланс MMR‑переранжирования между релевантностью и разнообразием результатов: ближе к 1 — чистая релевантность, ближе к 0 — больше разнообразия (меньше дублирующих друг друга чанков).dimension— задаёт размерность колонки в таблице pgvector, должна совпадать с выходной размерностью embedding‑модели (уbge-m3это 1024).temperature— параметр отвечает за «креативность»; у себя для примера выбрал 0.2, потому что нам нужно опираться на факты, а не фантазировать.embedding-model— модель для построения эмбеддингов по чанкам.chat-model— модель для генерации ответов ассистента.
Пример реализации я опубликовал в репозитории. В README.md описал как, что запустить + добавил описание нескольких эндпоинтов, которые позволяют смотреть индекс и подбирать параметры руками — можно пощупать, что там в индексе на самом деле и как меняется выдача при изменении top-k/min-score/mmr-lambda, не гадая по логам. На выходе в итоге я получаю ссылки на документы и summary, что в общем‑то упрощает жизнь.
Из выводов: работать с RAG на Java с помощью LangChain4j достаточно приятно, само взаимодействие с LLM легко организовать, и есть множество вариантов улучшения взаимодействия с RAG. У себя локально я уже не пользуюсь Confluence как таковым.