Dejé que AWS reescribiera el prompt de mi agente DevOps: 142 sesiones, +6.2% y un p-value de 0.38

Tabla de Contenidos

  1. El Desafío: El Ciclo que Nadie Cierra
  2. El Punto de Partida: Portar el Agente (y Por Qué No Había Alternativa)
    1. Las Tools con el Decorador de Strands
    2. El Hook que Lee el Bundle
    3. La Decisión de Apagar la Memoria
  3. Implementación Paso a Paso
    1. Paso 1: Aprovisionar Runtime, Online Eval y Gateway
    2. Paso 2: Generar la Línea Base
    3. Paso 3: Pedir la Recomendación
  4. Resultados: Qué Encontró el Optimizador
  5. Validación: Bundles, A/B y el Veredicto Incómodo
    1. Por Qué el Resultado “Malo” es el Hallazgo Bueno
  6. Promoción: Donde el CLI se Quedó Corto
  7. Lecciones Aprendidas
    1. El CLI Tiene un Homónimo Deprecado que te Arruina la Tarde
    2. Los Prerrequisitos Fallan en el Orden Equivocado
    3. El Motor de Análisis Corre en su Propio Ciclo
    4. Los Dos Planos, Otra Vez
    5. El Costo es Sorprendentemente Bajo
  8. Conclusión
  9. Recursos Oficiales 📚

Dejé que AWS reescribiera el prompt de mi agente DevOps: 142 sesiones, +6.2% y un p-value de 0.38

Cerré el artículo de Memoria Episódica con una frase que me quedó dando vueltas por meses: “hay un momento en el desarrollo de un agente en el que ya no puedes seguir mejorándolo solo con prompts”.

En ese momento propuse una respuesta: que el agente aprenda de su propia experiencia. Pero había otra respuesta que dejé pendiente, y es bastante más incómoda: ¿y si el problema no es que el prompt no alcance, sino que yo no soy la persona indicada para seguir escribiéndolo?

Piénsalo un segundo. Cuando ajustas el system prompt de un agente en producción, ¿en qué te basas? En los tres o cuatro casos que recuerdas que salieron mal. En la intuición de que “esta instrucción debería ayudar”. En un puñado de pruebas manuales que hiciste después del cambio. Nunca en los 200 traces reales que están sentados en CloudWatch esperando que alguien los lea.

Amazon Bedrock AgentCore Optimization propone exactamente eso: que un modelo lea los traces por ti, encuentre el patrón de fallo, y te devuelva un prompt mejorado. Y después —esta es la parte que me interesó de verdad— que valides ese cambio con un A/B test sobre tráfico real, con significancia estadística, antes de comprometerlo.

Lo corrí completo sobre el mismo agente DevOps de la serie. Esto es lo que pasó: la recomendación fue sorprendentemente buena, el A/B test me dijo que no había evidencia suficiente para declararla ganadora, y en el camino fui anotando cada trampa que la documentación no menciona — están repartidas como ProTips a lo largo del artículo. Todo el código está en github.com/codecr/agentcore-optimization-loop.

🎯 ProTip #1: Ojo con el estado de madurez, porque cambió hace poco y buena parte de lo que vas a encontrar escrito está desactualizado. Según las release notes, el Agent Optimization Loop entró en public preview en abril de 2026, pero Recommendations, A/B Testing y Batch Evaluations llegaron a GA en junio de 2026. Lo único que sigue en preview es Failure Insights, y AgentCore Evaluations —la base sobre la que todo esto se apoya— es GA desde marzo de 2026. Esto ya no es un juguete de preview: es superficie de producción. Dicho eso, GA no significa completo. En mi cuenta las llamadas a estas APIs no aparecieron en el event history de CloudTrail, y aunque la documentación tiene páginas dedicadas de CloudTrail para Gateway, Runtime y Harness, no encontré la equivalente para optimization. No afirmo que el soporte no exista; afirmo que no lo vi. Si tu proceso exige audit trail de quién cambió la configuración del agente, compruébalo en tu cuenta antes de asumirlo, porque ese es justamente uno de los argumentos de venta de los configuration bundles.

El Desafío: El Ciclo que Nadie Cierra

La mayoría de los equipos que conozco tienen las dos primeras piezas del ciclo y les falta la tercera.

Observan: tienen observabilidad, traces en CloudWatch, dashboards. Evalúan: si ya leyeron el artículo de AgentCore Evaluations, tienen scores de calidad corriendo sobre tráfico real. Pero cuando llega el momento de mejorar, el ciclo se rompe y vuelve a ser artesanal: alguien abre el archivo del prompt, escribe unas líneas nuevas basado en su lectura personal de los datos, y despliega esperando lo mejor.

AgentCore Optimization ataca ese último tramo con tres capacidades que trabajan juntas:

Recommendations — Le apuntas a los traces de tu agente en CloudWatch Logs y le dices cuál evaluador quieres optimizar. El servicio analiza los patrones de fallo y te devuelve un system prompt optimizado (o descripciones de tools más precisas), con una explicación de qué cambió y por qué.

Configuration bundles — Snapshots inmutables y versionados de la configuración de tu agente: system prompts, model IDs, descripciones de tools. La analogía con git no es mía ni es aproximada: es literalmente el modelo de datos. Cada versión lleva commitMessage, y parentVersionIds está documentado como “los commits regulares tienen un solo padre; los merge commits tienen dos: el padre de la rama destino y el de la rama origen”. Hay branchName, que si lo omites hereda la rama del padre o cae en una por defecto llamada mainline. Y el CLI expone agentcore cb versions y agentcore cb diff --from <v1> --to <v2>. Todo eso está en la referencia de UpdateConfigurationBundle y en el tipo VersionLineageMetadata de CloudFormation. Desacoplan el comportamiento del agente del código, así que puedes cambiar cómo responde sin redesplegar. Son opcionales: también puedes validar desplegando a un runtime endpoint separado.

A/B testing — Divide el tráfico en vivo entre dos variantes a través del AgentCore Gateway. La asignación es sticky por session ID, el online evaluation puntúa cada sesión, y el servicio calcula media, cambio porcentual, p-value, intervalo de confianza y una bandera de significancia.

La promesa junta es un loop que se retroalimenta: los traces de la variante ganadora se vuelven la nueva línea base para la siguiente recomendación.

🔍 ProTip #2: El evaluador que elijas es la función objetivo de la optimización. El optimizador empuja el prompt hacia lo que ese evaluador puntúa alto, con todo lo bueno y lo malo que eso implica. Si tu agente tiene una tarea clara que completar, Builtin.GoalSuccessRate es la señal correcta. Si es más abierto y te importa la calidad de la interacción, Builtin.Helpfulness encaja mejor. Elegir mal aquí no te da un mal prompt: te da un prompt excelente para la métrica equivocada.

El Punto de Partida: Portar el Agente (y Por Qué No Había Alternativa)

Aquí viene la primera lección dura del ejercicio, y llegó antes de escribir una sola línea de código nuevo.

El agente DevOps de la serie de Memory era una aplicación Flask local que llamaba a Bedrock Converse directo. Funcionaba perfecto para lo que necesitaba ese artículo. Pero no puede entrar al loop de optimización, y por dos razones que no se negocian:

  1. Recommendations y online evaluation leen traces desde CloudWatch bajo la convención de service name {RuntimeName}.DEFAULT. Eso implica un agente desplegado en AgentCore Runtime con observabilidad, o un framework soportado (Strands Agents, o LangGraph con instrumentación de OpenTelemetry/OpenInference).
  2. Para que el A/B test pueda intercambiar el system prompt entre variantes, el agente tiene que leer su prompt desde el configuration bundle en runtime, no tenerlo hardcodeado.

Así que “reusar mi agente” en realidad significó portarlo a Strands sobre AgentCore Runtime. No fue un retoque: fue un port.

Las Tools con el Decorador de Strands

Al portar a Strands, las tools se cablean con el decorador @tool y el framework se encarga del resto:

@tool
def describe_rds_metrics(instance_id: str, period_minutes: int = 30) -> dict:
    """Obtiene métricas de CloudWatch de una instancia RDS: conexiones, CPU,
    memoria libre y latencias de lectura/escritura. Úsala cuando el incidente
    involucre timeouts, saturación o lentitud de base de datos.

    Args:
        instance_id: Identificador de la instancia RDS (ej. prod-db-1).
        period_minutes: Ventana de tiempo hacia atrás en minutos.
    """
    end_time = datetime.now(timezone.utc)
    start_time = end_time - timedelta(minutes=period_minutes)
    metrics = ["DatabaseConnections", "CPUUtilization", "FreeableMemory",
               "ReadLatency", "WriteLatency"]
    result = {}
    for name in metrics:
        try:
            resp = _cloudwatch.get_metric_statistics(
                Namespace="AWS/RDS",
                MetricName=name,
                Dimensions=[{"Name": "DBInstanceIdentifier", "Value": instance_id}],
                StartTime=start_time,
                EndTime=end_time,
                Period=300,
                Statistics=["Average", "Maximum"],
            )
            if resp["Datapoints"]:
                latest = sorted(resp["Datapoints"], key=lambda x: x["Timestamp"])[-1]
                if name == "FreeableMemory":
                    result[f"{name}_GB"] = round(latest["Average"] / (1024 ** 3), 2)
                elif "Latency" in name:
                    result[f"{name}_ms"] = round(latest["Average"] * 1000, 2)
                else:
                    result[name] = round(latest["Average"], 2)
            else:
                result[name] = None
        except Exception as e:
            # Si el rol del runtime no tiene permisos, esto aflora como dato, no como caída
            result[name] = f"Error: {e}"
    return result

Ese detalle del docstring no es cosmético: en Strands, la descripción de la tool es lo que ve el modelo para decidir si la usa. Es literalmente el artefacto que optimiza la recomendación de tool descriptions.

El Hook que Lee el Bundle

La pieza clave del port es el hook BeforeModelCallEvent. Se dispara antes de cada llamada al modelo y aplica el system prompt del bundle activo:

def _resolve_system_prompt() -> str:
    """Lee el system prompt base del config bundle; si no hay, usa el DEFAULT."""
    try:
        config = BedrockAgentCoreContext.get_config_bundle()
        return config.get("system_prompt", DEFAULT_SYSTEM_PROMPT) if config else DEFAULT_SYSTEM_PROMPT
    except Exception:
        # Fallback defensivo: nunca dejamos al agente sin prompt
        return DEFAULT_SYSTEM_PROMPT


def dynamic_config_hook(event: BeforeModelCallEvent):
    """Antes de cada llamada al modelo, aplica el prompt del bundle activo.
    Durante un A/B test, el Gateway propaga por baggage qué versión de bundle
    corresponde a esta sesión (control o treatment), y aquí se materializa.
    """
    event.agent.system_prompt = _resolve_system_prompt()


agent = Agent(
    model=BedrockModel(model_id=DEFAULT_MODEL_ID),
    tools=TOOLS,
    system_prompt=DEFAULT_SYSTEM_PROMPT,
)
agent.hooks.add_callback(BeforeModelCallEvent, dynamic_config_hook)

Lo elegante del diseño: el código del agente nunca sabe en qué variante está. El Gateway asigna la sesión, inyecta la referencia del bundle vía headers de W3C Baggage con dos claves —aws.agentcore.configbundle_arn y aws.agentcore.configbundle_version—, el BedrockAgentCoreApp las parsea, resuelve la versión contra el control plane, cachea el resultado, y get_config_bundle() devuelve la configuración del componente que corresponde a tu runtime ARN. Si no hay referencia de bundle en el request, devuelve un diccionario vacío y cae al default.

Vale aclarar que este patrón no lo inventé: el hook BeforeModelCallEvent es exactamente el patrón recomendado en la documentación para agentes Strands, y ahí están también las variantes para LangGraph, Google ADK y el SDK de OpenAI. Si necesitas aplicar más campos que el prompt —model ID, temperature, tools— la doc sugiere construir el agente por request en lugar de usar el hook. Un extra útil que descubrí leyéndola: existe get_config_bundle_ref() para inspeccionar la referencia cruda (bundle_id, bundle_arn, bundle_version), que es la forma limpia de loggear en qué variante corrió una sesión sin que el agente cambie su comportamiento por saberlo. Y para probar en local puedes pasar baggage= a mano en invoke_agent_runtime, sin montar un A/B test.

⚠️ ProTip #3: get_config_bundle() existe a partir de bedrock-agentcore >= 1.8.0. Este piso sí está documentado: los prerrequisitos de A/B testing exigen “AgentCore SDK (version 1.8+)” porque el BaggageSpanProcessor del SDK es el que adjunta el ARN del experimento y el nombre de la variante a los spans de OpenTelemetry. Los otros dos pisos los deduje a golpes y no fallan de forma obvia si te quedas corto: strands-agents[otel] >= 1.13.0 y botocore[crt] >= 1.35.0. Y siempre envuelve la lectura en un try/except con defaults — si la llamada al control plane falla, la excepción se propaga y te tumba la invocación completa.

La Decisión de Apagar la Memoria

Tomé una decisión que vale la pena explicar porque va contra la intuición: durante el A/B test dejé la memoria episódica apagada.

El motivo es puramente experimental. La inyección dinámica de episodios y reflections cambia el contexto de cada sesión según lo que la búsqueda semántica recupere en ese momento. Eso mete varianza que no tiene nada que ver con el cambio que estoy midiendo. Si control y treatment difieren en el prompt base y además en qué experiencias recuperaron, ya no puedo atribuir la diferencia al prompt.

Con memoria apagada, lo único que difiere entre C y T1 es el system prompt. A/B limpio. En operación normal se enciende con ENABLE_MEMORY=true.

🧪 ProTip #4: Todo componente que agregue varianza por sesión —memoria episódica, RAG con recuperación dinámica, contexto de usuario— encarece tu A/B test en muestras. No es que esté mal tenerlo encendido; es que necesitas más sesiones para separar la señal del ruido. Si tu experimento es sobre el prompt, aísla el prompt.

Implementación Paso a Paso

Todo el camino va por el AgentCore CLI, que por debajo administra un stack de CDK. Esta fue mi primera decisión de arquitectura del ejercicio y la cambié a mitad de camino: mi convención es Terraform, pero el soporte de provider para las capacidades nuevas de AgentCore va por detrás, y el CLI ya tiene comandos de primera clase para bundles, online eval y A/B tests. Peleé menos y llegué más lejos.

Paso 1: Aprovisionar Runtime, Online Eval y Gateway

# Proyecto y runtime del agente
agentcore create --name DevOpsOptimizationLoop --no-agent
cd DevOpsOptimizationLoop

agentcore add agent \
  --name devopsAgent \
  --language Python \
  --framework Strands \
  --model-provider Bedrock \
  --memory none \
  --build CodeZip

agentcore deploy

# Online evaluation: puntúa cada sesión en vivo
agentcore add online-eval \
  --name devopsEval \
  --runtime devopsAgent \
  --evaluator "Builtin.GoalSuccessRate" \
  --sampling-rate 100.0 \
  --enable-on-create

# Gateway y target: el A/B por config bundle rutea por aquí
agentcore add gateway --name devopsGateway

agentcore add gateway-target \
  --name devops-diag \
  --gateway devopsGateway \
  --type http-runtime \
  --runtime devopsAgent

agentcore deploy

El --sampling-rate 100.0 es deliberado. En producción normal usarías 10% o menos por costo, pero durante un A/B test quieres que cada sesión se evalúe: entre más muestras puntuadas, antes llegas a significancia. Lo bajas cuando termina el experimento.

Runtime desplegado con observabilidad: 31 sesiones y 31 invocaciones Figura 1: El runtime durante la fase de línea base — 31 sesiones y 31 invocaciones. Nótese que ya hay dos versiones del runtime: cada agentcore deploy genera un snapshot versionado, y el endpoint DEFAULT apunta a la Version 2.

Ese consumo de 0.074 vCPU-hrs y 3.4 GB-hrs para 31 invocaciones da una idea del costo de compute del runtime, que es independiente del costo de inferencia. La consola advierte que los datos de consumo pueden retrasarse hasta 60 minutos.

Un detalle de bookkeeping para que los números del artículo cuadren: el contador sube a lo largo del ejercicio. El seed son 30 invocaciones, esta captura muestra 31, y el desglose de costo del final cierra en 32 porque incluye las verificaciones que hice después de tomar la pantalla.

Paso 2: Generar la Línea Base

La recomendación necesita traces que analizar, así que hay que hacer trabajar al agente primero. Mi script recorre diez incidentes DevOps reales en varias rondas:

# 3 rondas × 10 incidentes = 30 invocaciones directas al runtime
./seed_traffic.sh ../../incidents.txt 3

Los incidentes son del dominio real, no “hola mundo”: “Estamos viendo timeouts intermitentes en checkout-api contra la instancia RDS prod-db-1”, “payment-service tiene errores de conexión desde hace 10 minutos”, “Aurora prod-db-1 muestra 95% de utilización de conexiones, ¿escalo o hay otra causa?”.

🔧 ProTip #5: Después de invocar, espera de 2 a 5 minutos antes de lanzar la recomendación. CloudWatch necesita ingerir la telemetría, y si disparas antes el servicio simplemente no encuentra traces suficientes. También necesitas Transaction Search habilitado en CloudWatch — y ahí hay una trampa que me costó tiempo, ver el ProTip #10.

Con el tráfico corriendo, el dashboard de online evaluation empezó a mostrar la línea base:

Widget de Builtin.GoalSuccessRate mostrando 0.826 de score promedio Figura 2: Línea base del evaluador Builtin.GoalSuccessRate — score promedio de 0.826 en esa ventana, con la distribución Yes/No que caracteriza a este evaluador binario a nivel de sesión.

Un score de 0.826 no es malo. Y aquí está la trampa conceptual del ejercicio completo, así que la dejo dicha desde ya: entre más alto arranca tu agente, más difícil es demostrar estadísticamente que lo mejoraste. Volveré sobre esto.

📊 ProTip #6: No mezcles el score del widget de online evaluation con los números del A/B test. Son ventanas de tiempo, muestras y contextos distintos. En mi corrida, el widget marcó 0.826 sobre la ventana de línea base, la recomendación reportó 16 éxitos de 20 traces analizados (0.80), y el A/B test midió 0.94 para el control sobre 103 sesiones. Los tres números son correctos y ninguno es el otro. Cada vez que cites uno, cita también de qué pantalla salió y sobre cuántas sesiones — sin eso, cualquiera de los tres puede pasar por “el score del agente”.

Paso 3: Pedir la Recomendación

agentcore run recommendation \
  --type system-prompt \
  --run devops-prompt-rec \
  --runtime devopsAgent \
  --evaluator Builtin.GoalSuccessRate \
  --prompt-file ../../bundles/control_prompt.txt \
  --lookback 7

Por debajo, el CLI resuelve los ARNs de log group y los service names desde la configuración del runtime, y arma una llamada equivalente a esta:

response = client.start_recommendation(
    name="devops-prompt-rec",
    type="SYSTEM_PROMPT_RECOMMENDATION",
    recommendationConfig={
        "systemPromptRecommendationConfig": {
            "systemPrompt": {"text": prompt_actual},
            "agentTraces": {
                "cloudwatchLogs": {
                    "logGroupArns": ["<log-group-arn>"],   # ARNs, no nombres
                    "serviceNames": ["devopsAgent.DEFAULT"],  # verifica el valor real
                    "startTime": now - timedelta(days=7),
                    "endTime": now,
                }
            },
            "evaluationConfig": {
                "evaluators": [
                    {"evaluatorArn": "arn:aws:bedrock-agentcore:::evaluator/Builtin.GoalSuccessRate"}
                ]
            },
        }
    },
    clientToken=str(uuid.uuid4()),
)

No copies ese serviceNames a ciegas. El CLI lo resuelve por ti, pero si armas la llamada a mano tienes que usar el nombre literal con el que el runtime emite telemetría, y ese nombre carga los prefijos y sufijos que el proyecto le agrega — en mi cuenta la consola muestra el runtime como DevOpsOptimizationLoop_dev, no como devopsAgent. Si te equivocas aquí no obtienes un error: obtienes cero traces y una recomendación que nunca arranca, que es el peor modo de falla posible para depurar.

Y aquí llegó la sorpresa agradable del ejercicio.

Resultados: Qué Encontró el Optimizador

El servicio analizó veinte trayectorias: dieciséis éxitos y cuatro fallos. Veinte no es el número de traces que sembré, es el máximo que el servicio muestrea por recomendación — volveré sobre eso. Y en esos cuatro fallos encontró un patrón que yo no había visto, a pesar de que los datos llevaban ahí toda la tarde.

Explicación del optimizador sobre el patrón de fallo detectado Figura 3: La pestaña Explanation del resultado — el optimizador identifica un único patrón dominante de fallo, cita los cuatro traces específicos, y explica en qué se diferenciaban las trayectorias exitosas que enfrentaron el mismo error de herramienta.

El razonamiento, resumido: los cuatro traces fallidos compartían la misma estructura. El agente llamaba a las herramientas contra prod-db-1, recibía un error DBInstanceNotFound o métricas nulas en todos los campos de CloudWatch, y entonces respondía pidiendo el identificador correcto de la instancia sin entregar ningún contenido diagnóstico sobre los síntomas que el usuario ya había descrito.

(Un paréntesis honesto: prod-db-1 e i-0abc123def456 son identificadores ficticios de mi laboratorio. Esos errores de herramienta son esperados. Lo interesante no es que fallaran, sino qué hizo el agente cuando fallaron.)

Porque el optimizador también miró los traces exitosos que enfrentaron exactamente el mismo fallo —el mismo DBInstanceNotFound, los mismos campos nulos— y se comportaron distinto de forma consistente: reconocían brevemente la falla de la herramienta y luego entregaban un diagnóstico diferencial completo basado en los síntomas descritos, con queries SQL concretas, comandos bash de verificación y pasos de remediación específicos.

La conclusión del optimizador me pareció una lección de diseño de agentes en sí misma: los agentes exitosos trataban los síntomas reportados como suficientes por sí solos para generar guía accionable, sin importar si las llamadas a herramientas devolvieron datos usables. El agente fallido se detenía a pedir más datos; el exitoso trataba el error de la herramienta como un dato más.

Y no se quedó en RDS. El optimizador reporta explícitamente que el mismo patrón aparecía también en los escenarios de EC2: cuando la tool devolvía todos los campos en null, las trayectorias exitosas igual entregaban un marco diagnóstico completo cubriendo estado del agente de CloudWatch, huecos de permisos IAM, estado de la instancia y retraso de métricas. Eso es lo que le da peso al hallazgo: no detectó una anécdota de una tool, detectó un comportamiento del agente que se repite cruzando dos familias de servicio.

El prompt resultante refleja eso quirúrgicamente:

Diff entre el prompt original y el recomendado Figura 4: Vista lado a lado del prompt original contra el recomendado. El optimizador conservó verbatim la estructura, la lista de especialidades y la metodología de cuatro pasos, y agregó dos bloques nuevos.

Lo que agregó, integrado dentro de la sección de metodología en lugar de anexado al final:

REGLA CRÍTICA: Cuando las herramientas fallen (errores, null, NotFound, AccessDenied), SIEMPRE entrega un diagnóstico diferencial completo basado en los síntomas del usuario. Incluye queries SQL, comandos y remediación concreta. Nunca te limites solo a pedir más datos sin ofrecer valor diagnóstico.

Y una política de confirmación que el prompt original no tenía: antes de ejecutar acciones con consecuencias reales —escalar instancias, terminar conexiones, modificar parámetros— indicar el plan y esperar confirmación explícita del usuario. Un guiño interesante a lo que cubrimos con AgentCore Policy, solo que aquí es una convención de prompt, no un control determinístico. No confundas una con la otra: un prompt sugiere, Cedar bloquea.

💡 ProTip #7: Las veinte trayectorias no son casualidad, son un techo. Los service quotas de AgentCore fijan Sessions per recommendation en 20, y no es ajustable. Da igual que siembres 30 traces o 3,000: cada recomendación muestrea veinte sesiones y de ahí sale todo el diagnóstico. Eso cambia cómo construyes la línea base — necesitas tráfico representativo antes que abundante, porque el muestreo va a dejar fuera casi todo lo demás. En la misma tabla: Prompt size tope de 20,000 caracteres, 5 recomendaciones activas por cuenta y StartRecommendation a 3 TPS.

Dos cosas que me parecen destacables de este resultado. La primera: conservó mi prompt original. No lo reescribió con su propio estilo ni tradujo nada al inglés; respetó el español, la estructura y el tono, y agregó lo mínimo necesario. La segunda: el diagnóstico causal está bien hecho. Contrastar traces fallidos contra traces exitosos que enfrentaron la misma condición es exactamente el análisis que haría un buen ingeniero, y es exactamente el que uno nunca tiene tiempo de hacer a mano sobre veinte trayectorias.

Validación: Bundles, A/B y el Veredicto Incómodo

Con el prompt recomendado en mano, el siguiente paso es no creerle. Para eso está el A/B test.

Creé los dos bundles —control con el prompt actual, treatment con el recomendado— y lancé el experimento con un split 80/20:

agentcore run ab-test \
  --mode config-bundle \
  --name devopsPromptTest \
  --gateway devopsGateway \
  --runtime devopsAgent \
  --control-bundle devopsControl \
  --control-version <version-id-control> \
  --treatment-bundle devopsTreatment \
  --treatment-version <version-id-treatment> \
  --online-eval devopsEval \
  --control-weight 80 \
  --treatment-weight 20

Después, tráfico real contra el endpoint HTTP del gateway, firmado con SigV4 y con un session ID nuevo por request para que el reparto de variantes funcione. Dejé acumular sesiones y consulté resultados.

Resultados del A/B test: control 0.94, variante 1.00, no significativo p=0.38 Figura 5: Resultados con 142 sesiones acumuladas — 103 ruteadas a control y 39 a la variante. Goal Success Rate: 0.94 contra 1.00. Variant improvement: Not significant: +6.2% (p=0.38). Recommended winner: ninguno.

Ahí está el veredicto, y no es el que uno quiere para un artículo:

Métrica Control (C) Treatment (T1)
Goal Success Rate (media) 0.94 1.00
Sesiones 103 39
Cambio porcentual +6.2%
p-value 0.38
¿Significativo? No

Un apunte de precisión, porque de aquí en adelante voy a hacer aritmética con estos números: la consola reporta dos decimales, 0.94 y 1.00. Eso es suficiente para reconstruir los conteos sin ambigüedad. Con 103 sesiones, un promedio que redondea a 0.94 solo admite un entero de éxitos: 97 (96/103 redondea a 0.93, 98/103 a 0.95). Así que el control fue 97/103 = 0.9417 y la variante 39/39, y el cambio porcentual (1 − 0.9417) / 0.9417 × 100 = 6.19% cuadra con el +6.2% de la consola.

La variante no falló ni una sola sesión de 39. Y aun así, el servicio se niega a declararla ganadora: con p=0.38, la probabilidad de ver esta diferencia por puro azar es demasiado alta. El umbral que aplica la consola para marcar significancia es p < 0.05.

Por Qué el Resultado “Malo” es el Hallazgo Bueno

Es tentador leer esto como un fracaso del experimento. Yo lo leo al revés: el servicio hizo exactamente su trabajo, y su trabajo es protegerte de ti mismo.

Piensa en lo que habría pasado sin A/B test. Cambio el prompt, corro diez pruebas manuales, todas pasan, escribo en el changelog “mejora del 6% en tasa de éxito” y sigo con mi vida. Ese número habría sido una ficción con apariencia de dato. El A/B test es el que te dice, con matemática, que 39 sesiones perfectas no alcanzan para distinguir una mejora real de una racha de suerte.

Antes de seguir, una honestidad que conviene decir yo mismo en lugar de dejar que alguien la encuentre cruzando las figuras: el control del A/B corre el mismo prompt que produjo la línea base de 0.826, y sin embargo puntúa 0.9417. Son 0.116 puntos de diferencia con el prompt sin tocar, el doble del efecto que estoy tratando de medir. Entre ambas mediciones cambió el entorno: la memoria episódica quedó apagada para el A/B, y según el orden en que resolví los permisos IAM del rol de ejecución es posible que parte del tráfico de línea base corriera con herramientas devolviendo AccessDenied. La conclusión práctica es que la línea base no es estrictamente comparable con el control: el 0.826 sirve como punto de partida narrativo, no como término de comparación. La única comparación limpia del ejercicio es control contra treatment dentro del mismo A/B test, y es la que sostiene el resto de la sección.

Hay una razón de fondo que conviene entender: el techo. Mi control ya venía en 0.9417. El espacio de mejora que queda es de 0.058 puntos. Detectar un efecto tan pequeño requiere muchísimas más muestras que detectar uno grande. Si tu agente estuviera en 0.60, la misma cantidad de tráfico probablemente habría bastado.

Quise poner número a ese “muchísimas más”, y el ejercicio terminó siendo más instructivo por cómo falló que por su resultado. Corrí las pruebas clásicas sobre los conteos reconstruidos (97/103 contra 39/39) y ninguna coincide con lo que reporta AWS: Fisher exacto bilateral da p=0.19, chi-cuadrado con corrección de Yates da p=0.28, y un t-test de Welch sobre los scores binarios da p=0.014. AWS reporta 0.38, más conservador que todas.

Después intenté el paso siguiente —calcular cuántas sesiones harían falta para 80% de potencia— y ahí me quedé sin piso. Con effect size de arcoseno salen unas 105 sesiones totales al reparto 80/20; con la aproximación normal de varianza pooled salen más de 400. Un factor de cuatro entre dos métodos igual de estándar, sobre los mismos datos. La razón es que con una variante clavada en 1.00 la varianza es cero y las fórmulas de libro de texto se degradan justo donde más las necesitas. Así que el número honesto no es un número: es que este cálculo no se puede hacer por fuera con confianza, y ese es el hallazgo.

🎓 ProTip #8: No intentes reproducir ni el p-value ni el análisis de potencia de AgentCore por fuera. La documentación no especifica qué prueba usa el motor, y con una variante en 1.00 (varianza cero) las pruebas clásicas divergen entre sí por factores de cuatro. Lo práctico: el motor es más conservador que cualquier cálculo ingenuo, así que si te dice que no hay evidencia, dale más tráfico en lugar de discutirle con una hoja de cálculo. Y dimensiona por el lado pesimista: si tu línea base ya está sobre 0.9, presupuesta cientos de sesiones por variante, no decenas.

Un detalle más que anoté: configuré el split en 80/20, pero el reparto observado fue 103/39, o sea 72.5%/27.5%. Con 142 sesiones, un 20% esperado da 28.4 sesiones con desviación estándar de 4.77; observar 39 son 2.2 sigmas (binomial exacto bilateral ≈ 0.035). Es una desviación mayor a la que esperaría del puro azar, aunque con este volumen tampoco es concluyente. Mi mejor hipótesis es la asignación sticky por session ID. Descarté la explicación fácil —que los conteos fueran sesiones evaluadas y no ruteadas— porque la consola es literal al respecto: los rótulos dicen “Sessions routed to control” y “Sessions routed to variant”. No tengo evidencia para cerrarlo, así que lo dejo como observación abierta: si tus pesos importan de verdad, verifica el reparto real en lugar de asumirlo.

Promoción: Donde el CLI se Quedó Corto

Con el resultado en la mano tomé una decisión de equipo: promover el treatment de todas formas. El prompt recomendado es mejor por razones cualitativas —la regla de entregar diagnóstico ante fallos de herramienta es objetivamente correcta para el dominio— y no hay evidencia de que perjudique. Pero eso queda documentado como decisión, no como victoria estadística. La diferencia importa.

Y ahí me estrellé con el gotcha más caro del ejercicio:

Cannot promote: control and treatment reference different config bundles.
A config-bundle A/B test can only promote between two versions of the SAME bundle.

agentcore promote ab-test solo promueve entre versiones del mismo bundle. Mi test comparaba devopsControl contra devopsTreatment, dos bundles distintos — que es exactamente como quedan si sigues el camino natural de crear un bundle por variante. El comando falla y, peor, el test se queda RUNNING: no lo detiene.

Lo que convierte esto de descuido mío en un hueco real es que la documentación avala explícitamente el camino que después bloquea la promoción. Los prerrequisitos de A/B testing listan como requisito “dos versiones de bundle o dos bundles separados: uno para control y uno para treatment”. Las dos opciones son válidas para correr el test; solo una de las dos te deja promoverlo. Y el prerrequisito no lo advierte.

La salida está en la consola. En la vista del A/B test hay un botón Deploy configuration bundle as rule:

Diálogo de la consola para desplegar un bundle como regla del gateway Figura 6: El diálogo pide elegir cuál bundle desplegar como regla en el gateway, y exige escribir “confirm” como consentimiento explícito. Nótese que los nombres de bundle aparecen prefijados con el nombre del proyecto: DevOpsOptimizationLoopdevopsControl-....

Esto crea una regla estática en el gateway apuntando al bundle ganador:

Regla estática creada en el gateway con prioridad 900000 Figura 7: El gateway con su target devops-diag y la nueva regla de configuration bundle en modo Static, con prioridad asignada 900000 — que según el propio diálogo se puede sobrescribir.

No es lo mismo que un promote: no actualiza el bundle de control ni detiene el experimento. Pero logra el efecto práctico —todo el tráfico que pase por ese gateway usa el prompt ganador— y luego detienes el test aparte con agentcore stop ab-test -i <id>.

🚨 ProTip #9: Si piensas usar agentcore promote ab-test, modela tus variantes como dos versiones del mismo bundle desde el inicio, no como dos bundles distintos. Es una decisión de diseño que tomas en el minuto cinco del proyecto y que te cobra en el minuto quinientos. Bonus de nomenclatura: los nombres de bundle no admiten guiones ([a-zA-Z][a-zA-Z0-9_]{0,99}) mientras que los de recomendación y A/B test sí; el CLI les antepone el nombre del proyecto sin separador (DevOpsOptimizationLoopdevopsControl-...), y a los gateways además los pasa a minúsculas (devopsoptimizationloop-devopsgateway-...). Si construyes ARNs o scripts asumiendo el nombre que escribiste, no vas a encontrar nada. Y ojo con dos cupos que muerden al diseñar: un A/B test por gateway y dos variantes por test (control más un treatment), ambos no ajustables.

Intenté después gestionar las gateway rules por API para limpiar, y me topé con algo que no supe explicar: ListGatewayRules y DeleteGatewayRule devuelven AccessDeniedException incluso con un rol de AdministratorAccess. Parece un guardrail de cuenta sobre APIs en preview más que un problema de política IAM. No bloquea el cleanup —la regla se elimina en cascada al destruir el gateway— pero sí impide gestión granular.

Lecciones Aprendidas

Más allá de los ProTips repartidos por el artículo, esto es lo que me llevo del ejercicio completo.

El CLI Tiene un Homónimo Deprecado que te Arruina la Tarde

Existen dos binarios llamados agentcore: el AgentCore CLI de Node (npm install -g @aws/agentcore) y el viejo Starter Toolkit de Python (pip install agentcore), que está deprecado. Si tienes ambos instalados, el de Python puede resolver primero en el PATH y ninguno de los comandos de este artículo existe. El propio binario avisa que las capacidades nuevas solo están en el CLI nuevo, pero el mensaje es fácil de pasar por alto cuando estás depurando otra cosa. Verifica con agentcore --version: el de Node imprime algo como 0.25.0, el de Python no soporta el flag.

Los Prerrequisitos Fallan en el Orden Equivocado

Todo el loop depende de Transaction Search habilitado en CloudWatch, y habilitarlo tiene su propia trampa de orden de operaciones.

⏱️ ProTip #10: Habilitar Transaction Search tiene un orden obligatorio que no es obvio. Si corres xray update-trace-segment-destination --destination CloudWatchLogs directo, falla con AccessDeniedException aunque tengas permisos de sobra. Primero necesitas un logs put-resource-policy que autorice a xray.amazonaws.com a hacer logs:PutLogEvents sobre aws/spans y /aws/application-signals/data; después el update de X-Ray. El error no te dice nada de eso.

En la misma familia de fallos por permisos: el rol de ejecución que genera el CDK no trae los permisos que necesitan tus tools. Si no adjuntas una policy con cloudwatch:GetMetricStatistics, rds:Describe* y ec2:Describe* al rol AgentCore-<Proyecto>-ApplicationAgent<Agente>..., tus herramientas devuelven AccessDenied y el agente sigue respondiendo como si nada — otro fallo silencioso que solo ves si lees los traces.

Y una advertencia con fecha de vencimiento, porque cambió dos semanas antes de que yo corriera esto. Las release notes de julio de 2026 introdujeron el destino unificado de spans: en lugar del log group compartido aws/spans, los agentes pueden entregar sus spans al log group propio, /aws/bedrock-agentcore/runtimes/<agent_id>-<endpoint_name>, en el log stream spans. Se controla con la variable UNIFIED_TRACES_DESTINATION_ENABLED, y el detalle que importa es este: desde el 20 de julio de 2026, los agentes nuevos usan su propio log group por defecto, mientras los creados antes se quedan en aws/spans salvo que los migres. Si estás armando a mano los logGroupArns de una recomendación —o copiando un tutorial escrito antes de julio— vas a apuntar al log group equivocado y obtener cero traces. Verifica a dónde están cayendo tus spans antes de asumir la ruta.

El Motor de Análisis Corre en su Propio Ciclo

Después de generar tráfico nuevo, agentcore view ab-test --json me devolvía el analysisTimestamp y los sampleSize de la última corrida del motor, no del momento de la consulta. A veces acompañado de un 403 token expired en el campo results.error que no tenía relación con mis credenciales —persistía con una sesión SSO recién renovada—. La consola sí reflejaba el conteo real.

La lección práctica: los resultados tardan. Dependen del session timeout de tu online eval —una sesión se considera completa cuando no llegan requests nuevos dentro de la ventana— y después de que cierra, los scores aparecen típicamente en unos 15 minutos. Si sospechas un resultado stale, contrasta contra la consola o consulta directo con aws bedrock-agentcore get-ab-test --ab-test-id <id> (nótese: plano de datos, y el comando es get-ab-test, no get-a-b-test).

Los Dos Planos, Otra Vez

Como en el artículo de Memory, la distinción entre bedrock-agentcore (plano de datos: recommendations, A/B tests, invoke, leer bundle) y bedrock-agentcore-control (plano de control: crear bundles, online eval) es real y tiene consecuencias. Confundirlos te da un método inexistente, no un error de permisos. Y en la misma línea de detalles que muerden: la API de recommendations usa logGroupArns (ARNs completos) mientras que las batch evaluations usan logGroupNames (nombres). La documentación lo marca explícitamente, lo cual ya dice algo.

El Costo es Sorprendentemente Bajo

La página oficial de pricing de AgentCore es explícita: la generación de recommendations no tiene cargo —pagas solo las Evaluations que consuma el flujo— y los A/B tests se cobran por Gateway, Runtime y Evaluations consumidos. En números reales de mi cuenta, 32 invocaciones consumieron 50,020 tokens de input y 17,663 de output. Con el pricing de Claude Sonnet 4.6 ($3 por millón de input, $15 por millón de output):

input:  50,020 tokens ÷ 1M × $3  = $0.1501
output: 17,663 tokens ÷ 1M × $15 = $0.2649
                                   -------
total (32 invocaciones)            $0.4150   →  ~$0.013 por invocación

Extrapolando a las ~172 invocaciones del loop completo, la inferencia del agente ronda los $2.23. Hay que sumar el modelo juez del online evaluation y el compute del runtime, así que el ejercicio completo queda en el orden de unos pocos dólares. Para el tipo de decisión que respalda —cambiar el comportamiento de un agente en producción con evidencia en lugar de intuición— es barato.

Pero ojo con la trampa: si tu línea base ya está alta y necesitas cientos de sesiones por variante para alcanzar significancia, ese costo escala. A $0.013 por invocación, 1,000 sesiones son ~$13 de inferencia del agente. Sigue siendo barato; solo hay que presupuestarlo.

Conclusión

Volvamos a la pregunta del inicio: ¿es AWS mejor que tú escribiendo el prompt de tu agente?

En mi ejercicio, la respuesta honesta es parcialmente sí, y por una razón que no esperaba. El optimizador no fue más creativo que yo ni escribió mejor prosa. Lo que hizo fue algo mucho más aburrido y mucho más valioso: leyó los veinte traces completos y comparó sistemáticamente los fallos contra los éxitos que enfrentaron la misma condición. Es un trabajo que yo sé hacer perfectamente y que nunca hago, porque implica leer trayectorias completas una por una un martes por la tarde.

Ese es el verdadero producto de Recommendations: no la inteligencia, sino la disciplina.

Y el A/B test resultó ser la mitad más importante del loop, precisamente porque me dijo que no. Una mejora de 0.94 a 1.00 sin un solo fallo en 39 sesiones suena a victoria, y en un blog post menos honesto habría titulado “+6.2% de mejora con AgentCore Optimization”. El servicio me recordó que ese número no significa lo que parece significar con ese tamaño de muestra. Prefiero mil veces esa fricción que un dashboard que me dé la razón.

Si vas a llevar esto a producción, mi recomendación práctica: empieza el loop antes de que tu agente esté bueno. Suena contraintuitivo, pero es donde más rinde. Con un agente en 0.60 de Goal Success Rate, unas decenas de sesiones te dan significancia y el ciclo itera rápido. Con un agente en 0.94, estás peleando contra el techo y necesitas volumen de producción real para mover la aguja con evidencia.

Con este artículo, la serie ya cubre el ciclo completo de un agente en AgentCore: Policy para lo que el agente puede hacer, Evaluations para medir qué tan bien lo hace, Memoria Episódica para que aprenda de lo que vivió, Session Storage para que no pierda lo que construyó, y ahora Optimization para cerrar el ciclo y mejorarlo con evidencia en lugar de intuición.

El código completo del ejercicio —agente portado a Strands, scripts numerados del 00 al 07, y el runbook con los nueve gotchas documentados— está en github.com/codecr/agentcore-optimization-loop. Se reproduce en tu cuenta por unos pocos dólares.

🚀 ProTip Final: Antes de correr tu primer loop de optimización, mide tu línea base y calcula si tu volumen de tráfico puede alcanzar significancia para el tamaño de mejora que esperas. Un A/B test que nunca llega a p < 0.05 no es un experimento fallido: es un experimento mal dimensionado. Y esa cuenta se hace antes, no después.


¿Ya tienes agentes en producción con suficiente tráfico para correr A/B tests con significancia real? ¿O estás en la etapa donde el volumen todavía no da y las decisiones siguen siendo cualitativas? Me interesa mucho conocer cómo están validando cambios de prompt en producción — los comentarios están abiertos.

¡Nos vemos en el próximo artículo! 🚀


Recursos Oficiales 📚

Portada de AgentCore in Production, de Gerardo Arroyo

El libro

Hay un libro detrás de esto.

AgentCore in Production son 229 páginas sobre llevar agentes Bedrock de demo a producción: políticas Cedar, una arquitectura de referencia de once componentes y el playbook operativo del día uno.

Kindle + tapa blanda · 12 capítulos · 229 páginas

Conseguirlo en Amazon →
Escrito por

Gerardo Arroyo Arce

Arquitecto de Soluciones y autor de AgentCore in Production: The Operator’s Playbook for AWS Bedrock Agents — 229 páginas sobre llevar agentes Bedrock de demo a producción. AWS Golden Jacket con pasión por compartir conocimiento. Como miembro activo de AWS Community Builders, ex-AWS Ambassador y AWS User Group Leader, me dedico a construir puentes entre la tecnología y las personas. Desarrollador Java de corazón y consultor independiente, llevo la arquitectura cloud más allá de la teoría a través de conferencias internacionales y soluciones del mundo real. Mi curiosidad insaciable por aprender y compartir me mantiene en constante evolución junto a la comunidad tech.

Inicia la conversación