Cuando empecé a hacer proyectos de RAG con Amazon Bedrock, vi la ventaja de usar una Knowledge Base gestionada: te resuelve casi todo, como la ingesta, el corte de los documentos (chunking), los embeddings y la base vectorial. Subes tus documentos y el sistema responde. Parece magia.
Pero para entender bien la arquitectura, quería ver qué pasaba por dentro. Así que construí el mismo asistente tres veces, quitando una capa de abstracción en cada paso.
El caso de uso es un asistente que responde preguntas sobre las fichas técnicas (ficticias) de tres vehículos: Alpha, Beta y Gamma. El caso se mantuvo igual en las tres fases: los mismos tres archivos .txt y las mismas dos preguntas de prueba:
- Real: ¿Cuáles son las especificaciones de seguridad del modelo Alpha?
- Trampa: ¿Cuál es la garantía extendida del modelo Alpha? (esa información no está en los documentos)
Lo que cambió fue cuánto controlaba yo de cada pieza. Un RAG tiene cuatro:
| Pieza | Qué hace |
|---|---|
| Chunking | Corta los documentos en fragmentos |
| Embeddings | Convierte cada fragmento en un vector que representa su significado |
| Base vectorial | Guarda los vectores y busca los más parecidos |
| Generación | Un modelo redacta la respuesta con lo recuperado |
Las tres primeras forman la R (recuperación); la última, la G (generación).
| Fase 1: RAG gestionado | Fase 2: RAG por componentes en AWS | Fase 3: RAG fuera de AWS | |
|---|---|---|---|
| Enfoque | Bedrock gestionado (solo la R) | Piezas propias con modelos de AWS | Fuera de AWS |
| Corte | Bedrock (300 tokens, 20 % de traslape) | Mi función en Python (500 caracteres, 100 de traslape) | LangChain (500 / 100) |
| Embeddings | Gestionados | Titan Text Embeddings V2 | Gemini |
| Base vectorial | Gestionada | ChromaDB | ChromaDB |
| Generación | No validada | Nova Lite | Gemini |
Fase 1: empezar por lo gestionado
Quería comprobar que la recuperación funcionaba bien antes de sumarle un LLM, porque si la recuperación falla, ningún modelo lo arregla. Usé una Knowledge Base de Amazon Bedrock con la creación rápida, y una función Lambda que llama a retrieve.
Lo que decidí yo: el nombre de la KB y mi bucket de S3. Todo lo demás lo decidió el servicio:
| Configuración | Decisión |
|---|---|
| Parser | Managed parser |
| Chunking | Por defecto: 300 tokens máximo, 20 % de traslape |
| Modelo de embeddings | Gestionado |
| Vector store | Gestionado |
| Sync | Automático al crear la KB |
La pantalla de creación también deja elegir un modelo de embeddings propio, como Titan v2, con cobro aparte. Yo dejé el gestionado: es más cómodo.
El sync terminó con 3 archivos escaneados, 3 agregados y 0 fallidos, en unos dos minutos. Ahí ocurrió la ingesta: leer los archivos, cortarlos, calcular los vectores y guardarlos, sin que yo escribiera una línea de código.
En la consola de prueba (Test knowledge base, con Standard retrieval only) la pregunta real devolvió tres fragmentos: dos de la ficha de Alpha y uno de Beta. La ficha de Alpha quedó cortada en dos pedazos, y el segundo empieza antes de donde terminó el primero: es el traslape.
Después lo hice desde código, con una Lambda de unas 15 líneas:
import os
import boto3
client = boto3.client("bedrock-agent-runtime", region_name="us-east-1")
def lambda_handler(event, context):
resp = client.retrieve(
knowledgeBaseId=os.environ["KB_ID"],
retrievalQuery={"text": event["prompt"]},
)
resultados = []
for i, r in enumerate(resp["retrievalResults"], start=1):
resultados.append({
"n": i,
"score": round(r["score"], 2),
"texto": r["content"]["text"][:300],
})
return resultados
El rol de la Lambda solo tiene el permiso bedrock:Retrieve sobre esa KB, en lugar de un acceso amplio. El resultado:
| # | Score | Contenido |
|---|---|---|
| 1 | 0.59 | Inicio de la ficha de Alpha (el título, "Modelo Alpha") |
| 2 | 0.35 | La sección de seguridad de Alpha |
| 3 | 0.15 | La sección de seguridad de Beta |
El score mide qué tan parecido es el fragmento a la pregunta; sirve para ordenar los fragmentos, no es un porcentaje de acierto. Lo que me llamó la atención es que el mejor score no fue el fragmento con la lista de seguridad, sino el del título: el score refleja la similitud con toda la pregunta, y ahí pesa mucho "modelo Alpha". Es mi interpretación. También vi que la recuperación trae un fragmento de otro modelo: busca similitud de significado, no entiende de "modelos".
Dos cosas que descubrí en el camino
- La consola indica que Standard retrieval with answer generation no está soportado para KB gestionadas: hay que usar la opción agentic.
- Al probar la generación con agentic me apareció
ThrottlingException: Too many tokens per day, la cuota diaria de tokens de mi cuenta. Es un límite de mi cuenta; no demuestra una limitación general de Bedrock.
Fase 2: ahora armo las piezas
Como la generación con mi KB se topó con esa cuota, armé yo el flujo, en un entorno de laboratorio de AWS donde sí podía invocar modelos: Titan Text Embeddings V2 para los vectores, ChromaDB como base vectorial y Amazon Nova Lite (API Converse) para redactar.
Lo que Bedrock hacía por mí en la Fase 1, ahora lo hacía con mi código. Empezando por el corte:
def dividir(texto, tamano=500, solape=100):
chunks, inicio = [], 0
while inicio < len(texto):
chunks.append(texto[inicio:inicio + tamano])
if inicio + tamano >= len(texto):
break
inicio += tamano - solape
return chunks
Corta por cantidad de caracteres, sin mirar el contenido, así que puede partir palabras: el traslape evita perder información, pero el corte es básico. Cada fragmento lleva delante el nombre de su archivo.
El prompt lleva el guardrail: responder solo con los fragmentos del modelo que menciona la pregunta, y si la información no aparece, responder exactamente "No tengo esa información en las fichas técnicas disponibles."
Con eso, la pregunta real respondió solo con la seguridad de Alpha, y la pregunta trampa con el mensaje fijo.
Fase 3: salir de AWS
Me quedó otra duda: ¿esto es de AWS, o es el patrón? Para responderla, repetí el mismo flujo fuera de AWS, con LangChain, ChromaDB y Gemini. En ese momento no tenía acceso a AWS, y eso me sirvió para comprobar que el patrón funciona con otro proveedor.
Mismos documentos, mismo corte de 500 y 100, mismo prompt:
- El corte lo hace
RecursiveCharacterTextSplitterde LangChain. - Gemini genera los embeddings y las respuestas.
- Chroma guarda los vectores, esta vez a través de LangChain.
- La clave de la API se lee de un archivo
.envque no se sube al repositorio.
La ingesta generó 6 fragmentos. La pregunta real respondió con los cinco puntos de seguridad de Alpha, y la trampa con el mensaje fijo.
Lo que más me enseñó: cómo cortas cambia lo que recuperas
La misma lista de seguridad de Alpha quedó así:
| Fase | Quién cortó | Qué pasó |
|---|---|---|
| 1 | Bedrock (300 tokens, 20 %) | Partida en 2 fragmentos |
| 2 | Mi función (500 / 100) | Partida, incluso a mitad de palabra |
| 3 | LangChain (500 / 100) | Completa en un solo fragmento |
Con el mismo tamaño en las Fases 2 y 3, el resultado fue distinto: mi función corta por posición, y el splitter de LangChain intenta priorizar separadores naturales antes de partir el texto. En estas fichas eso conservó la lista entera. No significa que LangChain siempre corte mejor: depende de la estructura de los documentos.
Sobre la portabilidad
Esto es lo que probé y lo que no:
-
Probé, en una prueba aparte, que LangChain puede crear los modelos de Bedrock:
ChatBedrockConversecon Nova Lite respondió, yBedrockEmbeddingscon Titan V2 devolvió un vector de 1024 números. - No probé la Fase 3 completa con Bedrock. Mi cuenta tiene una cuota diaria de tokens que ya me había bloqueado la generación, y lo más probable es que me hubiera atascado otra vez.
Lo más probable, por cómo está pensado LangChain (una interfaz común para modelos de distintos proveedores), es que baste con cambiar las clases que crean los modelos, las credenciales y volver a ingestar: los vectores de un modelo de embeddings no sirven para otro. Es una expectativa, no un resultado.
Además, en la Fase 3 cambian dos cosas a la vez: el proveedor y el framework. Por eso no es una comparación aislada de proveedores. Para aislarla, repetiría el flujo con LangChain y Amazon Bedrock (Titan + Nova Lite).
Qué me llevo
- Cómo cortas los documentos cambia lo que recuperas.
- La búsqueda siempre devuelve algo, aunque la respuesta no exista. Quien decide qué hacer con eso es el prompt.
No hay una arquitectura mejor, sino distintos niveles de control. Gestionado, si quieres salir rápido y delegar la infraestructura; propio, si necesitas ver y controlar el corte, el prompt o la base vectorial; y con un framework como LangChain, si te importa poder cambiar de proveedor. Mi consejo: empieza con lo que viene por defecto, mide, y cambia solo lo que mida mal.
Lo que no medí
- Costo, latencia ni calidad de las respuestas: no hay comparaciones de rendimiento.
- Son solo tres documentos cortos: no sé cómo se comportaría con miles.
- La generación con mi Knowledge Base no se llegó a probar (cuota diaria de mi cuenta).
- Mi corte a mano es básico y parte palabras.
Las restricciones que encontré eran de mi cuenta y de mi entorno de práctica; no demuestran limitaciones generales de Bedrock.
Código y cómo reproducirlo
El código de las tres fases, las capturas y los pasos para correrlo (en Windows) están en mi GitHub, con un README por fase:
👉 Amazon-Bedrock-RAG-3-fases en GitHub
Un recordatorio si lo clonas: cada persona debe usar su propia clave de API, que nunca se sube al repositorio. Las fichas son ficticias.

Top comments (0)