Paralelización en agentes IA: Sectioning y Voting paso a paso

Aprende a ejecutar tareas de IA en paralelo: Sectioning divide el trabajo, Voting repite para mayor fiabilidad. Guía con código para juniors.

Colaboradores: Esther Aznar

Imagina que tienes que analizar diez documentos. Si los procesas uno detrás de otro, puedes estar esperando un minuto mas o menos mirando la pantalla. Y eso no mola. La buena noticia es que hay dos formas de hacer ese trabajo en paralelo, y cada una sirve para algo distinto.

La primera se llama Sectioning: coges la tareas, la divides en partes independientes y lanzas todas a la vez. La segunda se llama Voting: en vez de dividir, repites la misma tarea varias veces y te quedas con el resultado en el que más coinciden los agentes. Una divide el trabajo, la otra lo repite para estar más seguro del resultado.

Para seguir este post necesitas saber más o menos qué es un agente IA (básicamente un programa que le manda tareas a un modelo de lenguaje y hace algo con la respuesta) y tener nociones básicas de TypeScript con async/await. Si todavía no has visto cómo funciona eso de encadenar llamadas paso a paso, esmpieza por el post de prompt chaining (dividir una tarea compleja en pasos encadenados — más aquí) y luego vuelves aquí.

¿Qué significa paralelizar en un sistema agéntico?

Imagina que tienes que preparar tres platos para una cena. Si los cocinas uno detrás del otro, tardas la suma de los tres. Si los pones a calentar al mismo tiempo, tardas lo que tarda el más lento. Eso es paralelizar: hacer varias cosas a la vez en lugar de esperar que termine una para empezar la siguiente.

En un sistema agéntico (un sistema con varios agentes que trabajan juntos), paralelizar significa lanzar varias llamadas al modelo al mismo tiempo y esperar a que todas terminen. En TypeScript, Promise.all() hace exactamente eso: recibe un array de promesas y las ejecuta en paralelo, devolviendo un array con todos los resultados cuando la última termina.

// Sin paralelizar: 15 segundos si cada tarea tarda 5
const result1 = await task1(); // espera 5s
const result2 = await task2(); // espera 5s más
const result3 = await task3(); // espera 5s más

// Con Promise.all: 5 segundos (el tiempo de la más lenta)
const [result1, result2, result3] = await Promise.all([
  task1(), // las tres empiezan al mismo tiempo
  task2(),
  task3(),
]);

La diferencia de latencia se nota desde las primeras pruebas. Pero el tiempo ahorrado no siempre es gratis, y ahí es donde entra la elección entre Sectioning y Voting.

Sectioning: divide la tarea, ejecuta en paralelo

Sectioning es el patrón que usas cuando una tarea grande se puede dividir en partes independientes. Y cuando digo independientes, quiero decir que cada parte no necesita saber qué están haciendo las demás para funcionar.

Un ejemplo concreto: tienes que analizar diez contratos PDF para detectar cláusulas problemáticas. Cada contrato es independiente de los demás. No necesitas leer el contrato 3 para analizar el contrato 7. Puedes lanzarlos todos a la vez.

// Sectioning: cada contrato se analiza en paralelo
async function analyzeContracts(contracts: string[]) {
  // Convertimos cada contrato en una llamada al modelo
  const analysisPromises = contracts.map((contract) =>
    callLLM(contract, "Detecta cláusulas problemáticas en este contrato")
  );

  // Promise.all espera a que todos los análisis terminen
  const results = await Promise.all(analysisPromises);

  // results[0] = análisis del contrato 0, results[1] del contrato 1, etc.
  return results;
}

Si tienes diez contratos y cada análisis tarda ocho segundos, en secuencial esperas ochenta segundos. Con Sectioning esperas ocho. El coste total de tokens es exactamente el mismo porque estás procesando el mismo texto en total, simplemente lo haces todo a la vez en lugar de en fila.

Este patrón tiene nombre en la programación clásica. Scatter-Gather (dispersar tareas y recoger resultados), MapReduce (mapear una operación sobre una colección y reducir los resultados) o Fork-Join (dividir en ramas paralelas y unirlas al final) según el contexto, pero la idea de fondo es siempre la misma: repartes el trabajo, cada parte lo hace por su cuenta y al final recoge todos los resultados. En Sectioning simplemente estas haciendo eso mismo, pero cada “parte de trabajo” es una llamada a un modelo de lenguaje.

¿Cuándo tiene sentido usarlo? Cuando tienes varios elementos similares que procesar, cuando cada uno es independiente de los demás, y cuando el tiempo de espera es un problema real.

Pero ¿qué pasa cuando no tienes una colección de cosas distintas, sino una sola tarea que necesitas que salga bien a la primera? Ahí entra Voting.

Voting: repite la misma tarea y agrega los resultados

Los modelos de lenguaje no siempre dan la misma respuesta. Si preguntas lo mismo varias veces, obtienes respuestas ligeramente distintas, y algunas serán mejores que otras. Voting aprovecha esa variabilidad: lanzas la misma tarea N veces en paralelo y luego te quedas con el resultado en el que más coinciden los agentes.

¿Cómo decides cuál gana? Depende del tipo de respuesta que esperas:

Mayoría simple: la respuesta que aparece más veces gana. Funciona cuando las respuestas son fáciles de comparar: “positivo / negativo”, respuestas de sí o no, categorías concretas. Si de cinco ejecuciones cuatro dicen “positivo” y una dice “negativo”, el resultado es “positivo”. Simple.

Por pesos: no todas las respuestas cuentan igual. Por ejemplo, puedes lanzar una llamada con temperatura baja (el modelo es más consistente, casi siempre dice lo mismo) y otras con temperatura alta (más variedad), y darle más peso a la primera. La temperatura es básicamente cuánto “varía” el modelo en cada respuesta (más sobre temperatura aquí): un valor bajo produce siempre la misma respuesta, un valor alto produce más variedad.

LLM-as-Judge: un modelo lee todas las respuestas y decide cuál es la mejor. Es el método más robusto cuando las respuestas son textos largos que no se pueden comparar con lógica simple, no tiene sentido contar cuántas veces aparece la misma frase. Si quieres entender este patrón en detalle, hay un post dedicado: Modelo como juez: evalúa las respuestas de tu agente IA.

// Voting con mayoría simple para clasificación de sentimiento
async function classifyWithVoting(text: string, runs: number = 5) {
  // Lanzamos la misma clasificación N veces en paralelo
  const votes = await Promise.all(
    Array.from({ length: runs }, () =>
      classifyText(text) // devuelve "positivo" o "negativo"
    )
  );

  // Contamos cuántas veces aparece cada respuesta
  const counts: Record<string, number> = {};
  for (const vote of votes) {
    counts[vote] = (counts[vote] ?? 0) + 1;
  }

  // Devolvemos la respuesta más frecuente
  return Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
}

El error más común con Voting es agregar resultados sin comprobar que tienen sentido juntos. Si las cinco respuestas son completamente distintas entre sí, el resultado ganador puede no ser fiable aunque “gane por mayoría”. Antes de confiar en el resultado, vale la pena revisar qué tan de acuerdo estaban los agentes.

¿Cuándo tiene sentido usarlo? En tareas donde la precisión importa de verdad: clasificación de tickets de soporte, decisiones de moderación, análisis de riesgo. No para tareas rutinarias donde una respuesta correcta e suficiente y no merece la pena multiplicar el coste.

¿Cuánto cuesta paralelizar?

Aquí viene lo que la mayoría de tutoriales no te cuentan: paralelizar no siempre es gratis.

Con Sectioning, el coste total es el mismo que hacerlo en secuencia. Diez análisis son diez análisis, da igual si los haces en fila o todos a la vez. Pagas lo mismo y ganas tiempo. Fácil.

Con Voting la historia cambia. Si lanzas la misma tarea cinco veces, pagas cinco veces. Así de simple. La pregunta que tienes que hacerte siempre es: ¿la mejora en fiabilidad justifica ese coste extra?

Patrón¿Qué reduce?¿Qué multiplica?
SectioningLatencia totalNada (coste igual que secuencial)
Voting (N runs)FiabilidadCoste × N

Para Sectioning, la respuesta casi siempre es sí: misma factura, menos espera. Para Voting, hay que pensárselo. Si tu tarea ya funciona bien con una sola llamada en la mayoría de los casos, añadir cuatro runs más puede no valer lo que cuesta. Pero si estás tomando decisiones con consecuencias reales moderar contenido, aprobar transacciones, clasificar algo crítico, puede que valga cada céntimo.

Errores comunes

Paralelizar tareas con dependencias

El error más frecuente cuando alguien descubre Promise.all() por primera vez y se emociona. Si el paso B necesita el resultado del paso A para funcionar, no puedes lanzarlos a la vez. B empieza antes de que A haya terminado, va a leer un valor vacío o undefined, y vas a obtener basura o una excepción directamente.

El test para saber si puedes paralelizar es rápido: ¿puede el paso B empezar sin saber lo que devolvió el paso A? Si la respuesta es no, van en secuencia. Si todavía no tienes claro como gestionar pasos que dependen unos de los otros, el post sobre prompt chaining cubre exactamente ese caso.

Usar mayoría simple con textos libres

La mayoría simple solo funciona cuando las respuestas son fáciles de comparar directamente. Si le pides a cinco agentes que escriban un resumen del mismo contrato, vas a obtener cinco resúmenes distintos. Ninguno va a ser idéntico al otro, así que “el más frecuente” es básicamente uno elegido al azar, y eso no mejora nada.

Para textos libres el método correcto es LLM-as-Judge. Usar mayoría simple en este caso no mejora nada, solo multiplica el coste por cinco sin ninguna ganancia real en calidad.

Lanzar demasiadas peticiones a la vez sin límite

Promise.all() con cincuenta o cien llamadas simultáneas puede superar los límites de tasa de la API. Los proveedores limitan cuántas llamadas puedes hacer por segundo o por minuto, y si te pasa, te devuelven una cascada de errores 429 que interrumpe todo el proceso de golpe.

La solución es procesar en lotes: en lugar de lanzar cien llamadas a la vez, lanzas grupos más pequeños con una pequeña pausa entre ellos. No es difícil de implementar, pero es mucho mejor planificarlo desde el principio que descubrirlo cuando ya esta en producción.

Checklist de implementación

  • Las tareas que voy a paralelizar son independientes entre sí (ninguna depende del resultado de otra)

  • He calculado el coste multiplicado por el número de runs antes de pasar a producción

  • El método de agregación es compatible con el tipo de respuesta (mayoría simple solo para respuestas discretas, no para texto libre)

  • Tengo manejo de errores cuando una llamada paralela falla (uso Promise.allSettled si quiero que las demás continúen aunque una falle)

  • He revisado los límites de tasa de la API y proceso en lotes si lanzo más de unas pocas decenas de llamadas en paralelo

  • Para Voting con LLM-as-Judge, el prompt del juez tiene criterios explícitos de evaluación

Preguntas Frecuentes

¿Cuál es la diferencia entre Sectioning y Voting?

Sectioning divide una tarea en partes distintas (contrato 1, contrato 2, contrato 3…) y cada parte se ejecuta una vez. Voting ejecuta la misma tarea varias veces con el mismo input y agrega los resultados. Sectioning reduce latencia sin aumentar el coste. Voting mejora la fiabilidad multiplicando el coste por el número de runs.

¿Qué pasa si una de las llamadas paralelas falla?

Promise.all() falla entera si cualquiera de las promesas lanza un error: las demás llamadas quedan pendientes pero sus resultados se descartan. Si necesitas que el resto continúe aunque una falle, usa Promise.allSettled() en su lugar. Devuelve un array con el estado de cada promesa (fulfilled o rejected) sin cancelar ninguna, y luego puedes filtrar las que fallaron y decidir qué hacer con ellas.

¿Con cuántos runs tiene sentido usar Voting?

Con tres runs ya puedes detectar una respuesta dominante. Con cinco la mayoría se estabiliza mejor en la mayoría de los casos. Por encima de siete el retorno suele ser bajo y el coste sube linealmente. Pero calibra con tus datos: si la tarea es de alto riesgo y el coste lo permite, más runs pueden estar justificados.

¿Sectioning solo sirve para listas de documentos?

No. También funciona cuando una tarea tiene secciones naturales que no dependen entre sí. Por ejemplo, si tienes que generar el título, el resumen y las etiquetas de un artículo de forma independiente, puedes generarlos en paralelo. No necesitas tener una colección de documentos para usar Sectioning: basta con que las partes de la tarea sean independientes.

¿Cuándo conviene no usar ninguno de los dos?

Cuando las tareas tienen dependencias fuertes entre sí, cuando el coste ya es alto y multiplicarlo no está justificado, o cuando el tiempo de respuesta no es crítico para tu caso de uso. Paralelizar añade complejidad: manejo de errores, límites de tasa, lógica de agregación. Si no hay una ganancia clara de latencia o fiabilidad, la versión secuencial es más fácil de mantener y de depurar.