Lab práctico · Semana 1: Fundamentos de IA generativa y selección de modelos en Amazon Bedrock

Comparar modelos y cambiarlos sin tocar código

⏱ 90-120 minDificultad: mediaTask statements: 1.11.2

Qué vas a construir

Un pequeño banco de pruebas (benchmark) en Python que envía el mismo conjunto de tareas a varios modelos de Amazon Bedrock con la Converse API y mide, para cada uno, latencia, tokens de entrada y salida, motivo de parada (stopReason) y coste estimado. Después harás que el modelo que usa tu «aplicación» se lea de AWS AppConfig, de modo que puedas cambiar de modelo o de parámetros sin modificar ni redesplegar el código, que es justo lo que pide la Skill 1.2.2.

flowchart LR
  A["bench.py (CloudShell)"] -->|"Converse"| B["eu.amazon.nova-micro-v1:0"]
  A -->|"Converse"| C["eu.amazon.nova-lite-v1:0"]
  A -->|"Converse"| D["eu.amazon.nova-2-lite-v1:0"]
  E["app.py"] -->|"GetLatestConfiguration"| F["AWS AppConfig (modelo + parámetros)"]
  E -->|"Converse con el modelo leído"| G["Amazon Bedrock"]

Aprenderás a:

  • Medir en vez de opinar: el examen premia la selección basada en datos (performance benchmarks, capability analysis, limitation evaluation).
  • Ver el efecto real de temperature y maxTokens.
  • Separar la configuración del modelo del código.

Antes de empezar

  • Haz primero el lab-01-cuenta-y-bedrock: necesitas la cuenta con presupuesto, la región elegida y la primera llamada funcionando.
  • Trabaja en AWS CloudShell en eu-central-1 (Fráncfort) o eu-west-1 (Irlanda). Los ejemplos usan eu-central-1.
  • En Europa, Nova Micro, Nova Lite y Nova 2 Lite no se sirven dentro de una única región: se invocan con el perfil de inferencia geográfico de la UE (prefijo eu.), que enruta la petición a regiones de la UE. Compruébalo en la ficha de cada modelo.
  • Todos los modelos de este lab son de Amazon: no necesitan suscripción de AWS Marketplace ni el formulario de Anthropic.

Paso 1: prepara el entorno

En CloudShell:

export AWS_REGION=eu-central-1
mkdir -p ~/lab02 && cd ~/lab02
python3 -c "import boto3; print(boto3.__version__)"

Comprueba que los tres perfiles de inferencia existen en tu región:

aws bedrock list-inference-profiles --region $AWS_REGION \
  --query "inferenceProfileSummaries[?contains(inferenceProfileId, 'nova')].[inferenceProfileId,status]" \
  --output table

Deberías ver, entre otros, eu.amazon.nova-micro-v1:0, eu.amazon.nova-lite-v1:0 y eu.amazon.nova-2-lite-v1:0.

Paso 2: el banco de pruebas

Crea bench.py. Fíjate en que el código es el mismo para los tres modelos: esa es la ventaja de la Converse API frente a InvokeModel, cuyo cuerpo JSON cambia según el proveedor.

import time
import boto3
from botocore.config import Config

REGION = "eu-central-1"
# Precios orientativos USD por millón de tokens (entrada, salida), eu-central-1, 01/10/2026.
MODELOS = {
    "eu.amazon.nova-micro-v1:0": (0.046, 0.184),
    "eu.amazon.nova-lite-v1:0": (0.078, 0.312),
    "eu.amazon.nova-2-lite-v1:0": (0.429, 3.597),
}

TAREAS = {
    "clasificar": "Clasifica este mensaje de un cliente en FACTURACION, SOPORTE o VENTAS. "
                  "Responde solo con la etiqueta.\nMensaje: Me habéis cobrado dos veces la cuota de septiembre.",
    "extraer": "Devuelve solo un JSON con las claves nombre, ciudad y fecha a partir de este texto: "
               "Laura Pérez visitará la oficina de Valencia el 14 de noviembre de 2026.",
    "razonar": "Un almacén tiene 3 estanterías con 4 baldas y 12 cajas por balda. "
               "Se retiran 30 cajas. ¿Cuántas quedan? Explica el cálculo en dos líneas.",
    "resumir": "Resume en una frase: Amazon Bedrock es un servicio totalmente gestionado que ofrece "
               "modelos fundacionales de varios proveedores mediante una única API, con opciones para "
               "personalizarlos y para construir agentes y aplicaciones RAG sin gestionar infraestructura.",
}

cliente = boto3.client(
    "bedrock-runtime",
    region_name=REGION,
    config=Config(retries={"total_max_attempts": 5, "mode": "adaptive"}),
)

def llamar(modelo, texto, temperatura=0.2, max_tokens=300):
    inicio = time.perf_counter()
    r = cliente.converse(
        modelId=modelo,
        system=[{"text": "Eres un asistente preciso y conciso. Responde en español."}],
        messages=[{"role": "user", "content": [{"text": texto}]}],
        inferenceConfig={"temperature": temperatura, "maxTokens": max_tokens},
    )
    total_ms = (time.perf_counter() - inicio) * 1000
    uso = r["usage"]
    precio_in, precio_out = MODELOS[modelo]
    coste = uso["inputTokens"] / 1e6 * precio_in + uso["outputTokens"] / 1e6 * precio_out
    return {
        "texto": r["output"]["message"]["content"][0]["text"].strip(),
        "stop": r["stopReason"],
        "entrada": uso["inputTokens"],
        "salida": uso["outputTokens"],
        "latencia_modelo_ms": r["metrics"]["latencyMs"],
        "latencia_total_ms": round(total_ms),
        "coste_usd": coste,
    }

if __name__ == "__main__":
    for modelo in MODELOS:
        print(f"\n=== {modelo} ===")
        coste_total = 0
        for nombre, texto in TAREAS.items():
            res = llamar(modelo, texto)
            coste_total += res["coste_usd"]
            print(f"[{nombre}] {res['latencia_modelo_ms']} ms | in {res['entrada']} / out {res['salida']} "
                  f"| stop={res['stop']} | {res['coste_usd']:.7f} USD")
            print("   ", res["texto"][:160].replace("\n", " "))
        print(f"Coste estimado del modelo: {coste_total:.6f} USD")

Ejecútalo:

python3 bench.py

Anota en una tabla, por modelo: latencia media, tokens de salida, si la extracción devolvió JSON válido y si el razonamiento es correcto (3 × 4 × 12 − 30 = 114).

Paso 3: juega con los parámetros de inferencia

Añade al final de bench.py (o en un fichero nuevo) este experimento: la misma petición creativa cinco veces con temperature baja y alta, y una prueba con maxTokens muy bajo.

from bench import llamar

pregunta = "Inventa un nombre original para una cafetería de Zaragoza. Responde solo con el nombre."
for t in (0.0, 1.0):
    nombres = [llamar("eu.amazon.nova-lite-v1:0", pregunta, temperatura=t, max_tokens=20)["texto"] for _ in range(5)]
    print(f"temperature={t}: {nombres}")

corta = llamar("eu.amazon.nova-micro-v1:0", "Explica qué es un embedding.", max_tokens=15)
print(corta["stop"], "->", corta["texto"])

Qué deberías observar:

  • Con temperature=0.0 los nombres se repiten mucho; con 1.0 varían. Baja temperatura = respuestas más deterministas.
  • Con maxTokens=15 la respuesta sale cortada y stopReason vale max_tokens. En producción, vigila ese valor: una respuesta truncada suele ser un JSON inválido.

Paso 4: la configuración del modelo, fuera del código (AWS AppConfig)

Ahora simulas la «aplicación». En vez de escribir el modelId en el código, lo guardas en AWS AppConfig como configuración freeform (JSON libre). Cambiar de modelo será desplegar una nueva versión de la configuración.

Crea la aplicación, el entorno y el perfil de configuración:

APP_ID=$(aws appconfig create-application --name genai-lab02 --query Id --output text)
ENV_ID=$(aws appconfig create-environment --application-id $APP_ID --name dev --query Id --output text)
PROF_ID=$(aws appconfig create-configuration-profile --application-id $APP_ID \
  --name modelo-llm --location-uri hosted --type AWS.Freeform --query Id --output text)
echo $APP_ID $ENV_ID $PROF_ID

Sube la primera versión de la configuración:

cat > config-v1.json <<'EOF'
{"modelId": "eu.amazon.nova-micro-v1:0", "temperature": 0.2, "maxTokens": 300}
EOF

aws appconfig create-hosted-configuration-version --application-id $APP_ID \
  --configuration-profile-id $PROF_ID --content-type application/json \
  --content fileb://config-v1.json version-v1.out
cat version-v1.out; echo

Despliégala con la estrategia predefinida que aplica el cambio de golpe (en producción usarías una estrategia gradual con alarmas de CloudWatch para revertir):

aws appconfig start-deployment --application-id $APP_ID --environment-id $ENV_ID \
  --configuration-profile-id $PROF_ID --configuration-version 1 \
  --deployment-strategy-id AppConfig.AllAtOnce

Crea app.py, que lee la configuración con la API appconfigdata y llama al modelo indicado:

import json
import sys
import boto3

REGION = "eu-central-1"
app_id, env_id, prof_id = sys.argv[1:4]

datos = boto3.client("appconfigdata", region_name=REGION)
sesion = datos.start_configuration_session(
    ApplicationIdentifier=app_id,
    EnvironmentIdentifier=env_id,
    ConfigurationProfileIdentifier=prof_id,
)
conf = json.loads(
    datos.get_latest_configuration(ConfigurationToken=sesion["InitialConfigurationToken"])["Configuration"].read()
)
print("Configuración leída:", conf)

bedrock = boto3.client("bedrock-runtime", region_name=REGION)
r = bedrock.converse(
    modelId=conf["modelId"],
    messages=[{"role": "user", "content": [{"text": "¿Qué es Amazon Bedrock? Una frase."}]}],
    inferenceConfig={"temperature": conf["temperature"], "maxTokens": conf["maxTokens"]},
)
print(r["output"]["message"]["content"][0]["text"])
python3 app.py $APP_ID $ENV_ID $PROF_ID

Cambia de modelo sin tocar app.py: sube la versión 2 y despliégala.

cat > config-v2.json <<'EOF'
{"modelId": "eu.amazon.nova-lite-v1:0", "temperature": 0.5, "maxTokens": 400}
EOF
aws appconfig create-hosted-configuration-version --application-id $APP_ID \
  --configuration-profile-id $PROF_ID --content-type application/json \
  --content fileb://config-v2.json version-v2.out
aws appconfig start-deployment --application-id $APP_ID --environment-id $ENV_ID \
  --configuration-profile-id $PROF_ID --configuration-version 2 \
  --deployment-strategy-id AppConfig.AllAtOnce
python3 app.py $APP_ID $ENV_ID $PROF_ID

Comprueba que funciona

  • bench.py imprime, para los tres modelos, latencia, tokens, stopReason=end_turn y un coste de millonésimas de dólar por tarea.
  • El experimento de maxTokens=15 devuelve stopReason=max_tokens.
  • app.py usa Nova Micro con la versión 1 de la configuración y Nova Lite con la 2, sin que hayas editado el script.

Limpieza

Borra lo creado en AppConfig, en orden de dependencias (versiones de configuración → perfil → entorno → aplicación). Los tokens no generan costes fijos.

for v in 1 2; do
  aws appconfig delete-hosted-configuration-version --application-id $APP_ID \
    --configuration-profile-id $PROF_ID --version-number $v
done
aws appconfig delete-configuration-profile --application-id $APP_ID --configuration-profile-id $PROF_ID
aws appconfig delete-environment --application-id $APP_ID --environment-id $ENV_ID
aws appconfig delete-application --application-id $APP_ID
rm -rf ~/lab02

Comprueba que no queda nada:

aws appconfig list-applications --query "Items[?Name=='genai-lab02']"

Preguntas para pensar como arquitecto

1. Nova Micro ha sacado la misma nota que Nova 2 Lite en tu tarea de clasificación, pero cuesta mucho menos. ¿Lo eliges ya para producción?

Todavía no. Cuatro tareas no son un conjunto de evaluación. Necesitas un conjunto de referencia representativo (cientos de ejemplos reales etiquetados), métricas objetivas y, a ser posible, una evaluación automática (Amazon Bedrock Evaluations, módulo 10). Si con datos suficientes Nova Micro cumple el umbral de calidad, sí: es la opción MOST cost-effective y además la de menor latencia.

2. El equipo de seguridad exige que los datos no salgan de la UE. ¿Puedes usar global.amazon.nova-2-lite-v1:0?

No. Un perfil global puede enrutar la petición a cualquier región comercial del mundo. Para residencia de datos en la UE usa el perfil geográfico eu., que solo enruta a regiones de la UE, o un modelo con disponibilidad In-Region si la norma exige una única región.

3. ¿Por qué guardar temperature y maxTokens en AppConfig y no solo el modelId?

Porque los parámetros forman parte del «contrato» del modelo: al pasar a otro modelo quizá necesites otra temperatura o más tokens de salida. Versionarlos juntos permite desplegarlos de forma gradual, validarlos con un esquema JSON y revertir en bloque si una alarma de CloudWatch detecta más errores o más latencia.

4. ¿Qué harías si el modelo elegido empieza a devolver ThrottlingException en horas punta?

Por orden: reintentos con backoff exponencial y jitter (el SDK en modo adaptive ya lo hace), usar un perfil de cross-Region inference para repartir la carga entre regiones, pedir un aumento de cuota en Service Quotas y, si el tráfico es estable y alto, valorar capacidad reservada (niveles Reserved o Provisioned Throughput). Como degradación elegante, un modelo alternativo más pequeño configurado en AppConfig.


Volver al módulo