Crear un asistente RAG para Slack sin una base de datos vectorial separada
Un asistente RAG de Slack sin base de datos vectorial separada: los documentos se almacenan en una sola tabla StarRocks, la recuperación se realiza con SQL estándar, y seis lecciones tratan sobre prompts y fragmentación.
Fuentes: Lessons We Learned Building a RAG Assistant Without a Separate Vector Database
StarRocks es una base de datos analítica de código abierto y rápida, con un motor de ejecución unificado, una implementación flexible y un rendimiento excepcional en cargas de trabajo reales. A medida que ha crecido su comunidad, también lo ha hecho la cantidad de solicitudes de soporte: los mantenedores siguen recibiendo las mismas preguntas sobre la documentación, los problemas de GitHub, las notas de versión y los hilos antiguos de Slack.
Rocky es el asistente de Slack creado para asumir esa carga. Su función es sencilla: eliminar las preguntas y respuestas repetitivas de los mantenedores, al tiempo que asegura que cada respuesta esté basada en la documentación del proyecto y fuentes relacionadas.
Lo interesante es su arquitectura. Rocky funciona sobre StarRocks mismo. Los fragmentos de documentación, la búsqueda por palabras clave, la recuperación de vectores y la evaluación de similitud se almacenan todos en una sola tabla OLAP, de modo que el asistente que responde preguntas sobre la base de datos también opera en esa misma base de datos. Todo el sistema consta de unas 600 líneas de Python, una tabla y una clave API de Gemini.
La base de RAG
El primer paso habitual al crear sistemas de generación mejorada por recuperación de información es utilizar una base de datos vectorial dedicada. Cada componente adicional implica una implementación separada, un plan de respaldo y su propio modelo de consistencia, y para un bot comunitario de pequeño tamaño, ese sobrecosto resulta difícil de justificar.
Por eso todo se mantuvo en StarRocks. Los fragmentos de documentos y sus embeddings de Gemini de 768 dimensiones se almacenan en una tabla OLAP ordinaria que utiliza el modelo PRIMARY KEY, y la recuperación de información se realiza mediante consultas SQL. El principio es el siguiente: si su base de datos analítica ya soporta columnas de tipo array y funciones de similitud coseno, no necesita un segundo sistema de datos para búsquedas vectoriales.
La tabla se parece a cualquier otra:
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;
Y la recuperación se realiza con una sola consulta que utiliza la similitud coseno:
SELECT path, `index`, `text`,
approx_cosine_similarity([0.012, -0.034, ...], vector) AS similarity
FROM docs
ORDER BY similarity DESC
LIMIT 8;
No existe SDK, ni biblioteca de cliente, ni segundo sistema. Desde el lado de Rocky, la “base de datos vectorial” es simplemente otra tabla que ya puede consultar.
De un evento en Slack a una respuesta fundamentada
El flujo es deliberadamente mínimo. Una mención a @Rocky en Slack activa al bot, y este transmite los razonamientos a Gemini 3 Flash mediante llamadas a funciones nativas. El modelo decide si debe buscar en la documentación o consultar Google, y puede realizar hasta diez rondas de llamadas a herramientas por turno.
- Slack envía la mención. La biblioteca slack_bolt para Python recibe el evento a través del modo Socket y extrae la consulta del usuario junto con el contexto del hilo de conversación.
- Gemini razona y utiliza herramientas. El modelo recibe una instrucción de sistema con reglas estrictas de honestidad y dos herramientas:
search_starrocks_doc, una recuperación de vectores en el lado del cliente, ygoogle_search, una verificación en el lado del servidor a través de la búsqueda web integrada en Gemini. - La recuperación se realiza dentro de StarRocks. La herramienta de búsqueda incrusta la consulta con gemini-embedding-001 utilizando task_type=RETRIEVAL_QUERY y luego ejecuta la consulta SQL de similitud coseno mostrada anteriormente.
- El modelo escribe la respuesta. Gemini reúne los fragmentos recuperados en una respuesta en formato Markdown, la cual Rocky convierte al formato mrkdwn de Slack antes de publicarla.
- La telemetría se envía a los sistemas de observabilidad. Cada llamada al modelo, uso de herramientas y conteo de tokens se convierte en un span de OpenTelemetry, siendo
thread_tsel identificador de sesión.
El bot consta aproximadamente de 600 líneas de Python en un único archivo. Las herramientas adicionales para dividir el contenido en partes, generar embeddings y crear índices suman solo unos pocos miles de líneas más, y las tareas de almacenamiento, recuperación y evaluación de similitud quedan a cargo de StarRocks.
Por qué esta combinación es adecuada para una aplicación RAG ligera
Tres opciones permiten mantener un impacto operativo reducido mientras se generan respuestas útiles.
Tabla de clave primaria más Stream Load para documentos intercambiables
El corpus no representa una carga de trabajo en tiempo real. Cuando los documentos cambian, todo el directorio docs/en/ se vuelve a dividir en partes, se regeneran los embeddings y toda la tabla se vuelve a cargar mediante 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
Una solicitud HTTP carga alrededor de 5,000 filas, cada una con un arreglo de flotantes de 768 dimensiones. A esta escala, Stream Load es muy adecuado: no hay broker que mantener, ni pipeline de Flink que ejecutar, ni infraestructura de ingesta continua. Para miles o decenas de miles de filas con vectores incrustados, es la opción más sencilla.
El modelo de Clave Primaria garantiza un reemplazo atómico. Las consultas que llegan durante la recarga ven una instantánea consistente y nunca un corpus parcialmente actualizado. Esa es la razón principal para elegir una tabla con Clave Primaria en lugar de una con Clave Duplicada. No se trata de actualizaciones a nivel de fila, ya que la carga de trabajo consiste en reconstrucciones completas. Lo importante es que una tabla con PK junto con Stream Load hace que cada recarga sea atómica, de modo que Rocky sirve ya sea el corpus antiguo o el completamente nuevo, y nunca una mezcla de los dos.
La recuperación como herramienta, no como relleno de contexto
Los documentos no se cargan previamente en el sistema. search_starrocks_doc está disponible como una herramienta llamable, y el modelo decide cuándo y con qué frecuencia utilizarla.
En la práctica, un mensaje como “hola Rocky” no desencadena ninguna búsqueda y cuesta casi nada. Una pregunta más compleja, como la diferencia en el comportamiento de GROUP_CONCAT entre 3.5 y 4.0, provoca varias llamadas: el modelo consulta cada versión una vez y verifica los resultados. La cantidad de búsquedas está limitada a ocho por turno. El google_search del lado del servidor de Gemini se encarga de las notas de lanzamiento y los problemas de GitHub que van más allá de la documentación, mientras que Rocky abarca una amplia gama de temas sin necesidad de un proceso separado de extracción de datos de la web.
Un caché de embeddings para un costo predecible
Las preguntas de la comunidad se agrupan. Preguntas como cómo agregar un índice de mapa de bits y cómo desplegarlo en Kubernetes surgen varias veces a la semana. Un caché local de SQLite, indexado por el texto de la consulta y el tipo de tarea (ya que RETRIEVAL_QUERY y RETRIEVAL_DOCUMENT generan vectores diferentes), elimina las llamadas redundantes a los procesos de incrustación, de modo que las consultas más frecuentes no tienen costo alguno.
En cuanto a los modelos LLM, los tokens de entrada en caché de Gemini se cobran al 25 por ciento de la tarifa estándar. Con el caché de incrustación, el costo por respuesta se estima en alrededor de 0,001 dólares, lo cual es sostenible para un proyecto financiado por la comunidad que funciona de forma indefinida.
Seis lecciones del tráfico real
Estas provienen del uso real de la comunidad y no de pruebas de rendimiento, y la mayoría son aplicables a cualquier asistente en una plataforma de chat.
1. Los cambios en los prompts son más efectivos que el ajuste de la recuperación de información para evitar alucinaciones
Las versiones iniciales inventaban nombres de espejos de Red Hat, creaban URLs de notas de lanzamiento ficticias y generaban números de versión erróneos, lo que hacía que los miembros de la comunidad accedieran a páginas 404. Para un bot de preguntas y respuestas, una respuesta incorrecta pero segura es peor que no tener respuesta alguna.
La solución no consistió en mejorar la recuperación de información. Fue un cambio en la instrucción: una cláusula de honestidad explícita que indicaba al modelo que no fabricara hechos y, cuando no pudiera responder de manera fiable, que dijera que no estaba seguro, señalara fuentes autorizadas y sugiriera hacer la pregunta a la comunidad. El efecto fue inmediato. La recuperación de información es importante, pero la ingeniería de instrucciones suele ser la forma más rápida y económica de suprimir las alucinaciones.
2. Corregir el procesamiento en bloques antes de intervenir en la recuperación
La documentación de StarRocks contiene muchas líneas de importación MDX, tablas de Markdown que abarcan varios bloques y sentencias SQL DDL muy largas. Al introducir todo eso directamente en los modelos de embeddings, se obtiene como resultado información de mala calidad. Se invirtió mucho en el proceso de división en bloques:
- División jerárquica por encabezados. Los fragmentos se separan en los encabezados de Markdown, y toda la ruta del encabezado se almacena como metadatos, lo que permite al modelo saber dónde pertenece un fragmento en la jerarquía del documento.
- Filtrado de ruido. Las líneas de importación de MDX se eliminan, y los bloques de código de más de 4,000 caracteres se descartan porque diluyen la señal semántica sin contribuir a las respuestas.
- Tablas de salida más simples. La salida de ejemplo mostrada como tablas al estilo MySQL se condensa en descripciones estructuradas en lugar de grillas ASCII sin formato.
El proceso de fragmentación requirió aproximadamente diez veces más esfuerzo que la integración de búsquedas vectoriales. Cuando la precisión sea insatisfactoria, examine cómo se fragmentó el texto antes de cambiar los modelos de embedding o ajustar la configuración del índice.
3. RAG es una herramienta, no un pipeline
Cuando la recuperación de información consiste en una llamada a función y no está integrada en cada solicitud, el modelo puede omitirla siempre que no sea necesaria. Los saludos, las aclaraciones posteriores y las preguntas factuales simples nunca acceden al índice vectorial. Combinar una herramienta del lado del cliente (search_starrocks_doc) con una del lado del servidor (google_search), algo que el SDK de Gemini ha soportado plenamente desde la versión 1.70, también proporciona a Rocky una solución alternativa para contenido fuera del corpus, sin necesidad de un raspador personalizado.
4. Sigue el modelo de sesiones de la plataforma
Los hilos de Slack vienen con identificadores thread_ts. Rocky utiliza estos directamente como ID de sesión, guardando el historial en un archivo JSON con el mismo nombre e incluyendo las seis interacciones más recientes en cada solicitud:
contents = []
for turn in history[-MAX_CONTEXT_MESSAGES:]:
contents.append({"role": turn["role"], "parts": [{"text": turn["text"]}]})
contents.append({"role": "user", "parts": [{"text": query}]})
Eso equivale a unas veinte líneas de código, mucho más simple que ejecutar una base de datos de sesiones.
Un comentario recurrente de los usuarios es que “Rocky no tiene memoria”. Esto se debe a que en Slack, Rocky solo responde cuando se le menciona con @, lo que hace que parezca olvidadizo. Es un problema de interacción con el producto y no técnico.
5. Instrumenta temprano
Después del lanzamiento, el equipo quería respuestas a preguntas básicas. ¿Cuántas consultas manejó Rocky esta semana? ¿Dónde se concentraron las llamadas a las herramientas por tema? ¿Cuál era el costo promedio de una respuesta? ¿Qué respuestas fueron rechazadas, corregidas o requerieron seguimiento? Al no haber rastreos, el sistema era como una caja negra, por lo que posteriormente se implementó una capa completa de OpenTelemetry. La lección es añadir el rastreo antes del lanzamiento. Sin él, el uso de tokens, los costos y los patrones de llamadas a las herramientas quedan invisibles, lo que dificulta enormemente la optimización de costos, la atribución de errores y el análisis de preguntas débiles.
6. Usar caché de forma intensiva y hacer un seguimiento de cada dólar
Las APIs de integración parecen económicas por llamada, pero las frecuentes preguntas de la comunidad suman rápidamente costos. El caché de integración con SQLite, junto con la tarifa basada en tokens en caché de Gemini, mantiene los costos unitarios cerca de 0,001 dólares por respuesta. Para un proyecto mantenido por la comunidad, esa es la diferencia entre “ser sostenible indefinidamente” y “tener que hablar sobre el presupuesto”.
Compromisos honestos
Rocky es un asistente RAG ligero y no un sistema de soporte en producción; existen algunas limitaciones que merecen ser mencionadas:
- Las alucinaciones se reducen, pero no se eliminan. Las medidas de control de los prompts ayudan, pero no garantizan la corrección. Los temas bien documentados funcionan mejor, mientras que los casos extremos y peculiaridades específicas de cada versión siguen causando problemas.
- Respuestas extensas. Al igual que la mayoría de los bots RAG, explica en exceso. Se planea agregar una opción para respuestas cortas en formato de resumen.
- Sin memoria entre hilos. El contexto permanece dentro de un hilo de Slack, por lo que un usuario que preguntó sobre vistas materializadas ayer comienza desde cero hoy.
- Aún sin escalada humana. Después de varios intentos fallidos, debería etiquetar automáticamente a un mantenedor, pero ese flujo de trabajo aún no está implementado.
- Diagnóstico limitado. Para un diagnóstico más profundo será necesario combinarlo con una herramienta de depuración de código abierto que el proyecto lanzó recientemente.
Todos estos son elementos activos en la hoja de ruta, basados en los comentarios de la comunidad.
Qué muestra Rocky
Rocky es un proyecto pequeño, con 600 líneas de Python además de una tabla y una clave API, pero sirve para un propósito más amplio. En el caso de aplicaciones de IA ligeras, una base de datos analítica con columnas de array y funciones de similitud puede eliminar la necesidad de una base de datos vectorial separada. Una sola tabla se encarga del almacenamiento de documentos, la búsqueda por palabras clave y la recuperación de vectores, de modo que la superficie operativa se mantiene reducida y solo hay un sistema del que ocuparse en lugar de dos.
El bot maneja diariamente tráfico real de la comunidad, y según el proyecto, los usuarios le asignan una puntuación promedio de 4.2 sobre 5, siendo el costo de cada respuesta inferior a una décima de centavo. Para quienes estén desarrollando un asistente en un dominio similar y busquen una infraestructura sencilla, el patrón de una sola tabla merece ser considerado, especialmente si ya se utiliza StarRocks en el stack tecnológico.
Leer más
- Búsqueda híbrida en Postgres para un asistente de salud: esquema, fragmentación, RRF y evaluación — Construcción de la capa de recuperación de información de un asistente de salud en Postgres: fragmentación basada en la estructura, búsqueda híbrida con pgvector y RRF, cuantización binaria, umbrales y evaluación.
- Receta de Spring AI: Filtrado de resultados RAG con metadatos por título del juego — Una guía paso a paso de Spring AI que etiqueta los documentos con un título, utiliza un LLM para seleccionar el valor adecuado a partir de la pregunta y filtra la búsqueda vectorial en función de él.