Lab práctico · Semana 4: Ingeniería de prompts, gobierno y APIs de los modelos
Prompt Management y Flows con versiones, regresión y alias
Qué vas a construir
Un clasificador de tickets de soporte gobernado como un artefacto de software:
- Un prompt gestionado en Amazon Bedrock Prompt Management, con variables, publicado como versión 1.
- Una prueba de regresión con un conjunto de oro que calcula la exactitud y la publica como métrica de CloudWatch por versión.
- Una versión 2 mejorada con few-shot, comparada con la 1 antes de «aprobarla».
- Un Amazon Bedrock Flow (la guía los llama Prompt Flows) con rama condicional que reutiliza el prompt gestionado, desplegado con versión y alias, y un rollback cambiando el alias.
- La auditoría de los cambios en CloudTrail.
flowchart LR
PM["Prompt Management: DRAFT"] --> V1["Versión 1"]
PM --> V2["Versión 2 (few-shot)"]
V1 --> REG["Regresión: exactitud por versión en CloudWatch"]
V2 --> REG
V2 --> FLOW["Flow: Input, Condition, Prompt, Outputs"]
FLOW --> FV["Versión del Flow"]
FV --> ALIAS["Alias prod"]
APP["InvokeFlow"] --> ALIAS
CT["CloudTrail"] -.-> PM
Antes de empezar
- Región: eu-central-1. Modelo: Amazon Nova Micro mediante el perfil EU
eu.amazon.nova-micro-v1:0. - CloudShell en eu-central-1 con boto3 actualizado:
pip3 install --user --upgrade boto3 botocore
export AWS_REGION=eu-central-1
export CUENTA=$(aws sts get-caller-identity --query Account --output text)
mkdir -p ~/lab07 && cd ~/lab07
Paso 1: crea el prompt con variables
Plantilla CHAT con system prompt, un mensaje de usuario con dos variables (categorias y ticket) y parámetros deterministas. Las variables se escriben entre dobles llaves dentro del texto:
cat > variantes-v1.json <<'EOF'
[
{
"name": "v1",
"modelId": "eu.amazon.nova-micro-v1:0",
"templateType": "CHAT",
"templateConfiguration": {
"chat": {
"system": [{ "text": "Eres un clasificador de tickets de soporte de una operadora de telecomunicaciones. Responde solo con una categoría, en minúsculas y sin tildes." }],
"messages": [{ "role": "user", "content": [{ "text": "Categorías posibles: {{categorias}}\nTicket: {{ticket}}\nCategoría:" }] }],
"inputVariables": [{ "name": "categorias" }, { "name": "ticket" }]
}
},
"inferenceConfiguration": { "text": { "maxTokens": 10, "temperature": 0 } }
}
]
EOF
export PROMPT_ID=$(aws bedrock-agent create-prompt \
--name lab07-clasificador \
--description "Clasificador de tickets (lab 07)" \
--default-variant v1 \
--variants file://variantes-v1.json \
--region $AWS_REGION --query id --output text)
echo "PROMPT_ID=$PROMPT_ID"
Si quieres verlo en la consola: Amazon Bedrock → Prompt management → lab07-clasificador → Edit in prompt builder. Rellena las variables de prueba y pulsa Run.
Paso 2: publica la versión 1 e invócala desde Converse
El borrador (DRAFT) se puede seguir editando; la versión es una instantánea inmutable, que es lo que debe usar producción.
export PROMPT_V1=$(aws bedrock-agent create-prompt-version --prompt-identifier $PROMPT_ID \
--description "Versión inicial zero-shot" --region $AWS_REGION --query arn --output text)
echo "PROMPT_V1=$PROMPT_V1" # termina en :1
Guarda invocar.py:
import os, sys, boto3
brt = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
CATEGORIAS = "facturacion, averia, portabilidad, baja, otros"
def clasificar(prompt_arn, ticket):
r = brt.converse(
modelId=prompt_arn,
promptVariables={"categorias": {"text": CATEGORIAS}, "ticket": {"text": ticket}},
)
return r["output"]["message"]["content"][0]["text"].strip().lower().rstrip(".")
if __name__ == "__main__":
print(clasificar(os.environ["PROMPT_V1"], "Me han cobrado dos veces la factura de agosto."))
# Prueba la restricción: con un prompt gestionado no se puede enviar inferenceConfig
try:
brt.converse(modelId=os.environ["PROMPT_V1"],
promptVariables={"categorias": {"text": CATEGORIAS}, "ticket": {"text": "hola"}},
inferenceConfig={"temperature": 0.9})
except brt.exceptions.ValidationException as e:
print("ValidationException esperada:", str(e)[:120])
python3 invocar.py
Comprueba dos lecciones de examen: el modelId es el ARN de la versión del prompt y las variables van en promptVariables; y si intentas enviar inferenceConfig (o system, toolConfig, additionalModelRequestFields), la llamada falla, porque esos campos los fija el prompt gestionado. Tu identidad necesita bedrock:RenderPrompt sobre el prompt.
Paso 3: prueba de regresión con conjunto de oro y métrica en CloudWatch
Guarda regresion.py. Ejecuta el conjunto de oro contra una versión, valida que la salida sea una categoría permitida y publica la exactitud en CloudWatch con la dimensión PromptVersion:
import os, sys, boto3
from invocar import clasificar
PERMITIDAS = {"facturacion", "averia", "portabilidad", "baja", "otros"}
ORO = [
("Me habéis cobrado 15 euros de más este mes.", "facturacion"),
("No tengo línea desde ayer por la tarde.", "averia"),
("Quiero llevarme mi número a otra compañía.", "portabilidad"),
("Deseo cancelar mi contrato cuanto antes.", "baja"),
("¿Cuál es el horario de vuestra tienda de Sevilla?", "otros"),
("El router parpadea en rojo y no hay internet.", "averia"),
("La factura viene a nombre de mi antiguo piso.", "facturacion"),
("Me mudo al extranjero y no voy a seguir con vosotros.", "baja"),
]
def evaluar(prompt_arn, etiqueta_version):
aciertos = formato_ok = 0
for ticket, esperada in ORO:
salida = clasificar(prompt_arn, ticket)
formato_ok += salida in PERMITIDAS
aciertos += salida == esperada
print(f" {'OK ' if salida == esperada else 'MAL'} esperada={esperada:<12} obtenida={salida}")
exactitud = aciertos / len(ORO)
boto3.client("cloudwatch", region_name=os.environ["AWS_REGION"]).put_metric_data(
Namespace="Lab07/Prompts",
MetricData=[
{"MetricName": "Exactitud", "Value": exactitud, "Unit": "None",
"Dimensions": [{"Name": "PromptVersion", "Value": etiqueta_version}]},
{"MetricName": "FormatoValido", "Value": formato_ok / len(ORO), "Unit": "None",
"Dimensions": [{"Name": "PromptVersion", "Value": etiqueta_version}]},
],
)
print(f"Versión {etiqueta_version}: exactitud={exactitud:.2f} formato={formato_ok}/{len(ORO)}")
return exactitud
if __name__ == "__main__":
evaluar(sys.argv[1], sys.argv[2])
python3 regresion.py "$PROMPT_V1" v1
Esto es la skill 1.6.4 en pequeño: un validador (en producción, una Lambda), un conjunto de casos (en producción, cientos, orquestados con un estado Map de Step Functions) y una métrica de regresión en CloudWatch sobre la que puedes poner una alarma.
Paso 4: mejora el prompt (few-shot) y publica la versión 2
Actualiza el borrador con ejemplos few-shot y reglas de desempate. update-prompt sustituye la definición completa del borrador:
cat > variantes-v2.json <<'EOF'
[
{
"name": "v1",
"modelId": "eu.amazon.nova-micro-v1:0",
"templateType": "CHAT",
"templateConfiguration": {
"chat": {
"system": [{ "text": "Eres un clasificador de tickets de soporte de una operadora de telecomunicaciones. Responde solo con una categoría de la lista, en minúsculas y sin tildes. Si el cliente quiere irse a otra compañía conservando el número, es portabilidad; si quiere dejar de ser cliente sin más, es baja; problemas técnicos de línea, router o cobertura son averia." }],
"messages": [
{ "role": "user", "content": [{ "text": "Categorías posibles: facturacion, averia, portabilidad, baja, otros\nTicket: El móvil no tiene cobertura en casa.\nCategoría:" }] },
{ "role": "assistant", "content": [{ "text": "averia" }] },
{ "role": "user", "content": [{ "text": "Categorías posibles: facturacion, averia, portabilidad, baja, otros\nTicket: Me paso a otra operadora con mi mismo número.\nCategoría:" }] },
{ "role": "assistant", "content": [{ "text": "portabilidad" }] },
{ "role": "user", "content": [{ "text": "Categorías posibles: {{categorias}}\nTicket: {{ticket}}\nCategoría:" }] }
],
"inputVariables": [{ "name": "categorias" }, { "name": "ticket" }]
}
},
"inferenceConfiguration": { "text": { "maxTokens": 10, "temperature": 0 } }
}
]
EOF
aws bedrock-agent update-prompt --prompt-identifier $PROMPT_ID --name lab07-clasificador \
--default-variant v1 --variants file://variantes-v2.json --region $AWS_REGION >/dev/null
export PROMPT_V2=$(aws bedrock-agent create-prompt-version --prompt-identifier $PROMPT_ID \
--description "Few-shot y reglas de desempate" --region $AWS_REGION --query arn --output text)
echo "PROMPT_V2=$PROMPT_V2" # termina en :2
python3 regresion.py "$PROMPT_V2" v2
python3 regresion.py "$PROMPT_V1" v1 # la versión 1 sigue intacta: las versiones son inmutables
Abre CloudWatch → Metrics → All metrics → Lab07/Prompts → PromptVersion y compara Exactitud de v1 y v2. En un flujo de gobierno real, este paso lo haría CodeBuild dentro de CodePipeline, con una aprobación manual antes de publicar el ARN de la nueva versión en AWS AppConfig.
Paso 5: construye el Flow en la consola
Vas a crear este flujo: si el cliente es premium, se clasifica el ticket con el prompt gestionado (versión 2) y se devuelve la categoría; si no, se devuelve un mensaje fijo de cola estándar.
flowchart LR
IN["Flow input (Object)"] --> C{"Condition: plan == premium"}
C -- "premium" --> P["Prompt (Prompt Management, versión 2)"]
P --> O1["Output: categoria"]
C -- "default" --> O2["Output: cola_estandar"]
- Consola de Amazon Bedrock → Flows → Create flow. Nombre
lab07-flow. En Service role, elige Create and use a new service role (la consola añadirá los permisos de cada nodo, incluidobedrock:RenderPrompt). - En el Flow builder, selecciona el nodo Flow input y cambia su tipo de salida a Object. El flujo recibirá un JSON como el del paso 7, con los campos
ticketyplan. - Arrastra un nodo Condition. En Inputs define una entrada
plande tipo String con la expresión$.data.plany conéctala a la salida del nodo de entrada. En Conditions, añade una condición llamadapremiumcon la expresiónplan == "premium". Deja la condición por defecto. - Configura el nodo Prompt que trae el flujo por defecto (o arrastra uno): elige Use a prompt from your Prompt management, selecciona
lab07-clasificadory la versión 2. Aparecerán sus entradascategoriasyticket:ticket: tipo String, expresión$.data.ticket, conectada a la salida del nodo de entrada.categorias: tipo String, expresión$.data.categorias, también desde el nodo de entrada (enviaremos las categorías en la petición; así el prompt es reutilizable).
- Conecta la condición
premiumal nodo Prompt, y la salidamodelCompletiondel Prompt al nodo Flow output (renómbralo acategoria). - Añade un segundo Flow output llamado
cola_estandar, con entrada de tipo String y expresión$.data.ticket, conectado a la salida del nodo de entrada. Conecta la condición default a este nodo. - Pulsa Save (la consola prepara el flujo) y pruébalo en el panel Test flow con los dos JSON del paso 7.
Paso 6: versión, alias e invocación desde código
export FLOW_ID=$(aws bedrock-agent list-flows --region $AWS_REGION \
--query "flowSummaries[?name=='lab07-flow'].id" --output text)
aws bedrock-agent create-flow-version --flow-identifier $FLOW_ID --region $AWS_REGION \
--query version --output text # 1
export ALIAS_ID=$(aws bedrock-agent create-flow-alias --flow-identifier $FLOW_ID --name prod \
--routing-configuration '[{"flowVersion":"1"}]' --region $AWS_REGION --query id --output text)
echo "FLOW_ID=$FLOW_ID ALIAS_ID=$ALIAS_ID"
Paso 7: invoca el alias
Guarda flow.py:
import os, boto3
art = boto3.client("bedrock-agent-runtime", region_name=os.environ["AWS_REGION"])
def invocar(ticket, plan):
doc = {"ticket": ticket, "plan": plan, "categorias": "facturacion, averia, portabilidad, baja, otros"}
r = art.invoke_flow(
flowIdentifier=os.environ["FLOW_ID"], flowAliasIdentifier=os.environ["ALIAS_ID"],
inputs=[{"nodeName": "FlowInputNode", "nodeOutputName": "document", "content": {"document": doc}}],
)
for ev in r["responseStream"]:
if "flowOutputEvent" in ev:
print(f"[{plan}] {ev['flowOutputEvent']['nodeName']}: {ev['flowOutputEvent']['content']['document']}")
if "flowCompletionEvent" in ev:
print(" fin:", ev["flowCompletionEvent"]["completionReason"])
invocar("El router no enciende desde esta mañana.", "premium")
invocar("El router no enciende desde esta mañana.", "basico")
python3 flow.py
Si tu nodo de entrada tiene otro nombre en la consola (por defecto es FlowInputNode), cámbialo en nodeName.
Paso 8: rollback con el alias
Simula una versión defectuosa: en la consola edita el nodo Prompt para que use la versión 1 del prompt, guarda y crea la versión 2 del Flow. Apunta el alias a ella y, después, vuelve atrás sin tocar el código de la aplicación:
aws bedrock-agent create-flow-version --flow-identifier $FLOW_ID --region $AWS_REGION --query version --output text # 2
aws bedrock-agent update-flow-alias --flow-identifier $FLOW_ID --alias-identifier $ALIAS_ID --name prod \
--routing-configuration '[{"flowVersion":"2"}]' --region $AWS_REGION
python3 flow.py
# Rollback: el alias vuelve a la versión 1
aws bedrock-agent update-flow-alias --flow-identifier $FLOW_ID --alias-identifier $ALIAS_ID --name prod \
--routing-configuration '[{"flowVersion":"1"}]' --region $AWS_REGION
Paso 9: auditoría con CloudTrail
Las llamadas de gestión a Prompt Management y Flows quedan en el historial de eventos de CloudTrail (skill 1.6.3):
for EV in CreatePrompt UpdatePrompt CreatePromptVersion CreateFlowAlias UpdateFlowAlias; do
aws cloudtrail lookup-events --region $AWS_REGION --max-results 3 \
--lookup-attributes AttributeKey=EventName,AttributeValue=$EV \
--query 'Events[].[EventTime,EventName,Username]' --output text
done
Los eventos pueden tardar unos minutos en aparecer. Para registrar además el contenido de prompts y respuestas, activarías el model invocation logging de Bedrock hacia CloudWatch Logs o S3 (módulo 08).
Comprueba que funciona
-
invocar.pyclasifica el ticket y muestra laValidationExceptionesperada al enviarinferenceConfig. - CloudWatch muestra
Exactitudpara v1 y v2 enLab07/Prompts. -
flow.pydevuelve una categoría parapremiumy el ticket encola_estandarparabasico. - Tras el rollback, el alias
prodapunta a la versión 1 del Flow. - CloudTrail lista los eventos de creación y actualización.
Limpieza
Orden: alias → versiones del Flow → Flow → prompt → rol creado por la consola → ficheros locales.
aws bedrock-agent delete-flow-alias --flow-identifier $FLOW_ID --alias-identifier $ALIAS_ID --region $AWS_REGION
for V in 1 2; do
aws bedrock-agent delete-flow-version --flow-identifier $FLOW_ID --flow-version $V --region $AWS_REGION 2>/dev/null
done
aws bedrock-agent delete-flow --flow-identifier $FLOW_ID --region $AWS_REGION
aws bedrock-agent delete-prompt --prompt-identifier $PROMPT_ID --region $AWS_REGION
# Rol de servicio que creó la consola para el Flow (su nombre empieza por AmazonBedrockExecutionRoleForFlows)
aws iam list-roles --query "Roles[?starts_with(RoleName,'AmazonBedrockExecutionRoleForFlows')].RoleName" --output text
Para cada rol listado que corresponda a lab07-flow, borra sus políticas y el rol (en la consola de IAM es más cómodo: Roles → el rol → Delete). Por último, rm -rf ~/lab07. La métrica Lab07/Prompts de CloudWatch no se puede borrar: deja de cobrarse al no recibir datos y caduca sola.
Preguntas para pensar como arquitecto
1. ¿Cómo diseñarías un flujo de aprobación para que ningún prompt llegue a producción sin revisión?
Plantillas en un repositorio (S3 con versionado o Git). CodePipeline: CodeBuild ejecuta la regresión contra el conjunto de oro y publica métricas; si no hay regresión, acción de aprobación manual; tras aprobar, un paso llama a CreatePromptVersion y publica el ARN en AppConfig. IAM: solo el rol del pipeline puede crear versiones; las aplicaciones solo tienen bedrock:RenderPrompt e InvokeModel. CloudTrail audita todo.
2. Un equipo quiere cambiar el modelo del clasificador de Nova Micro a otro. ¿Qué ventaja te da haberlo hecho con Prompt Management?
El modelo forma parte de la variante del prompt: se crea una variante o versión nueva con el otro modelo, se pasa la misma regresión y, si es mejor, se publica. La aplicación sigue llamando a Converse con un ARN de versión y promptVariables; no cambia su código, solo el ARN configurado (o el alias del Flow).
3. ¿Cuándo usarías Step Functions en vez de Bedrock Flows para este clasificador?
Si necesito reintentos y captura de errores por paso, esperas largas o aprobaciones humanas (task token), integración directa con muchos servicios de AWS, ejecuciones de larga duración con historial detallado, o procesamiento masivo con estados Map distribuidos. Flows encaja cuando el objetivo es encadenar prompts y KB de forma visual y sin código.
4. ¿Cómo harías una prueba A/B entre las versiones 1 y 2 del prompt en producción?
Enrutar un porcentaje del tráfico a cada versión (lógica en la aplicación con la configuración en AppConfig, o dos aliases/Flows) y registrar la versión en requestMetadata y en los logs; medir exactitud (con feedback o revisión muestreada), latencia y tokens por versión en CloudWatch; decidir con significancia suficiente. Es el patrón de 3.4.2 con Prompt Management y Flows.