Technical Guide

Disenar Workflows de IA que realmente funcionan en produccion

Una guia practica para construir pipelines de IA robustos. Aprende arquitectura de pipelines, secuenciacion de pasos, logica de ramificacion, manejo de errores y encadenamiento de prompts de ingenieros que han desplegado estos sistemas.

10 de julio de 202518 min de lecturaEquipo de Ingenieria Oronts

Por que la mayoria de los workflows de IA fallan en produccion

Aqui tienes algo que aprendimos por las malas: hacer que una IA haga algo impresionante en una demo es facil. Hacer que haga lo mismo de manera fiable, miles de veces al dia, con datos reales y casos extremos reales? Ahi es donde la mayoria de los equipos chocan contra un muro.

Hemos construido workflows de IA para procesamiento de documentos, automatizacion de soporte al cliente, generacion de contenido y analisis de datos. En el camino, hemos cometido todos los errores imaginables. Esta guia es lo que nos hubiera gustado que alguien nos dijera antes de empezar.

La diferencia entre un prototipo y un sistema de produccion no es el modelo que usas. Es todo lo que hay alrededor del modelo.

Dejame mostrarte como disenamos realmente los workflows de IA ahora, despues de aprender estas lecciones.

La anatomia de un pipeline de IA en produccion

Piensa en un workflow de IA como una serie de etapas de procesamiento, cada una con un trabajo especifico. Aqui esta la estructura basica que usamos:

Entrada → Validacion → Pre-procesamiento → Procesamiento IA → Post-procesamiento → Validacion → Salida
            ↓               ↓                   ↓                   ↓               ↓
         [Manejo]      [Transformacion]     [Llamada LLM]       [Parsing]      [Control
         Error]        + Enriquecimiento    + Herramientas      + Formato       Calidad]

Cada etapa tiene entradas, salidas y modos de fallo claros. Vamos a desglosarlas.

Etapa 1: Validacion y normalizacion de entrada

Nunca confies en los datos entrantes. Nunca. Lo aprendimos cuando un cliente nos envio un "documento de texto" que en realidad era un archivo binario de 50 MB. El pipeline se atasco, la cola se congestion, y pasamos un fin de semana arreglandolo.

const validateInput = async (input) => {
  const checks = {
    exists: input !== null && input !== undefined,
    sizeOk: Buffer.byteLength(input, 'utf8') < MAX_INPUT_SIZE,
    formatOk: isValidFormat(input),
    contentOk: !containsMaliciousPatterns(input)
  };

  const failures = Object.entries(checks)
    .filter(([_, passed]) => !passed)
    .map(([check]) => check);

  if (failures.length > 0) {
    throw new ValidationError(`Entrada fallida: ${failures.join(', ')}`);
  }

  return normalizeInput(input);
};

Que debes verificar:

  • Limites de tamano de archivo (ponlos mas bajos de lo que crees)
  • Validacion de formato (es realmente JSON, no solo nombrado .json?)
  • Codificacion de caracteres (los problemas de UTF-8 te perseguiran)
  • Seguridad del contenido (no envies contenido malicioso a tu LLM)

Etapa 2: Pre-procesamiento y enriquecimiento

Las entradas crudas rara vez van directamente a un LLM. Normalmente necesitas transformarlas, agregar contexto o dividirlas en chunks.

Tarea de pre-procesamientoCuando usarEjemplo
ChunkingDocumentos largos que exceden limites de contextoDividir un PDF de 100 paginas en chunks de 2000 tokens
EnriquecimientoSe necesita contexto adicionalAgregar historial del cliente antes de procesar ticket de soporte
ExtraccionSolo partes de la entrada son relevantesExtraer solo el campo "description" de un payload JSON
TransformacionSe necesita conversion de formatoConvertir HTML a markdown para procesamiento mas limpio
DeduplicacionEl contenido repetido desperdicia tokensEliminar parrafos duplicados de contenido scrapeado

Aqui tienes una estrategia de chunking que realmente usamos:

const chunkDocument = (text, options = {}) => {
  const {
    maxTokens = 2000,
    overlap = 200,
    preserveParagraphs = true
  } = options;

  const chunks = [];
  let currentChunk = '';

  const paragraphs = text.split(/\n\n+/);

  for (const para of paragraphs) {
    const combined = currentChunk + '\n\n' + para;

    if (estimateTokens(combined) > maxTokens) {
      if (currentChunk) {
        chunks.push(currentChunk.trim());
        // Mantener overlap para continuidad de contexto
        currentChunk = getLastNTokens(currentChunk, overlap) + '\n\n' + para;
      } else {
        // Parrafo unico demasiado largo, split forzado
        chunks.push(...forceSplitParagraph(para, maxTokens));
        currentChunk = '';
      }
    } else {
      currentChunk = combined;
    }
  }

  if (currentChunk.trim()) {
    chunks.push(currentChunk.trim());
  }

  return chunks;
};

El overlap es crucial. Sin el, la informacion que cruza los limites de los chunks se pierde. Lo aprendimos cuando nuestro summarizer de documentos seguia perdiendo puntos clave que caian entre chunks.

Disenando el nucleo del procesamiento de IA

Aqui es donde ocurren las llamadas reales al LLM. Pero una sola llamada rara vez es suficiente para tareas complejas.

El patron Prompt Chain

En lugar de un prompt gigante que intenta hacer todo, divide las tareas complejas en pasos enfocados:

[Entender] → [Planificar] → [Ejecutar] → [Verificar] → [Formatear]

Ejemplo: Procesando una queja de cliente

const processComplaint = async (complaint) => {
  // Paso 1: Entender - Extraer informacion clave
  const analysis = await llm.call({
    system: `Extrae informacion estructurada de quejas de clientes.
             Devuelve JSON con: issue_type, urgency, customer_emotion, key_details`,
    user: complaint
  });

  // Paso 2: Planificar - Determinar estrategia de respuesta
  const strategy = await llm.call({
    system: `Basado en este analisis de queja, determina la estrategia de respuesta.
             Considera: opciones de resolucion, necesidades de escalacion, elegibilidad de compensacion`,
    user: JSON.stringify(analysis)
  });

  // Paso 3: Ejecutar - Generar respuesta
  const response = await llm.call({
    system: `Escribe una respuesta al cliente siguiendo esta estrategia.
             Tono: empatico, profesional. Longitud: 2-3 parrafos.`,
    user: `Estrategia: ${JSON.stringify(strategy)}\nQueja original: ${complaint}`
  });

  // Paso 4: Verificar - Control de calidad
  const verification = await llm.call({
    system: `Revisa esta respuesta al cliente. Verifica:
             - Aborda todas las preocupaciones? - Tono apropiado? - Siguientes pasos accionables?
             Devuelve: {approved: boolean, issues: string[]}`,
    user: response
  });

  if (!verification.approved) {
    return await regenerateWithFeedback(response, verification.issues);
  }

  return response;
};

Cada paso es mas simple, mas testeable y mas facil de debuguear. Cuando algo sale mal, sabes exactamente que etapa fallo.

Cuando encadenar vs. cuando paralelizar

No todo necesita ser secuencial. Asi es como decidimos:

PatronUsar cuandoEjemplo
Cadena secuencialCada paso depende de la salida anteriorEntender → Planificar → Ejecutar
Ejecucion paralelaLos pasos son independientesAnalizar multiples documentos simultaneamente
Map-ReduceNecesitas procesar items y luego agregarResumir cada chunk, luego combinar resumenes
Ramificacion condicionalDiferentes caminos para diferentes entradasConsultas simples vs. analisis complejo

Ejemplo de ejecucion paralela:

const analyzeDocuments = async (documents) => {
  // Procesar todos los documentos en paralelo
  const analyses = await Promise.all(
    documents.map(doc => analyzeDocument(doc))
  );

  // Reduce: combinar en informe final
  const combinedReport = await llm.call({
    system: 'Sintetiza estos analisis individuales en un informe coherente',
    user: analyses.map((a, i) => `Documento ${i + 1}:\n${a}`).join('\n\n---\n\n')
  });

  return combinedReport;
};

Logica de ramificacion: Enrutando al handler correcto

Las entradas del mundo real varian enormemente. Un "mensaje de cliente" puede ser una queja, una pregunta, un cumplido o spam. Cada uno necesita un tratamiento diferente.

Enrutamiento basado en clasificacion

const routeCustomerMessage = async (message) => {
  const classification = await llm.call({
    system: `Clasifica este mensaje de cliente en exactamente una categoria:
             - complaint: Cliente expresando insatisfaccion
             - question: Cliente buscando informacion
             - feedback: Comentarios generales o sugerencias
             - urgent: Problemas de seguridad, amenazas legales, escalacion ejecutiva
             - spam: Mensajes irrelevantes o automatizados

             Devuelve solo el nombre de la categoria.`,
    user: message
  });

  const handlers = {
    complaint: handleComplaint,
    question: handleQuestion,
    feedback: handleFeedback,
    urgent: escalateToHuman,
    spam: markAsSpam
  };

  const handler = handlers[classification.toLowerCase()] || handleUnknown;
  return await handler(message);
};

Enrutamiento basado en confianza

A veces la IA no esta segura. Incorpora esa incertidumbre en tu enrutamiento:

const routeWithConfidence = async (input) => {
  const result = await llm.call({
    system: `Analiza y enruta esta solicitud. Devuelve JSON:
             {
               "category": "string",
               "confidence": 0.0-1.0,
               "reasoning": "por que esta categoria"
             }`,
    user: input
  });

  if (result.confidence < 0.7) {
    // Baja confianza - obtener input humano o usar fallback
    return await handleLowConfidence(input, result);
  }

  if (result.confidence < 0.9 && isHighStakes(result.category)) {
    // Confianza media en decisiones importantes - verificar
    return await verifyThenRoute(input, result);
  }

  // Alta confianza - proceder automaticamente
  return await routeToHandler(result.category, input);
};

Manejo de errores: Porque las cosas van a romperse

Los workflows de IA fallan de maneras que el software tradicional no conoce. El LLM puede devolver JSON invalido, alucinar informacion, o simplemente... no seguir instrucciones. Planifica para eso.

La jerarquia de errores

class AIWorkflowError extends Error {
  constructor(message, stage, recoverable = true) {
    super(message);
    this.stage = stage;
    this.recoverable = recoverable;
  }
}

class ValidationError extends AIWorkflowError {
  constructor(message) {
    super(message, 'validation', true);
  }
}

class LLMError extends AIWorkflowError {
  constructor(message, type) {
    super(message, 'llm_call', type !== 'rate_limit');
    this.type = type; // 'timeout', 'rate_limit', 'invalid_response', 'refused'
  }
}

class OutputError extends AIWorkflowError {
  constructor(message) {
    super(message, 'output', true);
  }
}

Estrategias de retry que funcionan

Diferentes errores necesitan diferentes enfoques de retry:

Tipo de errorEstrategia de retryMax retriesBackoff
Rate limitEsperar y reintentar5Exponencial con jitter
TimeoutReintentar inmediatamente3Lineal
Respuesta invalidaReintentar con feedback2Ninguno
Modelo rechazaReformular y reintentar2Ninguno
Error de servidorEsperar y reintentar3Exponencial
const withRetry = async (operation, options = {}) => {
  const {
    maxRetries = 3,
    backoffMs = 1000,
    backoffMultiplier = 2,
    shouldRetry = () => true
  } = options;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;

      if (!shouldRetry(error) || attempt === maxRetries) {
        throw error;
      }

      const delay = backoffMs * Math.pow(backoffMultiplier, attempt);
      const jitter = Math.random() * delay * 0.1;
      await sleep(delay + jitter);
    }
  }

  throw lastError;
};

// Uso con llamadas LLM
const safeLLMCall = async (params) => {
  return await withRetry(
    () => llm.call(params),
    {
      maxRetries: 3,
      shouldRetry: (error) => {
        if (error.type === 'rate_limit') return true;
        if (error.type === 'timeout') return true;
        if (error.type === 'server_error') return true;
        return false;
      }
    }
  );
};

Auto-reparacion para respuestas invalidas

Cuando el LLM devuelve basura, a veces puedes arreglarlo:

const parseWithRecovery = async (llmResponse, expectedSchema) => {
  // Primer intento: parsing directo
  try {
    const parsed = JSON.parse(llmResponse);
    if (validateSchema(parsed, expectedSchema)) {
      return parsed;
    }
  } catch (e) {
    // Parsing JSON fallo, continuar a recuperacion
  }

  // Intento de recuperacion: pedir al LLM que arregle su salida
  const fixed = await llm.call({
    system: `La siguiente respuesta deberia ser JSON valido que coincida con este esquema:
             ${JSON.stringify(expectedSchema)}

             Arregla la respuesta a JSON valido. Devuelve SOLO el JSON arreglado.`,
    user: llmResponse
  });

  try {
    const parsed = JSON.parse(fixed);
    if (validateSchema(parsed, expectedSchema)) {
      return parsed;
    }
  } catch (e) {
    // Todavia roto
  }

  // Fallback final: extraer lo que se pueda
  return extractPartialData(llmResponse, expectedSchema);
};

Monitoreo y observabilidad

No puedes arreglar lo que no puedes ver. Esto es lo que rastreamos:

Metricas clave

MetricaPor que importaUmbral de alerta
Latencia (p50, p95, p99)Experiencia de usuario, riesgo de timeoutp95 > 10s
Tasa de exitoSalud general< 95%
Uso de tokensControl de costos> 150% de la baseline
Tasa de retryInestabilidad oculta> 10%
Distribucion de clasificacionesDetectar derivaCambio significativo de la baseline
Puntuaciones de calidad de salidaDetectar degradacionPromedio < 0.8

Logging estructurado

Cada ejecucion de workflow deberia producir logs trazables:

const runWorkflow = async (input, context) => {
  const runId = generateRunId();
  const startTime = Date.now();

  const log = (stage, data) => {
    logger.info({
      runId,
      stage,
      timestamp: Date.now(),
      elapsed: Date.now() - startTime,
      ...data
    });
  };

  try {
    log('start', { inputSize: input.length });

    const validated = await validate(input);
    log('validated', { valid: true });

    const processed = await process(validated);
    log('processed', {
      tokensUsed: processed.usage.total,
      model: processed.model
    });

    const output = await format(processed);
    log('complete', {
      success: true,
      outputSize: output.length,
      totalTime: Date.now() - startTime
    });

    return output;

  } catch (error) {
    log('error', {
      error: error.message,
      stage: error.stage,
      recoverable: error.recoverable
    });
    throw error;
  }
};

Juntando todo: Un ejemplo completo

Construyamos un workflow de analisis de documentos que use todo lo que hemos discutido:

class DocumentAnalysisPipeline {
  constructor(options = {}) {
    this.maxChunkSize = options.maxChunkSize || 3000;
    this.concurrency = options.concurrency || 5;
  }

  async run(document) {
    // Etapa 1: Validar
    const validated = await this.validate(document);

    // Etapa 2: Pre-procesar
    const chunks = await this.preprocess(validated);

    // Etapa 3: Analisis paralelo con control de concurrencia
    const analyses = await this.analyzeChunks(chunks);

    // Etapa 4: Sintetizar
    const synthesis = await this.synthesize(analyses);

    // Etapa 5: Control de calidad
    const final = await this.qualityCheck(synthesis, document);

    return final;
  }

  async validate(document) {
    if (!document || typeof document !== 'string') {
      throw new ValidationError('El documento debe ser un string no vacio');
    }

    if (document.length > 1000000) {
      throw new ValidationError('El documento excede el tamano maximo');
    }

    return document;
  }

  async preprocess(document) {
    return chunkDocument(document, {
      maxTokens: this.maxChunkSize,
      overlap: 200
    });
  }

  async analyzeChunks(chunks) {
    const results = [];

    // Procesar en lotes para controlar concurrencia
    for (let i = 0; i < chunks.length; i += this.concurrency) {
      const batch = chunks.slice(i, i + this.concurrency);
      const batchResults = await Promise.all(
        batch.map((chunk, idx) => this.analyzeChunk(chunk, i + idx))
      );
      results.push(...batchResults);
    }

    return results;
  }

  async analyzeChunk(chunk, index) {
    return await withRetry(async () => {
      const result = await llm.call({
        system: `Analiza esta seccion del documento. Extrae:
                 - Temas y topicos clave
                 - Hechos y cifras importantes
                 - Citas o declaraciones notables
                 - Preguntas o vacios de informacion

                 Devuelve JSON estructurado.`,
        user: `Seccion ${index + 1}:\n\n${chunk}`
      });

      return parseWithRecovery(result, ANALYSIS_SCHEMA);
    });
  }

  async synthesize(analyses) {
    const combined = analyses.map((a, i) =>
      `Seccion ${i + 1}:\n${JSON.stringify(a, null, 2)}`
    ).join('\n\n---\n\n');

    return await llm.call({
      system: `Sintetiza estos analisis de secciones en un resumen completo del documento.
               Estructura:
               1. Resumen ejecutivo (2-3 oraciones)
               2. Hallazgos clave (puntos)
               3. Detalles importantes
               4. Vacios o preguntas
               5. Recomendaciones`,
      user: combined
    });
  }

  async qualityCheck(synthesis, originalDocument) {
    const check = await llm.call({
      system: `Revisa este analisis por calidad. Verifica:
               - Precision: Refleja el documento original?
               - Completitud: Se cubren los puntos principales?
               - Claridad: Esta bien organizado y claro?

               Devuelve: {score: 0-1, issues: string[], approved: boolean}`,
      user: `Analisis:\n${synthesis}\n\nOriginal (primeros 2000 caracteres):\n${originalDocument.slice(0, 2000)}`
    });

    if (!check.approved) {
      // Loguear para revision pero no fallar
      logger.warn({ issues: check.issues, score: check.score });
    }

    return {
      analysis: synthesis,
      qualityScore: check.score,
      qualityIssues: check.issues
    };
  }
}

Trampas comunes y como evitarlas

Despues de construir docenas de estos sistemas, estos son los errores que vemos mas frecuentemente:

1. Sin limites de timeout

Cada llamada LLM necesita un timeout. Configurallos agresivamente.

// Malo: Sin timeout
const result = await llm.call(params);

// Bueno: Timeout explicito
const result = await Promise.race([
  llm.call(params),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 30000)
  )
]);

2. Ignorar limites de tokens

Rastrea el uso y establece presupuestos:

const tokenBudget = {
  max: 10000,
  used: 0,

  canSpend(amount) {
    return this.used + amount <= this.max;
  },

  spend(amount) {
    this.used += amount;
    if (this.used > this.max * 0.8) {
      logger.warn('Presupuesto de tokens consumido al 80%');
    }
  }
};

3. Sin fallback para rutas criticas

Siempre ten un plan B:

const processWithFallback = async (input) => {
  try {
    return await primaryProcess(input);
  } catch (error) {
    if (error.recoverable) {
      return await simplifiedProcess(input);
    }
    // Ruta critica - poner en cola para procesamiento manual
    await queueForManualReview(input, error);
    return { status: 'queued_for_review' };
  }
};

Que viene despues

Los workflows de IA se estan volviendo mas sofisticados. Esto es hacia donde vemos que van las cosas:

  • Mejor uso de herramientas: Los modelos mejoran en decidir cuando y como usar herramientas externas
  • Contexto mas largo: Ventanas de contexto mas grandes significan menos dolores de cabeza con chunking
  • Inferencia mas rapida: La latencia esta bajando, habilitando workflows en tiempo real mas complejos
  • Modelos especializados: Los modelos fine-tuned para tareas especificas superan a los de proposito general

Pero los fundamentos no cambian. Valida entradas, maneja errores elegantemente, monitorea todo y siempre ten un fallback. Construye sobre esos principios, y tus workflows de IA sobreviviran el contacto con el mundo real.

Si estas construyendo workflows de IA y chocando contra muros, probablemente ya hemos visto tu problema antes. Contactanos, y encontremos la solucion juntos.

Topics covered

diseno de workflows IAarquitectura de pipelines IAencadenamiento de promptsmanejo de errores IAorquestacion LLMautomatizacion IAramificacion de workflowsingenieria de sistemas IA

Ready to implement agentic AI?

Our team specializes in building production-ready AI systems. Let's discuss how we can help you leverage agentic AI for your enterprise.

Start a conversation