Main Branch

Fundamentals first, always

Artículos

Le Di Voz a Mi Newsletter (Literalmente)

Le puse un botón de micrófono al widget de chat de mi newsletter. Lo presionas y entras en una conversación de voz en tiempo real con un agente de IA que conoce cada edición que he escrito. Así es como LiveKit, RAG y un solo contenedor en Railway lo hacen funcionar.

Andrea Griffiths 6 min de lectura 🌐 Read in English 🌐 阅读中文版
LiveKit Voice AI RAG GitHub Models Python FastAPI
Escuchar artículo

Le Di Voz a Mi Newsletter (Literalmente)

Mi sitio de newsletter ahora tiene un widget de chat. Escribes una pregunta, busca en todas las ediciones que he publicado y te da una respuesta con fuentes.

Eso me tomó una noche. Bacano, pero no lo suficientemente interesante como para escribir al respecto.

Lo que me hizo escribir esto: le agregué un botón de micrófono al lado del campo de texto. Lo presionas, y entras en una conversación de voz en tiempo real con un agente de IA que conoce mi contenido. Tú hablas, él escucha, él responde. No es una grabación. No es texto a voz sobre una respuesta de chat. Es una conversación de voz real.

La tecnología detrás es LiveKit, y quiero explicar cómo funciona porque fue más sencillo de lo que esperaba.

Qué Hace LiveKit

LiveKit es infraestructura de comunicación en tiempo real. Piensa en “Zoom pero programable.” Maneja toda la complejidad de WebRTC — salas, enrutamiento de audio, codecs, optimización de latencia — para que tú no tengas que hacerlo.

La parte que importa para agentes de voz con IA: LiveKit tiene un framework de agentes. Escribes un worker en Python que se conecta a su servicio en la nube y espera. Cuando un usuario se une a una sala, LiveKit envía tu agente a esa sala. El agente escucha el micrófono del usuario, procesa el habla, piensa y responde. Todo en tiempo real.

La latencia es impresionante. Se siente como hablar con alguien, no como esperar a una computadora.

La Arquitectura

Tres piezas:

La API — Un servidor FastAPI que maneja el chat de texto (búsqueda RAG sobre el contenido de mi newsletter) y genera tokens de sala LiveKit cuando alguien presiona el botón del micrófono.

El agente de voz — Un worker en Python usando el SDK de agentes de LiveKit. Se conecta hacia afuera a LiveKit Cloud y espera por salas. Cuando alguien se une, lo despachan. Dentro del agente: detección de actividad de voz (Silero VAD), habla a texto (Azure), un LLM (GPT-4.1-mini vía GitHub Models), y texto a habla (Azure). Antes de cada respuesta, busca en mi base de conocimiento el contexto más relevante — el mismo pipeline de RAG que usa el chat de texto.

El frontend — Un componente Astro con un botón de micrófono. Al presionarlo, carga el SDK de LiveKit, pide un token de sala a mi API, y se conecta a la sala vía WebRTC. El agente se une, y ya están hablando.

La API y el agente de voz corren en un solo contenedor de Railway. Un script de bash inicia ambos procesos — si alguno muere, el contenedor se detiene y Railway lo reinicia.

La Parte de RAG

Cada vez que el usuario dice algo, el agente:

  1. Transcribe el habla (Azure Speech STT)
  2. Genera un embedding del texto usando la API de GitHub Models (text-embedding-3-small, 1536 dimensiones)
  3. Busca en una base de datos vectorial SQLite (sqlite-vec) los fragmentos más relevantes
  4. Reconstruye el prompt del sistema con contexto fresco
  5. Genera una respuesta (GPT-4.1-mini vía GitHub Models)
  6. La dice en voz alta (Azure Speech TTS)

Esto pasa por cada frase. El conocimiento del agente se mantiene actualizado con lo que el usuario está preguntando, no se queda pegado en la primera pregunta.

El truco de retrieval híbrido: La búsqueda vectorial sola no puede responder “¿cuál es el último issue?” porque la similitud semántica no entiende de orden. La solución: al iniciar, el agente consulta la base de datos por todas las URLs de issues, extrae los números, e inyecta un índice de contenido en cada prompt del sistema:

Issues disponibles: issue-1, issue-2, ..., issue-20
El issue más reciente es issue-20
Total de issues: 20

Ahora el LLM tiene tanto contexto semántico de la búsqueda vectorial como metadatos estructurales que no puede aprender de los embeddings. Pregunta “¿cuál es el último issue?” y lo sabe. Pregunta “cuéntame sobre GitHub Copilot” y la búsqueda vectorial encuentra los fragmentos correctos. Retrieval híbrido.

Qué Me Sorprendió

Los agentes de LiveKit son workers, no servidores. No escuchan en un puerto. Se conectan hacia afuera a LiveKit Cloud y son despachados a las salas. Esto me confundió al principio — seguía tratando de pensarlo como otro servicio HTTP. No lo es. Es un worker de fondo que maneja audio en tiempo real.

El pipeline de voz tiene requisitos reales de latencia. El chat de texto puede tardar 2-3 segundos y a nadie le importa. ¿La voz? Si hay un silencio de 2 segundos después de que alguien termina de hablar, se siente roto. La arquitectura de streaming de LiveKit maneja esto — el TTS empieza a hablar antes de que la respuesta completa del LLM esté lista.

sqlite-vec está subestimado. Estoy corriendo búsqueda vectorial en SQLite. Sin Pinecone, sin Weaviate, sin base de datos vectorial administrada. Para una base de conocimiento de ~130 fragmentos (los 20 issues, artículos, y posts del blog de GitHub), es más que suficiente. La consulta toma milisegundos. Los embeddings vienen de la API de GitHub Models durante la ingestión — gratis durante el preview, alta calidad (text-embedding-3-small, 1536 dims), y sin dolores de cabeza cargando modelos locales.

Depurar en producción es diferente. El agente de voz funcionaba localmente pero fallaba silenciosamente en Railway. El agente escuchaba, transcribía perfectamente, pero siempre respondía con “No tengo esa información.” Resultó que la API de embeddings retornaba errores 400 porque una variable de entorno antigua (LIVEBRAIN_EMBEDDING_MODEL) todavía estaba configurada con un nombre de modelo local (all-MiniLM-L6-v2) que la API no reconocía. La solución: eliminar la variable y dejar que use el valor por defecto text-embedding-3-small. Los logs en tiempo real hicieron esto visible — sin print() mostrando conteos de fragmentos y similitudes, hubiera estado adivinando por horas.

Qué Viene

Estoy extrayendo las partes reutilizables en un framework de código abierto. La idea: apuntas a un archivo YAML con tus fuentes de contenido, corres un script de ingestión, y obtienes un agente de voz que conoce tu contenido. Newsletter, documentación, blog — lo que le des.

Todavía no está listo. La versión de mainbranch-agent funciona, pero el framework genérico necesita limpieza antes de que alguien más pueda usarlo. Lo voy a publicar como código abierto cuando esté realmente bueno, no cuando sea “mínimo viable.”

Si quieres verlo en acción, ve a mainbranch.dev y haz clic en la burbuja de chat. El botón del micrófono está ahí.

Sobre la Autora: Andrea Griffiths es Senior Developer Advocate en GitHub, donde ayuda a equipos de ingeniería a adoptar y escalar tecnologías de desarrolladores. Le apasiona hacer conceptos técnicos accesibles—tanto para humanos como para agentes de IA. Conéctate con ella en LinkedIn, GitHub, o Twitter/X. · Leer en inglés · 阅读中文版