El punto de partida
Cuando empecé a hacer proyectos de RAG con Amazon Bedrock, pude ver las ventajas de usar una Knowledge Base gestionada, ya que te resuelve casi todo, como la ingesta, el chunking, los embeddings y la base vectorial.
Pero para entender bien la arquitectura, quería ver qué pasaba por dentro. Así que en vez de quedarme solo con lo gestionado, armé el mismo proyecto de tres formas distintas, quitándole una capa de abstracción en cada paso para ver qué hacía cada pieza.
El proyecto se llama AutoSpec: un asistente que responde preguntas técnicas sobre tres modelos de vehículos ficticios, Alpha, Beta y Gamma, usando sus fichas técnicas como fuente. El caso de uso se mantuvo igual en las tres fases: los mismos tres archivos .txt, la misma pregunta de prueba. Lo que cambió fue la arquitectura y cuánto control tenía sobre cada parte del proceso.
- Primero entender qué hace Bedrock por mí.
- Después construir esas piezas a mano.
- Y finalmente abstraer el flujo con un framework desacoplado para probar portabilidad multi-cloud.
Fase 1: empezar por lo gestionado
Quería comprobar si la recuperación funcionaba bien antes de sumarle un LLM. Para eso usé una Knowledge Base de Bedrock y una función Lambda que invocaba directamente retrieve. La Knowledge Base se encargó de la ingesta, la fragmentación, los embeddings y el almacenamiento vectorial, yo no toqué nada de eso.
La prueba fue enviar:
¿Cuáles son las especificaciones de seguridad del modelo Alpha?
Encontró el fragmento correcto con un score de similitud de 0.59, aunque la pregunta no repetía el texto del documento literalmente, suficiente para confirmar que la búsqueda era semántica, no un simple match de palabras. Y como no invoqué ningún LLM, el costo de generación fue $0.00.
Fase 2: ahora quiero saber qué hay detrás
Acá construí el ciclo RAG directamente en código: Amazon Titan Embeddings, ChromaDB, Amazon Nova Lite.
Tres obstáculos que no esperaba
- Permisos de sandbox: tenía bloqueado s3:CreateBucket en mi entorno de pruebas, así que no podía crear buckets nuevos para una Knowledge Base.
- Retrieve no era Generate: mi entorno no soportaba retrieve_and_generate. Tuve que separar las responsabilidades: retrieve para buscar en el vector store, y la Converse API aparte para redactar la respuesta.
- Throttling de tokens: la cuota diaria de la API se agotó (ThrottlingException), así que moví la base vectorial a un storage local sin esas restricciones: ChromaDB.
Todo esto fue específico de mi entorno de práctica, no una limitación general de Bedrock.
Cuando el LLM empezó a mezclar información
En esta versión, cada ficha técnica se almacenó como un único documento, por lo que con n_results=3 la búsqueda devolvía las tres fichas completas. Si preguntaba por el Alpha, el contexto que le llegaba al modelo incluía también datos del Beta y el Gamma.
Ahí entendí algo más: el LLM puede mezclar información de distintas fuentes si no restringes su comportamiento en el prompt. Por eso agregué un guardrail explícito para usar solo el vehículo consultado y responder de forma controlada cuando no hubiera información disponible.
Ojo: El filtro no lo hizo la búsqueda vectorial, sino el LLM siguiendo las reglas del prompt.
Fase 3: el test de portabilidad multi-cloud
Me quedó otra duda: ¿qué tan atada está mi solución a AWS? Si mañana cambia el proveedor, ¿tengo que cambiar toodo el código?
Para probarlo, migré la orquestación a LangChain y cambié el stack de AWS por Google Gemini API. Justo en ese momento me quedé sin credenciales activas en mi entorno de pruebas de AWS. Pero esto no frenó mi proyecto, al contrario validó el objetivo de la fase: pasar de un proveedor a otro sin reescribir la lógica de negocio.
Tres pasos clave en esta fase:
- Chunking explícito: en la Fase 2 subía los archivos completos; acá implementé RecursiveCharacterTextSplitter (fragmentos de 1500 caracteres, 100 de traslape). Es la base para poder escalar a miles de documentos sin saturar el contexto.
- Abstracción real con LangChain: como LangChain esconde los proveedores detrás de interfaces comunes (Embeddings, BaseChatModel), cambiar todo el motor de Titan/Nova a Gemini fue modificar un par de líneas de inicialización, no reescribir funciones.
- Observabilidad con SQLite: esto porque un RAG en producción no puede ser una caja negra. Agregué un registro local (conversation_logs.db) con cada pregunta, respuesta y timestamp, para poder auditar alucinaciones después.
¿Qué me llevo de todo esto?
Mi gran conclusión es que no hay una arquitectura mejor, solo distintos niveles de control. Con la opción gestionada ganas velocidad, haciéndolo a mano ganas control, y con componentes desacoplados ganas portabilidad.
Al final no quise solo hacer un RAG funcional, sino entender qué pasa en detrás de ese RAG.
📁 Código fuente completo:
Todo el código de las 3 fases y las instrucciones para correrlo, está en mi GitHub: https://github.com/sofia-torres-v/AutoSpec-Voice-Assistant




Top comments (0)