Lab práctico · Semana 3: Recuperación aumentada (RAG) a fondo
Knowledge Base con S3 Vectors
Qué vas a construir
Una Customer-managed Knowledge Base de Amazon Bedrock sobre un puñado de documentos internos de una empresa ficticia, usando Amazon S3 Vectors como almacén vectorial (la opción más barata vigente: sin capacidad mínima, pagas céntimos por almacenamiento y consultas). La crearás entera con AWS CLI y boto3 desde CloudShell para ver cada pieza:
flowchart LR
DOCS["Bucket S3 con documentos .md"] --> DS["Data source (chunking fijo)"]
DS --> EMB["Titan Text Embeddings V2 (1.024 dims)"]
EMB --> IDX[("S3 Vectors: vector bucket + índice")]
Q["Pregunta"] --> RET["Retrieve / RetrieveAndGenerate"]
IDX --> RET
RET --> NOVA["Nova Micro (perfil eu.)"]
NOVA --> R["Respuesta con citas"]
Al terminar sabrás: qué rol necesita una KB, cómo se crea un índice de S3 Vectors compatible, cómo se configura el chunking, qué hace un ingestion job y la diferencia práctica entre Retrieve y RetrieveAndGenerate.
Antes de empezar
- Cuenta en plan de pago o con créditos, y permisos de administrador (o equivalentes para IAM, S3, S3 Vectors y Bedrock).
- Región: eu-central-1 (Fráncfort). La elegimos porque el lab 06 usa reranking, que en Europa solo está en eu-central-1. Amazon Nova Micro se usa mediante el perfil de inferencia EU (
eu.amazon.nova-micro-v1:0); Titan Text Embeddings V2 está disponible en la región. - Abre AWS CloudShell en eu-central-1 y actualiza boto3 (CloudShell puede traer una versión antigua sin las APIs más recientes):
pip3 install --user --upgrade boto3 botocore
python3 -c "import boto3; print(boto3.__version__)"
aws --version
aws s3vectors help >/dev/null && echo "AWS CLI con soporte de S3 Vectors"
No instales awscli con pip: taparía la AWS CLI v2 que trae CloudShell. Si aws s3vectors help falla, tu CLI es antigua: cierra y vuelve a abrir CloudShell (se actualiza periódicamente) o crea el bucket y el índice desde la consola de S3 (Vector buckets).
- Variables que usarás en todo el lab (si CloudShell se reinicia, vuelve a definirlas):
export AWS_REGION=eu-central-1
export CUENTA=$(aws sts get-caller-identity --query Account --output text)
export SUFIJO=$(date +%s | tail -c 6)
export BUCKET_DOCS=lab05-docs-$CUENTA-$SUFIJO
export VBUCKET=lab05-vectores-$SUFIJO
export INDICE=kb-indice
export ROL_KB=lab05-kb-role
echo $BUCKET_DOCS $VBUCKET
Paso 1: crea los documentos de ejemplo y súbelos a S3
Documentos de la empresa ficticia «Cafés Montaña S. L.»: vacaciones, teletrabajo, gastos y seguridad. Son cortos a propósito para que veas bien el chunking.
mkdir -p ~/lab05/docs && cd ~/lab05/docs
cat > vacaciones.md <<'EOF'
# Política de vacaciones de Cafés Montaña (2026)
Todos los empleados en España disfrutan de 23 días laborables de vacaciones al año.
En Portugal son 25 días laborables. Las vacaciones se solicitan en el portal de RR. HH.
con al menos 15 días de antelación. Del 1 al 15 de agosto la tostadora de Toledo cierra
y el personal de planta debe coger al menos 5 de esos días.
Los días no disfrutados pueden trasladarse hasta el 31 de marzo del año siguiente.
EOF
cat > teletrabajo.md <<'EOF'
# Política de teletrabajo
El personal de oficina puede teletrabajar hasta 2 días por semana, previo acuerdo con su responsable.
El personal de planta y de tiendas no puede teletrabajar.
La empresa abona 30 EUR al mes por gastos de conexión a quien teletrabaje de forma regular.
Durante el teletrabajo es obligatorio usar la VPN corporativa y el portátil de empresa.
EOF
cat > gastos.md <<'EOF'
# Política de gastos de viaje
Las dietas en territorio nacional son de 40 EUR por día; en el extranjero, 60 EUR por día.
El kilometraje con vehículo propio se paga a 0,26 EUR por kilómetro.
Los tickets se suben a la aplicación de gastos en un plazo máximo de 30 días.
Los hoteles no pueden superar los 120 EUR por noche salvo aprobación del director financiero.
El código de proyecto de viajes comerciales es VIA-2026-COM.
EOF
cat > seguridad.md <<'EOF'
# Normas de seguridad de la información
Las contraseñas deben tener al menos 14 caracteres y se cambian si hay sospecha de compromiso.
Está prohibido compartir datos de clientes por correo personal o mensajería no corporativa.
Los incidentes de seguridad se notifican en menos de 1 hora a seguridad@cafesmontana.example
indicando el código de incidente con formato SEC-AAAA-NNN.
El error ERR-4012 en la VPN indica certificado caducado: hay que renovarlo desde el portal de TI.
EOF
aws s3 mb s3://$BUCKET_DOCS --region $AWS_REGION
aws s3 cp ~/lab05/docs/ s3://$BUCKET_DOCS/documentos/ --recursive
aws s3 ls s3://$BUCKET_DOCS/documentos/
Paso 2: crea el vector bucket y el índice de S3 Vectors
El índice debe casar con el modelo de embeddings: 1.024 dimensiones (Titan V2), float32 (S3 Vectors no admite binarios) y métrica cosine. Además, Bedrock guarda el texto del chunk y sus metadatos internos en dos claves que deben ser no filtrables: AMAZON_BEDROCK_TEXT y AMAZON_BEDROCK_METADATA. Estos valores no se pueden cambiar después de crear el índice.
aws s3vectors create-vector-bucket --vector-bucket-name $VBUCKET --region $AWS_REGION
aws s3vectors create-index \
--vector-bucket-name $VBUCKET \
--index-name $INDICE \
--data-type float32 \
--dimension 1024 \
--distance-metric cosine \
--metadata-configuration '{"nonFilterableMetadataKeys":["AMAZON_BEDROCK_TEXT","AMAZON_BEDROCK_METADATA"]}' \
--region $AWS_REGION
export INDEX_ARN=$(aws s3vectors get-index --vector-bucket-name $VBUCKET --index-name $INDICE \
--region $AWS_REGION --query 'index.indexArn' --output text)
echo $INDEX_ARN
Si el último comando no devuelve nada, ejecuta aws s3vectors get-index sin --query y copia el ARN del índice a mano en INDEX_ARN.
Paso 3: crea el rol de servicio de la Knowledge Base
La KB necesita un rol que Bedrock asume para: leer el bucket de documentos, invocar el modelo de embeddings y escribir/leer en el índice de S3 Vectors. Sigue el mínimo privilegio y restringe la confianza a tu cuenta.
cat > ~/lab05/confianza.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "bedrock.amazonaws.com" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:SourceAccount": "$CUENTA" },
"ArnLike": { "AWS:SourceArn": "arn:aws:bedrock:$AWS_REGION:$CUENTA:knowledge-base/*" }
}
}]
}
EOF
cat > ~/lab05/permisos.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "Embeddings", "Effect": "Allow", "Action": ["bedrock:InvokeModel"],
"Resource": "arn:aws:bedrock:$AWS_REGION::foundation-model/amazon.titan-embed-text-v2:0" },
{ "Sid": "ListarBucket", "Effect": "Allow", "Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::$BUCKET_DOCS",
"Condition": { "StringEquals": { "aws:ResourceAccount": "$CUENTA" } } },
{ "Sid": "LeerDocumentos", "Effect": "Allow", "Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::$BUCKET_DOCS/*",
"Condition": { "StringEquals": { "aws:ResourceAccount": "$CUENTA" } } },
{ "Sid": "S3Vectors", "Effect": "Allow",
"Action": ["s3vectors:PutVectors","s3vectors:GetVectors","s3vectors:DeleteVectors","s3vectors:QueryVectors","s3vectors:GetIndex"],
"Resource": "$INDEX_ARN" }
]
}
EOF
aws iam create-role --role-name $ROL_KB --assume-role-policy-document file://~/lab05/confianza.json
aws iam put-role-policy --role-name $ROL_KB --policy-name lab05-kb-permisos --policy-document file://~/lab05/permisos.json
export ROL_KB_ARN=$(aws iam get-role --role-name $ROL_KB --query Role.Arn --output text)
sleep 10 # propagación de IAM
Paso 4: crea la Knowledge Base
Tipo VECTOR (Customer-managed), embeddings de Titan V2 a 1.024 dimensiones en FLOAT32 y almacén S3_VECTORS.
cat > ~/lab05/kb-config.json <<EOF
{
"type": "VECTOR",
"vectorKnowledgeBaseConfiguration": {
"embeddingModelArn": "arn:aws:bedrock:$AWS_REGION::foundation-model/amazon.titan-embed-text-v2:0",
"embeddingModelConfiguration": {
"bedrockEmbeddingModelConfiguration": { "dimensions": 1024, "embeddingDataType": "FLOAT32" }
}
}
}
EOF
cat > ~/lab05/storage.json <<EOF
{
"type": "S3_VECTORS",
"s3VectorsConfiguration": { "indexArn": "$INDEX_ARN" }
}
EOF
export KB_ID=$(aws bedrock-agent create-knowledge-base \
--name lab05-kb-cafes \
--description "KB de politicas internas (lab 05)" \
--role-arn $ROL_KB_ARN \
--knowledge-base-configuration file://~/lab05/kb-config.json \
--storage-configuration file://~/lab05/storage.json \
--region $AWS_REGION \
--query knowledgeBase.knowledgeBaseId --output text)
echo "KB_ID=$KB_ID"
aws bedrock-agent get-knowledge-base --knowledge-base-id $KB_ID --region $AWS_REGION \
--query knowledgeBase.status --output text # espera a ACTIVE
Paso 5: crea la fuente de datos con chunking fijo
Usamos FIXED_SIZE con 100 tokens y 20 % de solapamiento: con documentos tan cortos, así verás varios chunks por documento. En un caso real partirías de unos 300 tokens. Recuerda: la estrategia de chunking no se puede cambiar después.
cat > ~/lab05/ds.json <<EOF
{
"type": "S3",
"s3Configuration": {
"bucketArn": "arn:aws:s3:::$BUCKET_DOCS",
"inclusionPrefixes": ["documentos/"]
}
}
EOF
export DS_ID=$(aws bedrock-agent create-data-source \
--knowledge-base-id $KB_ID \
--name lab05-s3-documentos \
--data-source-configuration file://~/lab05/ds.json \
--data-deletion-policy DELETE \
--vector-ingestion-configuration '{"chunkingConfiguration":{"chunkingStrategy":"FIXED_SIZE","fixedSizeChunkingConfiguration":{"maxTokens":100,"overlapPercentage":20}}}' \
--region $AWS_REGION \
--query dataSource.dataSourceId --output text)
echo "DS_ID=$DS_ID"
Paso 6: sincroniza (ingestion job)
La sincronización lee los documentos, los trocea, calcula los embeddings y escribe en S3 Vectors.
export JOB_ID=$(aws bedrock-agent start-ingestion-job --knowledge-base-id $KB_ID --data-source-id $DS_ID \
--region $AWS_REGION --query ingestionJob.ingestionJobId --output text)
while true; do
ESTADO=$(aws bedrock-agent get-ingestion-job --knowledge-base-id $KB_ID --data-source-id $DS_ID \
--ingestion-job-id $JOB_ID --region $AWS_REGION --query ingestionJob.status --output text)
echo "Estado: $ESTADO"; [ "$ESTADO" = "COMPLETE" ] || [ "$ESTADO" = "FAILED" ] && break; sleep 10
done
aws bedrock-agent get-ingestion-job --knowledge-base-id $KB_ID --data-source-id $DS_ID \
--ingestion-job-id $JOB_ID --region $AWS_REGION --query ingestionJob.statistics
Deberías ver 4 documentos escaneados e indexados y 0 fallidos. Comprueba que hay vectores en el índice:
aws s3vectors list-vectors --vector-bucket-name $VBUCKET --index-name $INDICE --region $AWS_REGION \
--max-results 5 --return-metadata --query 'vectors[].key'
Paso 7: consulta con Retrieve (solo recuperación)
Guarda este script como ~/lab05/consultar.py. Usa Retrieve, que devuelve los chunks con su puntuación y su fuente, sin generar texto.
import os, sys, boto3
REGION = os.environ["AWS_REGION"]; KB_ID = os.environ["KB_ID"]
rt = boto3.client("bedrock-agent-runtime", region_name=REGION)
pregunta = sys.argv[1] if len(sys.argv) > 1 else "¿Cuántos días de teletrabajo puedo hacer?"
resp = rt.retrieve(
knowledgeBaseId=KB_ID,
retrievalQuery={"text": pregunta},
retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": 4}},
)
print(f"Pregunta: {pregunta}\n")
for i, r in enumerate(resp["retrievalResults"], 1):
fuente = r["location"]["s3Location"]["uri"].split("/")[-1]
texto = r["content"]["text"].replace("\n", " ")
print(f"{i}. score={r['score']:.3f} [{fuente}] {texto[:110]}...")
cd ~/lab05
python3 consultar.py "¿Cuántos días de teletrabajo puedo hacer?"
python3 consultar.py "¿Qué significa el error ERR-4012?"
python3 consultar.py "¿Cuánto me pagan por kilómetro si voy con mi coche?"
Fíjate en dos cosas: «kilómetro con mi coche» encuentra «kilometraje con vehículo propio» sin compartir palabras (búsqueda semántica), y el código ERR-4012 probablemente también aparece, pero con una puntuación menos holgada. Con miles de documentos parecidos, los códigos exactos son donde la búsqueda solo semántica sufre (y S3 Vectors no ofrece búsqueda híbrida).
Paso 8: genera respuestas con citas (RetrieveAndGenerate)
Guarda ~/lab05/rag.py. Usa el ARN del perfil de inferencia EU de Nova Micro como modelo de generación.
import os, sys, boto3
REGION = os.environ["AWS_REGION"]; KB_ID = os.environ["KB_ID"]; CUENTA = os.environ["CUENTA"]
MODELO = f"arn:aws:bedrock:{REGION}:{CUENTA}:inference-profile/eu.amazon.nova-micro-v1:0"
rt = boto3.client("bedrock-agent-runtime", region_name=REGION)
def preguntar(texto, session_id=None):
args = {
"input": {"text": texto},
"retrieveAndGenerateConfiguration": {
"type": "KNOWLEDGE_BASE",
"knowledgeBaseConfiguration": {
"knowledgeBaseId": KB_ID,
"modelArn": MODELO,
"retrievalConfiguration": {"vectorSearchConfiguration": {"numberOfResults": 4}},
},
},
}
if session_id:
args["sessionId"] = session_id
r = rt.retrieve_and_generate(**args)
print("Respuesta:", r["output"]["text"])
fuentes = {ref["location"]["s3Location"]["uri"].split("/")[-1]
for c in r["citations"] for ref in c["retrievedReferences"]}
print("Fuentes:", ", ".join(sorted(fuentes)) or "(ninguna)", "\n")
return r["sessionId"]
sid = preguntar(sys.argv[1] if len(sys.argv) > 1 else "¿Cuántos días de vacaciones tengo en España?")
preguntar("¿Y en Portugal?", sid) # segundo turno: usa el contexto de la sesión
preguntar("¿Cuál es el sueldo del director financiero?") # no está en los documentos
python3 rag.py
Observa: la segunda pregunta («¿Y en Portugal?») funciona gracias al sessionId, que mantiene el contexto de la conversación. La tercera debería responder que no dispone de esa información: es el comportamiento de grounding que buscas.
Paso 9 (opcional): compara con la consola
En la consola de Amazon Bedrock → Knowledge Bases → lab05-kb-cafes → Test knowledge base. Prueba con y sin Generate responses, abre Show source details y mira el panel Configurations (número de chunks, tipo de búsqueda, filtros, reranking, plantilla de prompt). En el lab 06 usarás esas opciones desde código.
Comprueba que funciona
-
get-ingestion-jobmuestraCOMPLETEcon 4 documentos indexados y 0 fallidos. -
list-vectorsdevuelve claves de vectores en el índice. -
consultar.pydevuelve chunks conscorey el fichero de origen correcto. -
rag.pyresponde con fuentes, mantiene el contexto en el segundo turno y reconoce que no sabe el sueldo.
Limpieza
Si vas a hacer el lab 06, sáltate esta sección y límpialo todo al final de ese lab (allí se repite). Si no, ejecuta en este orden (primero lo que depende de otros recursos):
# 1. Fuente de datos (con política DELETE borra los vectores del índice) y Knowledge Base
aws bedrock-agent delete-data-source --knowledge-base-id $KB_ID --data-source-id $DS_ID --region $AWS_REGION
sleep 20
aws bedrock-agent delete-knowledge-base --knowledge-base-id $KB_ID --region $AWS_REGION
sleep 20
# 2. Índice y vector bucket de S3 Vectors
aws s3vectors delete-index --vector-bucket-name $VBUCKET --index-name $INDICE --region $AWS_REGION
aws s3vectors delete-vector-bucket --vector-bucket-name $VBUCKET --region $AWS_REGION
# 3. Bucket de documentos
aws s3 rb s3://$BUCKET_DOCS --force
# 4. Rol de IAM
aws iam delete-role-policy --role-name $ROL_KB --policy-name lab05-kb-permisos
aws iam delete-role --role-name $ROL_KB
# 5. Comprobación
aws bedrock-agent list-knowledge-bases --region $AWS_REGION --query 'knowledgeBaseSummaries[].name'
aws s3vectors list-vector-buckets --region $AWS_REGION --query 'vectorBuckets[].vectorBucketName'
Preguntas para pensar como arquitecto
1. ¿Por qué no has podido pedir búsqueda híbrida y qué cambiarías si los usuarios buscan sobre todo por códigos de error?
S3 Vectors solo admite búsqueda semántica en Knowledge Bases; la híbrida exige Aurora PostgreSQL (tipo RDS), OpenSearch Serverless o MongoDB con campo de texto filtrable, o bien la Managed Knowledge Base, que siempre es híbrida. Si los códigos exactos son críticos, cambiaría de almacén (pagando su coste) o pasaría a la Managed KB; como parche, includeForEmbedding de metadatos con los códigos o un reranker ayudan, pero no sustituyen a la búsqueda léxica.
2. Si mañana cambia la política de teletrabajo, ¿qué harías para que la KB lo refleje con la mínima operativa?
Subir el documento actualizado a S3 y lanzar un StartIngestionJob: la sincronización es incremental y solo procesa lo cambiado. Para automatizarlo, EventBridge Scheduler (sync nocturno) o eventos de S3 → EventBridge → Lambda que lanza el job. Si el cambio debe verse al instante, IngestKnowledgeBaseDocuments (ingesta directa), replicando luego el cambio en S3.
3. ¿Qué pasaría si decides pasar de Titan V2 a 1.024 dimensiones a Titan V2 a 256 para ahorrar almacenamiento?
El índice de S3 Vectors tiene la dimensión fijada al crearlo (no se puede cambiar) y la KB también fija el modelo de embeddings. Habría que crear un índice nuevo de 256 dimensiones, una KB nueva y reindexar todos los documentos, y evaluar antes con un conjunto de preguntas si la pérdida de precisión es aceptable.
4. Los documentos de RR. HH. no deben verlos los empleados de tiendas. ¿Cómo lo resolverías?
Con una Customer-managed KB no hay permisos por documento: todo lo sincronizado es accesible para quien tenga bedrock:Retrieve. Opciones: KB separadas con IAM distinto; o un metadato audiencia en cada documento y un filtro obligatorio que añada el backend según el grupo del usuario autenticado (nunca el cliente). Si se necesitan ACL heredadas de SharePoint o similares, la Managed Knowledge Base ofrece permisos por documento.
5. ¿Cuándo usarías Retrieve en vez de RetrieveAndGenerate en producción?
Cuando necesito controlar la generación: prompt propio, otro modelo o un agente que decide, caché semántica, combinar resultados de varias KB, aplicar mi propio reranking o posprocesado, o validar/filtrar los chunks antes de enviarlos al modelo. RetrieveAndGenerate es ideal para RAG con citas y mínimo código.