Создание помощника Slack RAG без отдельной векторной базы данных
Помощник Slack RAG без отдельной векторной базы данных: документы хранятся в одной таблице StarRocks, поиск осуществляется с помощью обычного SQL, а шесть уроков посвящены формулировке запросов и разбиению на части.
Источники: Lessons We Learned Building a RAG Assistant Without a Separate Vector Database
StarRocks — это была open-source аналитическая база данных с единым движком выполнения, гибкой возможностью развертывания и высокой производительностью при обработке реальных нагрузок. По мере роста сообщества увеличивалась и нагрузка по поддержке: разработчики постоянно получают одни и те же вопросы о документации, задачах в GitHub, записях об обновлениях и старых тредах в Slack.
Rocky — это помощник в Slack, созданный для снижения этой нагрузки. Его задача проста: избавить разработчиков от повторяющихся вопросов и ответов, при этом каждый ответ основывается на документации проекта и соответствующих источниках.
Интересна архитектура. Rocky работает на самой StarRocks. Фрагменты документации, поиск по ключевым словам, векторный поиск и оценка сходства хранятся в одной OLAP-таблице, поэтому помощник, отвечающий на вопросы о базе данных, также работает на той же базе данных. Всё это состоит примерно из 600 строк кода на Python, одной таблицы и ключа API Gemini.
Основы RAG
Обычным первым шагом при создании систем генерации с улучшением с помощью поиска является использование специализированной векторной базы данных. Каждый дополнительный компонент требует отдельной настройки, плана резервного копирования и собственной модели целостности, и для небольшого бота сообщества такая нагрузка кажется неоправданной.
Поэтому всё осталось в StarRocks. Фрагменты документов и их 768-мерные векторные представления Gemini хранятся в обычной OLAP-таблице с использованием модели PRIMARY KEY, а поиск осуществляется с помощью SQL-запроса. Принцип прост: если ваша аналитическая база данных уже поддерживает столбцы-массивы и функции косинусного сходства, вам не нужна дополнительная система для векторного поиска.
Таблица выглядит как любая другая:
CREATE TABLE docs (
id BIGINT NOT NULL,
path VARCHAR(512),
`index` INT,
`text` STRING,
vector ARRAY<FLOAT> -- 768-dim Gemini embedding
) ENGINE = OLAP
PRIMARY KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1;
Поиск осуществляется с помощью одного запроса на основе косинусного сходства:
SELECT path, `index`, `text`,
approx_cosine_similarity([0.012, -0.034, ...], vector) AS similarity
FROM docs
ORDER BY similarity DESC
LIMIT 8;
Нет SDK, нет библиотеки клиента и нет дополнительной системы. С точки зрения Rocky, «база векторных данных» — это просто ещё одна таблица, которую он уже может обрабатывать.
От события в Slack до конкретного ответа
Процесс специально сделан максимально простым. Упоминание @Rocky в Slack запускает бота, который с помощью встроенных функций передаёт задачу на обработку модели Gemini 3 Flash. Модель решает, следует ли ей искать информацию в документации или запрашивать данные у Google, и за один раз может выполнить до десяти запросов к инструментам.
- Slack передаёт сигнал об упоминании. Библиотека slack_bolt для Python получает событие через режим Socket Mode и извлекает запрос пользователя вместе с контекстом обсуждения.
- Gemini анализирует ситуацию и вызывает инструменты. Модель получает системное указание с строгими правилами честности и два инструмента:
search_starrocks_doc— для векторного поиска на стороне клиента, иgoogle_search— для поиска на стороне сервера с использованием встроенного в Gemini веб-поиска. - Поиск выполняется внутри StarRocks. Инструмент поиска встраивает запрос с использованием gemini-embedding-001, указывая параметр task_type=RETRIEVAL_QUERY, после чего выполняет SQL-запрос на расчёт косинусного сходства, описанный ранее.
- Модель формирует ответ. Gemini собирает полученные фрагменты в ответ в формате Markdown, который затем Rocky преобразует в формат mrkdwn для Slack перед публикацией.
- Данные телеметрии отправляются в систему мониторинга. Каждый вызов модели, использование инструментов и подсчёт токенов превращаются в элемент OpenTelemetry span, причём
thread_tsслужит идентификатором сессии.
Бот состоит примерно из 600 строк кода на Python в одном файле. Дополнительные инструменты для разбиения на части, генерации эмбеддингов и построения индекса добавляют ещё несколько тысяч строк, а хранение, поиск и оценка сходства полностью передаются на обработку StarRocks.
Почему этот набор инструментов подходит для лёгкого приложения RAG
Три варианта решения позволяют сохранить небольшие операционные затраты при одновременном получении полезных ответов.
Таблица первичных ключей в сочетании с Stream Load для замены документов
Корпус данных не является потоковой нагрузкой. При изменении документов весь каталог docs/en/ снова разбивается на части, генерируются новые эмбеддинги, и вся таблица перезагружается с помощью Stream Load:
curl --location-trusted -u root: \
-H "format: json" \
-H "strip_outer_array: true" \
-T docs_with_embedding.json \
-XPUT http://host:8030/api/starsage/docs/_stream_load
Одна HTTP-запрос загружает примерно 5 000 строк, каждая из которых содержит массив чисел с плавающей точкой размером 768. При таком масштабе метод Stream Load отлично подходит: нет необходимости в поддержке брокера, нет нужды запускать пайплайны Flink, не требуется постоянная инфраструктура для ввода данных. Для тысяч или десятков тысяч строк с встроенными векторами это самый простой способ.
Модель первичного ключа гарантирует атомарную замену данных. Запросы, поступающие во время загрузки, получают единообразную копию данных, а не частично обновленный набор. Именно по этой причине предпочтительнее использовать таблицу с первичным ключом вместо таблицы с дублирующимся ключом. Речь не идет об обновлении данных на уровне отдельных строк, поскольку объем работы заключается в полной перестройке данных. Таблица с первичным ключом в сочетании с методом Stream Load делает каждую загрузку атомарной, так что Rocky предоставляет либо старый набор данных, либо полностью новый, но никогда их смешение.
Поиск как инструмент, а не способ заполнения контекста
Документы не загружаются заранее в системный промпт. search_starrocks_doc представлен как инструмент, который можно вызвать, и модель сама решает, когда и с какой частотой им пользоваться.
На практике сообщение вроде «Привет, Rocky» совсем не запускает поиск и почти не требует ресурсов. Более сложный вопрос, например, о том, как отличается поведение функции GROUP_CONCAT в версиях 3.5 и 4.0, запускает несколько вызовов: модель запрашивает информацию для каждой версии и сравнивает результаты. Количество вызовов для поиска ограничено восемью за один раунд. Серверная функция google_search Gemini обрабатывает информацию из примечаний к выпускам и задач на GitHub, которые не входят в документацию, а Rocky охватывает широкий спектр задач без отдельного механизма веб-скрапинга.
Кэш эмбеддингов для предсказуемой стоимости
Часто возникают вопросы от сообщества: как добавить индекс в формате битмапа и как развернуть решение на Kubernetes. Локальный кэш SQLite, организованный по ключам из текста запроса и поля task_type (поскольку RETRIEVAL_QUERY и RETRIEVAL_DOCUMENT генерируют разные векторы), устраняет избыточные вызовы функций генерации векторов, благодаря чему популярные запросы не стоят ничего.
Что касается ИИ-моделей, то токены входных данных, хранящиеся в кэше Gemini, обходятся в 25 процентов от стандартной ставки. Благодаря кэшу векторов стоимость одного ответа составляет примерно 0,001 доллара, что является приемлемым показателем для проекта, финансируемого сообществом и работающего бесконечно.
Шесть уроков, извлеченных из реальных данных
Эти уроки основаны на фактическом использовании решения сообществом, а не на тестовых сценариях, и большинство из них применимы к любому вспомогательному инструменту в чат-платформах.
1. Изменение формулировки запроса эффективнее настройки алгоритмов поиска для предотвращения галлюцинаций
В ранних версиях генерировались вымышленные названия зеркал Red Hat, создавались фиктивные URL-адреса с примечаниями к выпуску и ложные номера версий, из-за чего участники сообщества попадали на страницы 404. Для бота для вопросов и ответов уверенный неверный ответ хуже, чем отсутствие ответа вовсе.
Решением не стало улучшение процесса поиска информации. Им стало изменение формулировки запроса: добавление четкого пункта о честности, требующего от модели не выдумывать факты, а при невозможности дать надежный ответ указывать на то, что она не уверена, ссылаясь на авторитетные источники и предлагая обратиться к сообществу. Эффект был немедленным. Поиск информации важен, но инжиниринг запросов часто является более быстрым и дешевым способом снижения вероятности галлюцинаций.
2. Исправьте разбиение на чанки до того, как заниматься поиском
В документации StarRocks много строк импорта MDX, таблиц Markdown, простирающихся через границы чанков, и очень длинных SQL-запросов DDL. Подача такого необработанного материала в систему генерации эмбеддингов приводит к плохим результатам. На процесс разбиения на чанки были вложены значительные ресурсы:
- Иерархическое разделение по заголовкам. Фрагменты формируются по заголовкам Markdown, причем весь путь заголовка сохраняется в качестве метаданных, что позволяет модели понимать, куда данный фрагмент относится в иерархии документа.
- Фильтрация шума. Строки импорта MDX удаляются, а блоки кода длиной более 4 000 символов исключаются, поскольку они снижают качество семантического сигнала без вклада в формирование ответов.
- Упрощённые таблицы вывода. Примеры вывода в формате таблиц MySQL заменяются структурированными описаниями вместо необработанных ASCII-таблиц.
Разделение текста на фрагменты потребовало примерно в десять раз больше усилий, чем интеграция векторного поиска. Если точность поиска оставляет желать лучшего, проверьте, как был разделён текст, прежде чем менять модели векторных представлений или настраивать индексы.
3. RAG — это инструмент, а не целый процесс
Когда получение данных осуществляется посредством вызова функции, а не встроено в каждый запрос, модель может пропустить этот шаг, если он не требуется. Приветствия, дополнительные уточнения и простые фактические вопросы никогда не затрагивают векторный индекс. Сочетание инструмента с клиентской стороны (search_starrocks_doc) и инструмента с серверной стороны (google_search), что полностью поддерживается SDK Gemini с версии 1.70, также позволяет Rocky использовать альтернативный способ получения контента вне корпуса без необходимости в специальном скрапере.
4. Соблюдайте модель сессий платформы
Треды в Slack имеют идентификаторы thread_ts. Rocky использует их напрямую в качестве идентификатора сессии, сохраняет историю в JSON-файле с таким же именем и включает в каждый запрос шесть самых последних сообщений:
contents = []
for turn in history[-MAX_CONTEXT_MESSAGES:]:
contents.append({"role": turn["role"], "parts": [{"text": turn["text"]}]})
contents.append({"role": "user", "parts": [{"text": query}]})
Это примерно двадцать строк кода, что гораздо проще, чем использование базы данных сессий.
В числе часто встречающихся отзывов пользователей есть фраза «У Rocky нет памяти». Это связано с тем, что в Slack Rocky отвечает только тогда, когда его упоминают через @, из-за чего кажется, что он забывчив. Это проблема взаимодействия с продуктом, а не техническая проблема.
5. Начните мониторинг заранее
После запуска команда хотела получить ответы на основные вопросы. Сколько вопросов обработал Rocky на этой неделе? Где сосредотачивались запросы к инструментам по тематике? Сколько стоил типичный ответ? Какие ответы были отклонены, исправлены или требовали дополнительных действий? Без возможности отслеживания система оставалась «черным ящиком», поэтому позже был внедрен полноценный слой OpenTelemetry. Урок заключается в необходимости добавления механизмов отслеживания ещё до запуска. Без них использование токенов, стоимость и паттерны запросов к инструментам остаются невидимыми, что значительно затрудняет оптимизацию затрат, определение причин ошибок и анализ слабых вопросов.
6. Агрессивно используйте кэш и отслеживайте каждый доллар
API для встраивания кажутся недорогими за один вызов, но частые вопросы сообщества быстро увеличивают расходы. Кэш SQLite в сочетании с системой ценообразования на кэшированные токены Gemini позволяет держать себестоимость ответа на уровне примерно 0,001 доллара. Для проекта, поддерживаемого сообществом, это разница между «возможностью работать бесконечно» и «необходимостью обсуждения бюджета».
Честный баланс
Rocky — это легкий ассистент типа RAG, а не система для производственного использования, поэтому стоит упомянуть несколько ограничений:
- Галлюцинации снижаются, но не устраняются полностью. Механизмы контроля вводных данных помогают, но не гарантируют точности. Лучше всего работают хорошо задокументированные темы, тогда как крайние случаи и особенности конкретных версий всё ещё могут вызывать проблемы.
- Длинные ответы. Как и у большинства ботов типа RAG, он слишком подробно объясняет. Планируется добавление опции для получения кратких ответов в виде краткого резюме.
Все эти пункты входят в текущий план развития и сформированы на основе отзывов сообщества.
Что показывает Rocky
Rocky — это небольшой проект: 600 строк кода на Python плюс одна таблица и один ключ API, но он решает более серьезную проблему. Для легких ИИ-приложений аналитическая база данных с массивными столбцами и функциями сходства может заменить необходимость в отдельной векторной базе данных. Одна таблица обеспечивает хранение документов, поиск по ключевым словам и извлечение векторов, благодаря чему объем управляемых ресурсов остается небольшим, и требуется обслуживать только одну систему, а не две.
Этот бот ежедневно обрабатывает реальный трафик сообщества; согласно данным проекта, пользователи в среднем ставят ему оценку 4,2 из 5, причем стоимость каждого ответа составляет менее десятой доли цента. Для тех, кто создает похожего вспомогательного ассистента в своей сфере и хочет простую инфраструктуру, подход с одной таблицей заслуживает внимания, особенно если в техполитике уже используется StarRocks.
Связанные материалы
- Гибридный поиск в Postgres для помощника в сфере здравоохранения: схема, разбиение на части, RRF и оценка — Создание слоя поиска для помощника в сфере здравоохранения в Postgres: разбиение данных с учётом структуры, гибридный поиск с использованием pgvector и алгоритма RRF, двоичная квантизация, пороговые значения и процедуры оценки.
- Рецепт использования Spring AI: фильтрация результатов RAG с учётом метаданных по названию игры — Пошаговая инструкция по использованию Spring AI, предусматривающая присвоение документам меток на основе названия, использование большой языковой модели для выбора подходящего значения из вопроса и фильтрацию результатов векторного поиска.