Desarrolla tu Copilot privado con RAG (Parte 1: Arquitectura y Stack)
Cansado de que LLMs alucinen con versiones antiguas de lenguaje o ignoren la estructura de mis proyectos, decidí montar un agente RAG 100% local. Un prototipo de fin de semana para tener un asistente con contexto real.
En mi post anterior, donde explico cómo montar un servidor de inferencia local, configuré un PC con una GPU moderna usando Ollama. La idea era aislar el cálculo pesado en la GPU y consumir los modelos desde el portátil a través de la red local, obteniendo velocidad y privacidad.
Pero interactuar con un LLM en “crudo”, por muy bueno que sea qwen2.5-coder:14b, tiene ciertos límites evidentes (como la fecha de entreno) con impacto en tareas del día a día.
El problema habitual al usar asistentes de programación genéricos o Copilots en la nube es la falta de contexto. Si le pides que te resuelva un problema en un framework concreto, a menudo tienden a alucinar métodos, usan código deprecado o ignoran por completo la arquitectura de tu proyecto porque no tienen acceso a tus archivos locales (sobre todo versiones lite con contexto o capacidades limitadas).
Como caso de uso para intentar solucionar esto, decidí ponerlo en práctica con Godot (un motor de juegos 2D y 3D de código libre que utilizo en proyectos en mi tiempo libre). Por tanto, es un escenario real y representativo: el salto de Godot 3.x a Godot 4.x cambió radicalmente la sintaxis, y la mayoría de LLMs comerciales siguen devolviendo código o referencias a documentación obsoletas.
Para resolverlo, planteé un prototipo de fin de semana: un Copilot privado utilizando un sistema RAG (Retrieval-Augmented Generation) que tuviera en contexto mi código y la última versión de la documentación oficial de Godot. No busco una arquitectura enterprise ni escalabilidad a nivel de producción, sino un flujo de trabajo funcional y simple que me ayude a desarrollar de forma más eficiente.
El Stack Tecnológico
Para evitar caer en la sobreingeniería decidí utilizar un stack ligero y fácil de mantener:
- El Orquestador: LangChain y LangGraph (versión Python). LangChain como framework y LangGraph como pieza clave para darle capacidad de decisión o enrutamiento al agente y compartir el estado del agente.
- LLM y embeddings: Mi servidor local sigue corriendo
qwen2.5-coder:14b. Para la vectorización, elegínomic-embed-textservido también desde Ollama y suficiente para este caso. - Vector Store: ChromaDB. Descarté levantar un PostgreSQL con
pgvector. ChromaDB funciona como serverless y guarda los vectores en una carpeta local (internamente utiliza SQLite), lo que encaja con la idea de este proyecto: que sea fácil de clonar y eliminar del sistema.
Ingesta de datos: HTML vs texto estructurado
Primero hay que alimentar al RAG. Necesitaba que el agente conociera tanto la documentación oficial de Godot 4 (versión 4.7 en el momento de esta publicación) como la base de código de mi juego.
Un “error” habitual al procesar documentación web suele ser usar librerías como BeautifulSoup para extraer el HTML. Pero esto introduce demasiado ruido: el HTML renderizado está plagado de etiquetas, clases de CSS y menús de navegación que ensucian los fragmentos de texto (chunks) y añaden confusión al LLM durante la fase de recuperación (el “Retrieve” que da la R a RAG).
La documentación oficial de Godot se ofrece en HTML y RST (formato de texto estructurado o reStructuredText) así que la alternativa más limpia fue atacar la fuente original: los archivos .rst (reStructuredText) del repositorio oficial. Procesar texto plano estructurado genera fragmentos mucho más limpios para la base de datos vectorial (ChromaDB), manteniendo íntegro el contexto de las explicaciones y los bloques de código. En resumen, vectores más precisos.
Diseño de la Arquitectura: Grafo con enrutamiento
No sirve de nada tener bases de datos vectoriales si el agente lanza búsquedas indiscriminadas contra todos los documentos a la vez. Había que dotar de cierto orden y criterio al agente antes de procesar las peticiones.
Para ello, diseñé un grafo (el agente) con LangGraph donde el primer paso es un nodo “enrutador” (o router node). Este nodo evalúa cualquier petición que le envíe y decide qué vía tomar:
- Docs: Si la consulta es sobre una API nativa o un concepto del motor, busca exclusivamente en la base de datos de la documentación oficial.
- Local: Si la consulta menciona archivos del proyecto (ej. “revisa cómo gestiono la vida del personaje en Player.gd”), busca sólo en la base de datos indexada con mis scripts locales (en este caso ficheros con extensión .gd de Godot).
- Híbrido: Si pido implementar o corregir una mecánica nativa sobre un script propio, el grafo ejecuta ambas búsquedas en paralelo (esto es algo que ofrece LangGraph de forma automática, también llamado super-step).
- Ninguno: Para peticiones o mensajes sin contexto técnico, responde directamente.
En la segunda parte de esta serie, pasaremos a la implementación técnica. Construiré paso a paso el grafo con LangChain y LangGraph, implementaré la inyección de los datos en ChromaDB y haré una comprobación final por CLI para verificar que el agente es capaz de enrutar las consultas correctamente.