Guía técnica · Stadium Assistant

Solución del problema de respuestas que no cumplen los parámetros requeridos

Corrección del Walking Skeleton: identificar el origen de los 503, corregir la medición, reducir latencia, estabilizar OpenAI Responses API y volver a validar el criterio p95.

Walking Skeleton AWS us-east-1 OpenAI Responses API
11pasos de solución
20solicitudes de validación
≤1500ms p95 objetivo
Prompt original

Punto exacto del chat

“Ok, ahora indicame el paso a paso y detalle a detalle para la solucion del problema de la respuesta que no cumplen con los parametros requeridos.”
Objetivo del trabajo

Corregir disponibilidad y latencia antes de cerrar el Walking Skeleton

El objetivo no es solamente conseguir que las 20 solicitudes terminen. Para considerar la prueba como aprobada, el reporte debe distinguir correctamente entre solicitudes exitosas y fallidas y evaluar la latencia únicamente con datos válidos.

El criterio de salida que debemos conseguir es:

Criterio final esperado
total = 20
successful = 20
failed = 0
successRatePct = 100
successfulP95Ms <= 1500
overallTargetMet = true
Importante: un 503 nunca debe contabilizarse como si fuera una respuesta válida para calcular el p95 de éxito. Primero se elimina la causa del 503; después se optimiza la latencia.

Problema 1 — Disponibilidad

Durante la ráfaga aparecieron respuestas HTTP 503. Es necesario determinar si el origen es API Gateway, Lambda o la llamada a OpenAI.

Problema 2 — Latencia

Las respuestas completas de la ruta directa OpenAI pueden superar la meta de 1500 ms. La optimización debe medirse separadamente de los errores.

Paso 1

Identificar exactamente de dónde vienen los 503

No cambies todavía el modelo ni el código de performance. Primero confirma el origen del error.

Abre PowerShell en el ambiente DEV y confirma primero que la Lambda no esté siendo limitada por concurrencia.

Concurrencia reservada de Lambda
aws lambda get-function-concurrency `
  --function-name sa-dev-orchestrator-chat `
  --region us-east-1 `
  --profile sa-dev

Después revisa la configuración de la función:

Configuración de Lambda
aws lambda get-function-configuration `
  --function-name sa-dev-orchestrator-chat `
  --region us-east-1 `
  --profile sa-dev `
  --query "{State:State,LastUpdateStatus:LastUpdateStatus,Timeout:Timeout,MemorySize:MemorySize,Runtime:Runtime}"

Revisa los logs de Lambda del periodo donde ejecutaste la prueba:

CloudWatch · Lambda
aws logs tail "/aws/lambda/sa-dev-orchestrator-chat" `
  --since 30m `
  --region us-east-1 `
  --profile sa-dev

Y revisa los logs de API Gateway:

CloudWatch · API Gateway
aws logs tail "/aws/apigateway/sa-dev-http-api" `
  --since 30m `
  --region us-east-1 `
  --profile sa-dev
Qué buscamos

Mensajes de throttling, timeout, 5xx de integración o un error proveniente de la llamada a OpenAI. No debemos asumir el origen sin evidencia.

Paso 2

Corregir el reporte de scripts/test-api-burst.mjs

El script debe separar éxitos y fallos y calcular p50/p95 sobre las respuestas exitosas.

Reemplaza el contenido de:

Archivo
scripts/test-api-burst.mjs

por una versión que reporte de forma explícita successful, failed, successRatePct, successfulP50Ms, successfulP95Ms y overallTargetMet.

scripts/test-api-burst.mjs
const args = process.argv.slice(2);

function getArg(name, fallback) {
  const index = args.indexOf(`--${name}`);
  return index >= 0 && args[index + 1] !== undefined
    ? args[index + 1]
    : fallback;
}

const url = getArg(
  "url",
  process.env.CHAT_URL ||
    "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat"
);

const total = Number(getArg("total", "20"));
const concurrency = Number(getArg("concurrency", "5"));
const delayMs = Number(getArg("delay-ms", "200"));
const message = getArg("message", "Hola");
const targetP95Ms = Number(getArg("target-p95-ms", "1500"));

function percentile(values, p) {
  if (!values.length) return null;
  const sorted = [...values].sort((a, b) => a - b);
  const index = Math.ceil((p / 100) * sorted.length) - 1;
  return sorted[Math.max(0, index)];
}

const results = [];
let nextIndex = 0;

async function sendOne(index) {
  if (delayMs > 0 && index > 0) {
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }

  const startedAt = performance.now();
  let status = 0;
  let body = "";
  let error = null;

  try {
    const response = await fetch(url, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        message,
        session_id: `burst-${Date.now()}-${index}`,
        locale: "es"
      })
    });

    status = response.status;
    body = await response.text();
  } catch (err) {
    error = err instanceof Error ? err.message : String(err);
  }

  const latencyMs = Math.round(performance.now() - startedAt);
  const ok = status >= 200 && status < 300 && !error;

  results.push({
    index: index + 1,
    ok,
    status,
    latencyMs,
    error,
    body: body.slice(0, 300)
  });

  console.log(
    `[${index + 1}/${total}] status=${status || "ERR"} ` +
    `latency_ms=${latencyMs} ok=${ok}`
  );
}

async function worker() {
  while (true) {
    const index = nextIndex++;
    if (index >= total) return;
    await sendOne(index);
  }
}

await Promise.all(
  Array.from({ length: Math.min(concurrency, total) }, () => worker())
);

const successfulResults = results.filter((item) => item.ok);
const failedResults = results.filter((item) => !item.ok);
const successfulLatencies = successfulResults.map((item) => item.latencyMs);

const successful = successfulResults.length;
const failed = failedResults.length;
const successRatePct = Number(((successful / total) * 100).toFixed(2));
const successfulP50Ms = percentile(successfulLatencies, 50);
const successfulP95Ms = percentile(successfulLatencies, 95);
const successfulMaxMs = successfulLatencies.length
  ? Math.max(...successfulLatencies)
  : null;

const overallTargetMet =
  total === 20 &&
  successful === 20 &&
  failed === 0 &&
  successRatePct === 100 &&
  successfulP95Ms !== null &&
  successfulP95Ms <= targetP95Ms;

const statusCounts = results.reduce((acc, item) => {
  const key = String(item.status || "ERR");
  acc[key] = (acc[key] || 0) + 1;
  return acc;
}, {});

console.log("\n=== RESULT ===");
console.log(JSON.stringify({
  url,
  message,
  total,
  concurrency,
  delayMs,
  successful,
  failed,
  successRatePct,
  successfulP50Ms,
  successfulP95Ms,
  successfulMaxMs,
  targetP95Ms,
  overallTargetMet,
  statusCounts,
  failures: failedResults
}, null, 2));

process.exit(overallTargetMet ? 0 : 1);
La corrección importante: los tiempos de las respuestas 503 no se mezclan con la distribución de latencia de las respuestas exitosas. El test falla si existe una sola respuesta no exitosa o si el p95 exitoso supera el objetivo.
Paso 3

Configurar reasoning.effort = "none"

Para “Hola” no necesitas razonamiento adicional. Ese trabajo aumenta latencia sin aportar valor.

En la llamada a OpenAI Responses API configura:

Responses API
reasoning: {
  effort: "none"
}

La idea es que una solicitud simple del Walking Skeleton no consuma tiempo de razonamiento que no necesita.

Paso 4

Configurar text.verbosity = "low"

La respuesta del Walking Skeleton debe ser corta. No queremos que “Hola” produzca una respuesta larga.

Responses API
text: {
  verbosity: "low"
}

Para esta prueba la respuesta debe mantenerse en una o dos frases. El objetivo aquí es validar el tubo end-to-end, no generar contenido largo.

Paso 5

Reducir max_output_tokens

Limita la generación para que una respuesta simple no tenga margen para crecer innecesariamente.

Límite de salida
max_output_tokens: 80

Para el Walking Skeleton, 80 tokens son suficientes para una respuesta muy corta y reducen el tiempo de generación frente a límites mucho mayores.

Paso 6

Evitar reintentos ocultos y controlar el timeout

Un retry automático puede convertir un error rápido en una respuesta extremadamente lenta.

Configura el cliente de OpenAI de forma explícita:

Cliente OpenAI
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  maxRetries: 0,
  timeout: 12000
});

Para esta fase queremos observar el error real. Si existe un 429, 5xx o timeout de OpenAI, debe quedar visible en logs y no quedar escondido detrás de múltiples retries del SDK.

Paso 7

Mejorar los logs de la llamada a OpenAI

Necesitamos saber si una falla de la Lambda realmente proviene de OpenAI y qué tipo de falla fue.

No registres la API key ni el contenido sensible. Registra únicamente información operativa necesaria para diagnóstico.

Ejemplo de log seguro
try {
  const startedOpenAI = Date.now();

  const response = await openai.responses.create({
    model: process.env.OPENAI_MODEL,
    reasoning: { effort: "none" },
    text: { verbosity: "low" },
    max_output_tokens: 80,
    instructions:
      "Responde en el idioma del usuario. " +
      "Para un saludo, responde brevemente en una o dos frases.",
    input: message
  });

  console.log(JSON.stringify({
    event: "openai_call_ok",
    trace_id,
    model: process.env.OPENAI_MODEL,
    openai_latency_ms: Date.now() - startedOpenAI,
    response_id: response.id,
    route: "direct"
  }));

  return response.output_text;

} catch (error) {
  console.error(JSON.stringify({
    event: "openai_call_error",
    trace_id,
    name: error?.name,
    status: error?.status,
    code: error?.code,
    type: error?.type,
    message: error?.message,
    request_id: error?.request_id,
    route: "direct"
  }));

  throw error;
}
Qué debes poder distinguir

Timeout, rate limit, error 5xx de OpenAI, error de aplicación o error de integración. Sin esta separación no sabremos qué optimizar.

Paso 8

Crear un fast_path para el saludo del Walking Skeleton

El criterio original prueba “Hola”. No necesitas llamar a OpenAI para resolver un saludo completamente determinista.

Antes de ejecutar la ruta directa a OpenAI, normaliza el mensaje y responde localmente a saludos simples.

Fast path
function normalizeMessage(value = "") {
  return value
    .trim()
    .toLocaleLowerCase("es")
    .replace(/[¡!¿?.,]/g, "");
}

function getFastPathResponse(message, locale = "es") {
  const normalized = normalizeMessage(message);

  const greetings = new Set([
    "hola",
    "hello",
    "hi",
    "buenas",
    "buen dia",
    "buenos dias",
    "buenas tardes",
    "buenas noches"
  ]);

  if (!greetings.has(normalized)) {
    return null;
  }

  if (locale === "en") {
    return "Hello! I’m the Stadium Assistant. How can I help you?";
  }

  return "¡Hola! Soy el Stadium Assistant. ¿En qué puedo ayudarte?";
}

En el handler:

Usar fast_path antes de OpenAI
const fastPathResponse = getFastPathResponse(message, locale);

if (fastPathResponse) {
  const latencyMs = Date.now() - startedAt;

  console.log(JSON.stringify({
    trace_id,
    latency_ms: latencyMs,
    status: 200,
    route: "fast_path"
  }));

  return {
    statusCode: 200,
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      userMessage: message,
      assistantResponse: fastPathResponse,
      trace_id
    })
  };
}
Por qué sí aplica: el objetivo del Walking Skeleton es validar que el recorrido Web/API Gateway/Lambda/respuesta/logs funciona dentro de la meta. Un saludo fijo es determinista y no necesita inferencia del modelo.
Paso 9

Empaquetar nuevamente y desplegar en Lambda

Después de modificar el código, debes construir otra vez deployment.zip y subirlo a la función DEV.

Primero ejecuta las pruebas locales existentes:

Pruebas locales
npm run test:health
npm run test:injection
npm run test:local

Genera de nuevo el paquete de despliegue con el procedimiento del proyecto. Si tu script de empaquetado ya está configurado:

Empaquetado
npm run package

Después actualiza la Lambda:

Deploy del ZIP
aws lambda update-function-code `
  --function-name sa-dev-orchestrator-chat `
  --zip-file fileb://deployment.zip `
  --region us-east-1 `
  --profile sa-dev

Espera a que termine la actualización:

Esperar despliegue
aws lambda wait function-updated `
  --function-name sa-dev-orchestrator-chat `
  --region us-east-1 `
  --profile sa-dev
deployment.zip: no se descomprime para subirlo a Lambda. Lambda recibe el ZIP como paquete de código.
Paso 10

Repetir las pruebas por niveles de concurrencia

No saltes directamente a la carga máxima. Sube la concurrencia gradualmente para ver dónde aparece el problema.

Prueba A — concurrencia 1

20 solicitudes · 1 concurrente
node .\scripts\test-api-burst.mjs `
  --url "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat" `
  --total 20 `
  --concurrency 1 `
  --delay-ms 200 `
  --message "Hola"

Prueba B — concurrencia 5

20 solicitudes · 5 concurrentes
node .\scripts\test-api-burst.mjs `
  --url "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat" `
  --total 20 `
  --concurrency 5 `
  --delay-ms 200 `
  --message "Hola"

Prueba C — concurrencia 10

20 solicitudes · 10 concurrentes
node .\scripts\test-api-burst.mjs `
  --url "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat" `
  --total 20 `
  --concurrency 10 `
  --delay-ms 200 `
  --message "Hola"

Prueba D — concurrencia 20

20 solicitudes · 20 concurrentes
node .\scripts\test-api-burst.mjs `
  --url "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat" `
  --total 20 `
  --concurrency 20 `
  --delay-ms 200 `
  --message "Hola"

En cada prueba registra:

  • successful.
  • failed.
  • successRatePct.
  • successfulP50Ms.
  • successfulP95Ms.
  • successfulMaxMs.
  • overallTargetMet.
  • Distribución de códigos HTTP.
Resultado requerido para cerrar el criterio original

20/20 exitosas, 0 fallidas, successRatePct 100, p95 exitoso ≤ 1500 ms y overallTargetMet=true.

Paso 11

Si todavía falla: revisar límites de OpenAI y Fast mode / Priority

Este paso se hace solamente después de tener evidencia de que AWS y el código están funcionando correctamente.

Si los logs muestran 429, rate limit o saturación de OpenAI, revisa los límites de tu proyecto y el patrón de concurrencia. No aumentes concurrencia a ciegas.

Si la ruta directa al modelo sigue estable pero no alcanza la latencia objetivo, trata esa medición como una métrica separada del criterio del saludo. Para el Walking Skeleton, el fast_path permite validar el tubo sin gastar una inferencia completa para una entrada determinista.

Opcional: si tu cuenta/proyecto dispone de un modo de procesamiento prioritario o rápido, puede evaluarse después. No debe sustituir la corrección del código, los logs ni la medición correcta.
Cierre

Evidencia que debes guardar

0%
Progreso de solución

Completa el checklist para confirmar que la solución quedó validada.

Orden exacto de trabajo

No mezcles diagnóstico y optimización

  1. Identificar el origen de los 503.
  2. Corregir el reporte del script.
  3. Configurar reasoning.effort = "none".
  4. Configurar text.verbosity = "low".
  5. Reducir max_output_tokens.
  6. Mejorar los logs de OpenAI.
  7. Empaquetar y desplegar nuevamente.
  8. Repetir las pruebas y guardar evidencia.

Resultado esperado

El Walking Skeleton queda estable, medible y con evidencia suficiente para separar disponibilidad, latencia y ruta de ejecución.

Volver al inicio