Lab práctico · Semana 10: Evaluación y resolución de problemas de aplicaciones de IA generativa
Evaluación barata con LLM como juez y evaluación de RAG en Bedrock
Qué vas a construir
Tres trabajos de Amazon Bedrock Evaluations sin crear ninguna Knowledge Base ni vector store, usando la modalidad bring your own inference responses (BYOI): tú aportas las respuestas ya generadas y Bedrock solo las puntúa con un modelo juez.
- Model evaluation con LLM-as-a-judge: 8 preguntas respondidas por Nova Micro (una respuesta estropeada a propósito) y puntuadas en Correctness, Completeness y Relevance.
- RAG evaluation «retrieve and generate»: 6 respuestas de un RAG ficticio con sus pasajes recuperados, puntuadas en Faithfulness, Correctness y Completeness.
- RAG evaluation «retrieve only»: la calidad de la recuperación con Context relevance y Context coverage.
Verás cómo el juez detecta la respuesta incorrecta, la respuesta no fiel a los pasajes y la recuperación irrelevante.
Antes de empezar
- Región eu-central-1: admite evaluaciones de modelos. Generador: Nova Micro (
eu.amazon.nova-micro-v1:0); juez: Nova Pro vía el perfil EU (eu.amazon.nova-pro-v1:0), que está en la lista de modelos evaluadores admitidos (los perfiles de cross-Region inference están soportados). - AWS CloudShell en eu-central-1, con permisos para S3, IAM y Bedrock.
- No hace falta configurar CORS: solo es obligatorio para los trabajos con evaluadores humanos creados desde la consola.
Paso 1: bucket y rol de servicio
export AWS_REGION=eu-central-1
export CUENTA=$(aws sts get-caller-identity --query Account --output text)
export BUCKET=lab18-eval-$CUENTA
export ROL=Lab18BedrockEvalRole
aws s3 mb s3://$BUCKET --region $AWS_REGION
cat > confianza.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "bedrock.amazonaws.com"},
"Action": "sts:AssumeRole",
"Condition": {"StringEquals": {"aws:SourceAccount": "$CUENTA"}}
}]
}
EOF
cat > permisos.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvocarJuez",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:CreateModelInvocationJob", "bedrock:StopModelInvocationJob"],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:$AWS_REGION:$CUENTA:inference-profile/*"
]
},
{
"Sid": "DatosS3",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:GetBucketLocation",
"s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads"],
"Resource": ["arn:aws:s3:::$BUCKET", "arn:aws:s3:::$BUCKET/*"]
}
]
}
EOF
aws iam create-role --role-name $ROL --assume-role-policy-document file://confianza.json
aws iam put-role-policy --role-name $ROL --policy-name eval --policy-document file://permisos.json
El recurso foundation-model usa * en la región porque el perfil EU puede enrutar la inferencia del juez a cualquier región europea del perfil.
Paso 2: genera los datasets
Crea preparar.py. Genera las respuestas con Nova Micro, estropea una a propósito y escribe los tres ficheros JSONL con el formato exacto que exige Bedrock:
import json
import boto3
brt = boto3.client("bedrock-runtime", region_name="eu-central-1")
GENERADOR = "eu.amazon.nova-micro-v1:0"
# 1) Conjunto de referencia (golden dataset) para LLM-as-a-judge
PREGUNTAS = [
("¿Cuál es la capital de Australia?", "La capital de Australia es Canberra.", "geografia"),
("¿Cuántos días tiene un año bisiesto?", "Un año bisiesto tiene 366 días.", "general"),
("¿Qué gas absorben las plantas en la fotosíntesis?", "Las plantas absorben dióxido de carbono (CO2).", "ciencia"),
("¿En qué año llegó el ser humano a la Luna?", "En 1969, con la misión Apolo 11.", "historia"),
("¿Qué es un token en un modelo de lenguaje?", "Es la unidad de texto (palabra, parte de palabra o signo) que procesa el modelo y por la que se factura.", "ia"),
("¿Cuál es el río más largo de la península ibérica?", "El Tajo.", "geografia"),
("¿Qué significa RAG?", "Retrieval Augmented Generation: recuperar información relevante y añadirla al prompt antes de generar.", "ia"),
("¿Cuántos lados tiene un hexágono?", "Seis.", "general"),
]
with open("juez.jsonl", "w", encoding="utf-8") as f:
for i, (pregunta, referencia, categoria) in enumerate(PREGUNTAS):
r = brt.converse(
modelId=GENERADOR,
messages=[{"role": "user", "content": [{"text": pregunta + " Responde en una o dos frases."}]}],
inferenceConfig={"maxTokens": 200, "temperature": 0},
)
respuesta = r["output"]["message"]["content"][0]["text"]
if i == 0:
respuesta = "La capital de Australia es Sídney, su ciudad más poblada." # error intencionado
f.write(json.dumps({
"prompt": pregunta,
"referenceResponse": referencia,
"category": categoria,
"modelResponses": [{"response": respuesta, "modelIdentifier": "nova-micro-v1"}],
}, ensure_ascii=False) + "\n")
print(i, respuesta[:80])
# 2) y 3) RAG ficticio sobre la política interna de una empresa
POLITICA = {
"vacaciones": "Los empleados disponen de 23 días laborables de vacaciones al año. Se solicitan en el portal con 15 días de antelación.",
"teletrabajo": "Se permite teletrabajar hasta 3 días por semana previo acuerdo con el responsable del equipo.",
"gastos": "Los gastos de viaje se reembolsan en un plazo de 30 días presentando la factura en el portal de gastos.",
"formacion": "Cada empleado tiene un presupuesto anual de formación de 1.200 euros.",
"comedor": "El comedor de la oficina abre de 13:00 a 16:00.",
}
CASOS = [
# (pregunta, referencia, pasajes recuperados, respuesta del RAG)
("¿Cuántos días de vacaciones tengo?", "23 días laborables al año.",
["vacaciones"], "Tienes 23 días laborables de vacaciones al año; solicítalos con 15 días de antelación."),
("¿Cuántos días puedo teletrabajar?", "Hasta 3 días por semana, previo acuerdo con el responsable.",
["teletrabajo"], "Puedes teletrabajar hasta 3 días por semana si lo acuerdas con tu responsable."),
("¿En cuánto tiempo me reembolsan los gastos?", "En 30 días, presentando la factura en el portal.",
["gastos"], "Te los reembolsan en 30 días tras presentar la factura en el portal de gastos."),
("¿Qué presupuesto de formación tengo?", "1.200 euros al año.",
["formacion"], "Dispones de 1.200 euros anuales para formación."),
# Respuesta NO fiel: añade datos que no están en los pasajes
("¿Cuántos días de vacaciones tengo?", "23 días laborables al año.",
["vacaciones"], "Tienes 23 días de vacaciones más 5 días extra por antigüedad y el día de tu cumpleaños libre."),
# Recuperación irrelevante: los pasajes no tratan el tema preguntado
("¿Cuál es el presupuesto de formación?", "1.200 euros al año.",
["comedor", "teletrabajo"], "El comedor abre de 13:00 a 16:00."),
]
def pasajes(claves):
return [{"name": k, "content": {"text": POLITICA[k]}} for k in claves]
with open("rag_generacion.jsonl", "w", encoding="utf-8") as fg, open("rag_recuperacion.jsonl", "w", encoding="utf-8") as fr:
for pregunta, referencia, claves, respuesta in CASOS:
turno = {
"prompt": {"content": [{"text": pregunta}]},
"referenceResponses": [{"content": [{"text": referencia}]}],
}
fg.write(json.dumps({"conversationTurns": [dict(turno, output={
"text": respuesta,
"modelIdentifier": "nova-micro-v1",
"knowledgeBaseIdentifier": "rag-politicas",
"retrievedPassages": {"retrievalResults": pasajes(claves)},
})]}, ensure_ascii=False) + "\n")
fr.write(json.dumps({"conversationTurns": [dict(turno, output={
"knowledgeBaseIdentifier": "rag-politicas",
"retrievedResults": {"retrievalResults": pasajes(claves)},
})]}, ensure_ascii=False) + "\n")
print("Datasets escritos")
python3 preparar.py
aws s3 cp juez.jsonl s3://$BUCKET/entrada/juez.jsonl
aws s3 cp rag_generacion.jsonl s3://$BUCKET/entrada/rag_generacion.jsonl
aws s3 cp rag_recuperacion.jsonl s3://$BUCKET/entrada/rag_recuperacion.jsonl
Paso 3: lanza los tres trabajos
Crea lanzar.py:
import os
import time
import boto3
bedrock = boto3.client("bedrock", region_name="eu-central-1")
BUCKET = os.environ["BUCKET"]
ROL = f"arn:aws:iam::{os.environ['CUENTA']}:role/Lab18BedrockEvalRole"
JUEZ = {"bedrockEvaluatorModels": [{"modelIdentifier": "eu.amazon.nova-pro-v1:0"}]}
sufijo = time.strftime("%H%M%S")
def config(nombre_dataset, fichero, metricas):
return {"automated": {
"datasetMetricConfigs": [{
"taskType": "General",
"dataset": {"name": nombre_dataset, "datasetLocation": {"s3Uri": f"s3://{BUCKET}/entrada/{fichero}"}},
"metricNames": metricas,
}],
"evaluatorModelConfig": JUEZ,
}}
trabajos = []
trabajos.append(bedrock.create_evaluation_job(
jobName=f"lab18-juez-{sufijo}",
roleArn=ROL,
applicationType="ModelEvaluation",
evaluationConfig=config("golden", "juez.jsonl",
["Builtin.Correctness", "Builtin.Completeness", "Builtin.Relevance"]),
inferenceConfig={"models": [{"precomputedInferenceSource": {"inferenceSourceIdentifier": "nova-micro-v1"}}]},
outputDataConfig={"s3Uri": f"s3://{BUCKET}/salida/"},
)["jobArn"])
trabajos.append(bedrock.create_evaluation_job(
jobName=f"lab18-rag-gen-{sufijo}",
roleArn=ROL,
applicationType="RagEvaluation",
evaluationConfig=config("rag-gen", "rag_generacion.jsonl",
["Builtin.Faithfulness", "Builtin.Correctness", "Builtin.Completeness"]),
inferenceConfig={"ragConfigs": [{"precomputedRagSourceConfig": {
"retrieveAndGenerateSourceConfig": {"ragSourceIdentifier": "rag-politicas"}}}]},
outputDataConfig={"s3Uri": f"s3://{BUCKET}/salida/"},
)["jobArn"])
trabajos.append(bedrock.create_evaluation_job(
jobName=f"lab18-rag-ret-{sufijo}",
roleArn=ROL,
applicationType="RagEvaluation",
evaluationConfig=config("rag-ret", "rag_recuperacion.jsonl",
["Builtin.ContextRelevance", "Builtin.ContextCoverage"]),
inferenceConfig={"ragConfigs": [{"precomputedRagSourceConfig": {
"retrieveSourceConfig": {"ragSourceIdentifier": "rag-politicas"}}}]},
outputDataConfig={"s3Uri": f"s3://{BUCKET}/salida/"},
)["jobArn"])
with open("trabajos.txt", "w") as f:
f.write("\n".join(trabajos))
print("\n".join(trabajos))
python3 lanzar.py
Los identificadores nova-micro-v1 y rag-politicas de la configuración deben coincidir con los modelIdentifier y knowledgeBaseIdentifier del dataset.
Paso 4: espera y consulta el estado
Los trabajos tardan desde unos minutos hasta más de media hora.
for arn in $(cat trabajos.txt); do
aws bedrock get-evaluation-job --job-identifier $arn --query "[jobName,status,failureMessages]" --output text
done
Si alguno pasa a Failed, lee failureMessages: los errores típicos son un permiso del rol, un identificador que no coincide o un JSONL mal formado.
Paso 5: analiza los resultados
En la consola de Bedrock → Evaluations, abre cada trabajo. El report card muestra un histograma por métrica y la explicación del juez para las primeras respuestas. Los resultados completos, con la puntuación y la justificación de cada registro, están en S3:
aws s3 ls s3://$BUCKET/salida/ --recursive
Descarga el fichero de resultados de cada trabajo (aws s3 cp de la ruta que aparezca) y busca:
- Juez: la respuesta «Sídney» debe tener Correctness baja. Lee la explicación: el juez compara con la
referenceResponse. - RAG generación: el caso con «5 días extra por antigüedad» debe tener Faithfulness baja aunque la cifra de 23 días sea correcta: la respuesta afirma cosas que no están en los pasajes. Eso es una alucinación en RAG.
- RAG recuperación: el último caso (pasajes del comedor y del teletrabajo para una pregunta de formación) debe tener Context relevance y Context coverage bajas: el problema está en la recuperación, no en el modelo.
Comprueba que funciona
- Los tres trabajos terminan en
Completed. - Las puntuaciones bajas aparecen exactamente en los registros que estropeaste.
- Sabes explicar qué métrica apunta a un problema de generación (Faithfulness, Correctness) y cuál a uno de recuperación (Context relevance, Context coverage).
Limpieza
aws s3 rm s3://$BUCKET --recursive
aws s3 rb s3://$BUCKET
aws iam delete-role-policy --role-name $ROL --policy-name eval
aws iam delete-role --role-name $ROL
rm -f confianza.json permisos.json preparar.py lanzar.py juez.jsonl rag_generacion.jsonl rag_recuperacion.jsonl trabajos.txt
Los trabajos de evaluación terminados no generan coste; si quieres borrarlos de la lista, usa aws bedrock batch-delete-evaluation-job --job-identifiers con los ARN de trabajos.txt antes de borrar el fichero.
Preguntas para pensar como arquitecto
- ¿Por qué has usado BYOI en lugar de dejar que Bedrock invoque el modelo y la Knowledge Base?
Respuesta
Por coste y por flexibilidad: no hace falta desplegar una Knowledge Base ni un vector store, y el mismo mecanismo sirve para evaluar un RAG o un modelo que no está en Bedrock (otro proveedor, un modelo en SageMaker AI, un RAG propio). Con BYOI, Bedrock se salta la invocación y solo puntúa. Contrapartida: tú eres responsable de generar las respuestas de forma representativa.
- El caso de «5 días extra» tenía la cifra correcta. ¿Por qué la Faithfulness debe ser baja y qué control en tiempo real lo mitigaría?
Respuesta
Faithfulness mide si todo lo que afirma la respuesta está respaldado por el contexto recuperado; los días extra no lo están. En tiempo de ejecución, el contextual grounding check de Bedrock Guardrails bloquea respuestas cuya puntuación de grounding queda por debajo del umbral. La evaluación mide el problema en lote; el guardrail lo frena en producción.
- ¿Cómo convertirías este lab en una puerta de calidad de CI/CD?
Respuesta
Una etapa de CodeBuild en CodePipeline genera las respuestas de la nueva versión (prompt o modelo) sobre el golden dataset, lanza el trabajo de evaluación, espera a que termine, lee las puntuaciones de S3 y falla la etapa si alguna métrica baja del umbral o empeora frente a la versión en producción (prueba de regresión). Solo si pasa se despliega, idealmente de forma gradual (canary).
- ¿Qué limitación tiene usar el mismo modelo como generador y como juez?
Respuesta
El juez puede ser indulgente con los errores de su propio estilo (sesgo de autopreferencia). Es mejor un juez distinto y, en general, más capaz que el generador, y calibrarlo con una muestra revisada por personas.