Créer un assistant Slack RAG sans base de données vectorielle distincte
Un assistant Slack RAG sans base de données vectorielle distincte : les documents se trouvent dans une seule table StarRocks, la récupération s’effectue avec du SQL classique, et six leçons traitent des prompts et du chunking.
Sources: Lessons We Learned Building a RAG Assistant Without a Separate Vector Database
StarRocks est une base de données analytique open source rapide, dotée d’un moteur d’exécution unifié, d’une flexibilité de déploiement et d’une performance élevée pour les charges de travail réelles. À mesure que sa communauté s’est agrandie, la charge de support a augmenté : les mainteneurs continuent de recevoir les mêmes questions concernant la documentation, les problèmes sur GitHub, les notes de version et les anciens threads de Slack.
Rocky est l’assistant Slack conçu pour absorber cette charge. Sa mission est simple : prendre en charge les questions répétitives des mainteneurs tout en s’assurant que chaque réponse soit fondée sur la documentation du projet et les sources pertinentes.
Le point intéressant réside dans l’architecture. Rocky fonctionne sur StarRocks lui-même. Les extraits de documentation, la recherche par mots-clés, la récupération vectorielle et l’évaluation de la similarité se trouvent tous dans une seule table OLAP, de sorte que l’assistant qui répond aux questions sur la base de données fonctionne également sur cette même base. L’ensemble ne compte que quelque 600 lignes de code Python, une table et une clé API Gemini.
Les fondements de RAG
La première étape habituelle lors de la création d’un système de génération améliorée par la récupération de données consiste à utiliser une base de données vectorielle dédiée. Chaque composant supplémentaire nécessite une mise en œuvre distincte, un plan de secours ainsi qu’un modèle de cohérence propre, et pour un petit bot communautaire, ces coûts supplémentaires sont difficiles à justifier.
Ainsi, tout est resté dans StarRocks. Les fragments de documents et leurs embeddings Gemini à 768 dimensions sont stockés dans une table OLAP ordinaire utilisant le modèle PRIMARY KEY, et la récupération s’effectue via une requête SQL. Le principe est le suivant : si votre base de données analytique prend déjà en charge les colonnes de type tableau et les fonctions de similarité cosinus, vous n’avez pas besoin d’un second système de données pour la recherche vectorielle.
La table ressemble à n’importe quelle autre :
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;
Et la récupération s’effectue par une seule requête utilisant la similarité cosinus :
SELECT path, `index`, `text`,
approx_cosine_similarity([0.012, -0.034, ...], vector) AS similarity
FROM docs
ORDER BY similarity DESC
LIMIT 8;
Il n’y a ni SDK, ni bibliothèque client, ni système supplémentaire. Du côté de Rocky, la « base de données vectorielle » n’est rien de plus qu’une autre table qu’il peut déjà interroger.
De l’événement Slack à une réponse pertinente
Le flux est délibérément minimal. Une mention de @Rocky dans Slack déclenche le bot, qui transmet les informations nécessaires à Gemini 3 Flash par l’intermédiaire d’appels de fonctions natifs. Le modèle décide s’il faut rechercher dans la documentation ou interroger Google, et peut effectuer jusqu’à dix appels à des outils par tour.
- Slack transmet l’événement. La bibliothèque slack_bolt pour Python reçoit l’événement via le mode Socket et extrait la requête de l’utilisateur ainsi que le contexte du fil de discussion.
- Gemini raisonne et utilise des outils. Le modèle reçoit un prompt système avec des règles strictes d’honnêteté ainsi que deux outils :
search_starrocks_doc, un système de recherche vectorielle côté client, etgoogle_search, un système de vérification côté serveur via la recherche web intégrée à Gemini. - La récupération des données a lieu à l’intérieur de StarRocks. L’outil de recherche intègre la requête avec gemini-embedding-001 en utilisant task_type=RETRIEVAL_QUERY, puis exécute la requête SQL de similarité cosinus présentée précédemment.
- Le modèle rédige la réponse. Gemini assemble les fragments récupérés en une réponse au format Markdown, que Rocky convertit ensuite en format mrkdwn propre à Slack avant publication.
- Les données de télémétrie sont envoyées aux systèmes d’observabilité. Chaque appel au modèle, chaque utilisation d’outil et chaque comptage de tokens devient un span OpenTelemetry, où
thread_tssert d’ID de session.
Le bot se compose d’environ 600 lignes de code Python dans un seul fichier. Les outils complémentaires pour le découpage en blocs, la génération d’embeddings et la création d’index ajoutent seulement quelques milliers de lignes supplémentaires, tandis que le stockage, la récupération des données et l’évaluation de la similarité sont gérés par StarRocks.
Pourquoi cette stack convient à une application RAG légère
Trois choix permettent de maintenir une empreinte opérationnelle réduite tout en produisant des réponses utiles.
Table de clé primaire associée à Stream Load pour les documents interchangeables
Le corpus ne constitue pas une charge de travail en temps réel. Lorsque les documents changent, tout le répertoire docs/en/ est à nouveau découpé en blocs, les embeddings sont régénérés, et toute la table est rechargée via 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
Une requête HTTP charge environ 5 000 lignes, chacune contenant un tableau flottant à 768 dimensions. À cette échelle, Stream Load s’adapte parfaitement : pas de broker à gérer, pas de pipeline Flink à exécuter, pas d’infrastructure d’ingestion continue. Pour des milliers voire des dizaines de milliers de lignes contenant des vecteurs intégrés, c’est la solution la plus simple.
Le modèle de clé primaire garantit un remplacement atomique. Les requêtes arrivées au milieu du rechargement obtiennent une vue cohérente et jamais un corpus partiellement mis à jour. C’est la raison principale de choisir une table à clé primaire plutôt qu’une table à clé unique. Il ne s’agit pas d’opérations d’insertion ou de mise à jour au niveau des lignes, car la charge de travail consiste en des reconstructions complètes. En effet, une table à clé primaire associée à Stream Load rend chaque rechargement atomique, de sorte que Rocky sert soit l’ancien corpus, soit le nouveau corpus complet, et jamais un mélange des deux.
La récupération en tant qu’outil, et non pour remplir le contexte
Les documents ne sont pas préchargés dans la requête du système. search_starrocks_doc est exposé en tant qu’outil appelable, et le modèle choisit quand et avec quelle fréquence l’utiliser.
En pratique, un message comme « hi Rocky » ne déclenche aucune recherche et ne coûte presque rien. Une question plus nuancée, comme la différence de comportement de GROUP_CONCAT entre 3.5 et 4.0, déclenche plusieurs appels : le modèle interroge chaque version une fois et vérifie les résultats. Le nombre de recherches est limité à huit par tour. Le google_search côté serveur de Gemini gère les notes de version et les problèmes GitHub en dehors des documents, tandis que Rocky couvre une large gamme de sujets sans nécessiter de pipeline de scraping web distinct.
Un cache d’embeddings pour des coûts prévisibles
Les questions des utilisateurs se regroupent souvent. La manière d’ajouter un index bitmap et celle de le déployer sur Kubernetes sont posées plusieurs fois par semaine. Un cache SQLite local, indexé par le texte de la requête et le type de tâche (car RETRIEVAL_QUERY et RETRIEVAL_DOCUMENT génèrent des vecteurs différents), élimine les appels redondants aux fonctions d’incorporation, de sorte que les requêtes fréquentes ne coûtent rien.
Du côté des modèles LLM, les tokens d’entrée mémorisés par Gemini sont facturés à 25 % du tarif standard. Grâce au cache d’incorporation, le coût par réponse serait d’environ 0,001 dollar, ce qui est viable pour un projet financé par la communauté et fonctionnant indéfiniment.
Six leçons tirées du trafic réel
Ces enseignements proviennent de l’utilisation réelle par la communauté et non de benchmarks, et la plupart s’appliquent à tout assistant de domaine sur une plateforme de chat.
1. Les modifications des prompts sont plus efficaces que le réglage des mécanismes de récupération pour lutter contre les hallucinations
Les premières versions inventaient des noms de miroirs Red Hat, créaient des URL de notes de version fictives et produisaient des numéros de version erronés, ce qui amena les membres de la communauté à se retrouver sur des pages 404. Pour un bot de Q&R, une réponse fausse mais assurée est pire que pas de réponse du tout.
La solution ne résidait pas dans une meilleure récupération des informations. Il s’agissait plutôt d’une modification de la prompt : une clause d’honnêteté explicite indiquant au modèle de ne pas fabriquer de faits et, lorsqu’il ne pouvait pas répondre de manière fiable, de dire qu’il n’en était pas certain, de citer des sources fiables et de suggérer de poser la question à la communauté. L’effet fut immédiat. La récupération des informations est importante, mais l’ingénierie des prompts est souvent le moyen le plus rapide et le moins coûteux pour supprimer les hallucinations.
2. Corriger le découpage avant d’intervenir sur la récupération
Les documents StarRocks contiennent de nombreuses lignes d’import MDX, des tableaux Markdown qui s’étendent sur plusieurs blocs ainsi que des instructions SQL DDL très longues. Fournir tout cela directement aux modèles d’embedding entraîne des résultats de mauvaise qualité. Un investissement important a donc été consacré au processus de découpage :
- Séparation hiérarchique par en-tête. Les blocs sont divisés aux niveaux d’en-tête de Markdown, le chemin complet de l’en-tête étant stocké comme métadonnées, ce qui permet au modèle de savoir où un bloc se situe dans la hiérarchie du document.
- Filtrage du bruit. Les lignes d’import MDX sont supprimées, et les blocs de code dépassant 4 000 caractères sont éliminés car ils affaiblissent le signal sémantique sans améliorer les réponses.
- Tables de sortie simplifiées. Les exemples de sortie présentés sous forme de tables au style MySQL sont transformés en descriptions structurées à la place de grilles ASCII brutes.
La segmentation du texte a nécessité environ dix fois plus d’efforts que l’intégration de la recherche vectorielle. Lorsque le taux de rappel est insatisfaisant, examinez comment le texte a été segmenté avant de changer de modèles d’incorporation ou de modifier les paramètres de l’index.
3. RAG est un outil, pas un pipeline
Lorsque la récupération des données se fait par appel de fonction et non intégrée à chaque requête, le modèle peut l’omettre chaque fois qu’elle n’est pas nécessaire. Les salutations, les précisions complémentaires et les questions factuelles simples n’interagissent jamais avec l’index vectoriel. En combinant un outil côté client (search_starrocks_doc) et un outil côté serveur (google_search), ce que le SDK Gemini prend en charge pleinement depuis la version 1.70, Rocky dispose également d’une solution de secours pour les contenus hors corpus en l’absence de script de collecte personnalisé.
4. Suivre le modèle de session de la plateforme
Les threads de Slack disposent d’identifiants thread_ts. Rocky utilise directement ces identifiants comme ID de session, en enregistrant l’historique dans un fichier JSON portant son nom et en incluant les six dernières interactions dans chaque requête :
contents = []
for turn in history[-MAX_CONTEXT_MESSAGES:]:
contents.append({"role": turn["role"], "parts": [{"text": turn["text"]}]})
contents.append({"role": "user", "parts": [{"text": query}]})
Cela correspond à environ vingt lignes de code, bien plus simple que de gérer une base de données de sessions.
L’un des commentaires récurrents des utilisateurs est : « Rocky n’a pas de mémoire ». Cela vient du fait que, dans Slack, Rocky ne répond que lorsqu’il est mentionné par @, ce qui lui donne l’air oubliant. Il s’agit d’un problème d’interaction avec le produit et non d’un problème technique.
5. Instrumenter tôt
Après le lancement, l’équipe voulait des réponses à des questions de base. Combien de questions Rocky a-t-il traitées cette semaine ? Où se concentraient les appels aux outils par thème ? Quel était le coût d’une réponse typique ? Quelles réponses ont été reportées, corrigées ou suivi ? Sans traçage, le système était une boîte noire, c’est pourquoi une couche OpenTelemetry complète a été ajoutée par la suite. La leçon à retenir est d’ajouter le traçage avant le lancement. Sans cela, l’utilisation des tokens, les coûts et les schémas d’appel aux outils restent invisibles, ce qui rend beaucoup plus difficiles l’optimisation des coûts, l’attribution des hallucinations et l’analyse des questions faibles.
6. Utiliser activement le cache et suivre chaque dollar
Les API d’incorporation semblent peu coûteuses par appel, mais les questions fréquentes de la communauté ajoutent rapidement des coûts. Le cache d’incorporation SQLite, combiné au système de tarification basé sur des tokens en mémoire de Gemini, maintient les coûts unitaires autour de 0,001 $ par réponse. Pour un projet géré par la communauté, c’est la différence entre « être viable indéfiniment » et « devoir discuter du budget ».
Compromis honnêtes
Rocky est un assistant RAG léger et non un système de support en production ; il présente quelques limites qu’il convient de mentionner :
- Les hallucinations sont réduites, mais pas éliminées. Les règles de contrôle des prompts aident, mais ne garantissent pas la précision. Les sujets bien documentés fonctionnent le mieux, tandis que les cas limites et les particularités propres à certaines versions continuent de poser des problèmes.
- Réponses longues. Comme la plupart des bots RAG, il explique de manière excessive. Une option pour des réponses courtes et synthétiques est prévue.
Tous ces points font partie du plan d’action actif, établi sur la base des retours de la communauté.
Ce que Rocky montre
Rocky est un projet de petite taille, composé de 600 lignes de code Python, d’une table et d’une clé API, mais il permet de résoudre un problème plus important. Pour les applications d’IA légères, une base de données analytique dotée de colonnes de type tableau et de fonctions de similarité peut éliminer la nécessité d’une base de données vectorielle distincte. Une seule table gère le stockage des documents, la recherche par mots-clés et la récupération des vecteurs, ce qui maintient l’architecture simple et réduit le nombre de systèmes à gérer à un seul plutôt qu’à deux.
Le bot traite quotidiennement du trafic réel provenant de la communauté, et selon le projet, les utilisateurs lui attribuent en moyenne une note de 4,2 sur 5, chaque réponse coûtant moins d’un dixième de centime. Pour quiconque souhaite créer un assistant dans ce domaine tout en ayant besoin d’une infrastructure simple, le modèle à une seule table mérite d’être examiné, surtout si StarRocks est déjà intégré à l’écosystème.
Lectures complémentaires
- Recherche hybride dans Postgres pour un assistant de santé : schéma, segmentation, RRF et évaluation — Construction de la couche de récupération d’informations pour un assistant de santé dans Postgres : segmentation en fonction de la structure, recherche hybride avec pgvector et RRF, quantification binaire, seuils et évaluation.
- Spring AI Recipe : Filtrage des résultats RAG avec des métadonnées selon le titre du jeu — Guide pas à pas sur Spring AI permettant d’attribuer des étiquettes aux documents selon leur titre, d’utiliser un LLM pour sélectionner la bonne valeur à partir de la question, et de filtrer la recherche vectorielle en fonction de celle-ci.