Map-reduce en agentes IA: documentos largos sin overflow
El cliente manda un contrato de 180 páginas. O 200 facturas en un ZIP. O un informe anual de 300 páginas que hay que resumir, clasificar y extraer datos clave.
Tu agente IA lo recibe. Y revienta.
No porque el modelo sea malo. Porque ningún LLM actual puede ingerir 180 páginas en una sola llamada y darte una respuesta coherente. El contexto tiene un límite, el coste por token escala de forma no lineal, y cuanto más largo el input, más pierde calidad la respuesta — el fenómeno conocido como lost in the middle.
El problema no es tecnológico. Es arquitectónico. Y tiene solución.
Por qué los enfoques naivos fallan
El primer impulso cuando te llega un documento largo es buscar el modelo con el contexto más grande. Claude tiene 200K tokens. GPT-4 tiene 128K. Con eso debería bastar, ¿no?
No.
Problema 1: el coste. Un documento de 200 páginas son aproximadamente 150.000 tokens. A los precios actuales de los modelos premium, procesar ese documento con Sonnet cuesta entre 0,90€ y 1,50€ por llamada. Si tienes 50 contratos al mes, son 75€ solo en ese proceso. Sin contar que necesitarás iteraciones, reprocesados y errores.
Problema 2: la calidad. Los modelos tienen un fenómeno documentado: la información del principio y el final del contexto se retiene bien; la del medio, no. Un contrato de 180 páginas tiene la mayoría de las cláusulas relevantes en las páginas 40-140. Justo donde el modelo pierde foco.
Problema 3: los timeouts. Una llamada con 150K tokens de entrada puede tardar 30-60 segundos en responder. En una arquitectura serverless como Vercel, eso es un timeout garantizado. En una arquitectura propia, es un bloqueo de recursos que no puedes asumir con concurrencia real.
El RAG (retrieval-augmented generation) resuelve otro problema distinto: te ayuda a encontrar documentos relevantes en una base de conocimiento. No está diseñado para procesar un único documento largo de principio a fin extrayendo información estructurada.
El patrón correcto es map-reduce.
El patrón map-reduce para documentos largos
La idea viene de la informática distribuida de los años 2000: divide el problema en partes independientes (map), procésalas en paralelo, y luego combina los resultados (reduce).
Aplicado a agentes IA con documentos largos:
- Chunk — divide el documento en fragmentos manejables (típicamente 10-25 páginas cada uno)
- Map — lanza un agente LLM ligero sobre cada chunk en paralelo con
Promise.all() - Reduce — un agente final sintetiza los resultados parciales en una respuesta coherente
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
type ChunkResult = {
chunkIndex: number;
keyPoints: string[];
entities: string[];
flags: string[];
};
async function processChunk(
text: string,
index: number
): Promise<ChunkResult> {
const response = await anthropic.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 1024,
messages: [
{
role: "user",
content: `Analiza este fragmento del documento (páginas ${index * 25 + 1}-${(index + 1) * 25}).
Extrae en JSON con este schema exacto:
{
"keyPoints": ["punto clave 1", "punto clave 2"],
"entities": ["empresa X", "persona Y", "fecha Z"],
"flags": ["cláusula problemática si la hay"]
}
FRAGMENTO:
${text}`,
},
],
});
const content = response.content[0];
if (content.type !== "text") throw new Error("Unexpected response type");
const parsed = JSON.parse(content.text);
return { chunkIndex: index, ...parsed };
}
async function reduceResults(
results: ChunkResult[],
task: string
): Promise<string> {
const summary = results
.map(
(r) =>
`[Chunk ${r.chunkIndex + 1}]\n` +
`Puntos clave: ${r.keyPoints.join(", ")}\n` +
`Entidades: ${r.entities.join(", ")}\n` +
`Alertas: ${r.flags.join(", ") || "ninguna"}`
)
.join("\n\n");
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 2048,
messages: [
{
role: "user",
content: `Tienes los resultados del análisis por secciones de un documento de ${results.length * 25} páginas.
TAREA: ${task}
RESULTADOS POR SECCIÓN:
${summary}
Sintetiza toda la información en un informe coherente, sin repetir información y priorizando lo más relevante.`,
},
],
});
const content = response.content[0];
if (content.type !== "text") throw new Error("Unexpected response type");
return content.text;
}
export async function processLongDocument(
documentText: string,
task: string,
chunkSize = 8000 // ~25 páginas en tokens
): Promise<string> {
// 1. CHUNK: divide el documento
const words = documentText.split(" ");
const chunkWords = Math.floor(chunkSize / 1.3); // approx tokens → words
const chunks: string[] = [];
for (let i = 0; i < words.length; i += chunkWords) {
chunks.push(words.slice(i, i + chunkWords).join(" "));
}
// 2. MAP: procesa en paralelo con modelo ligero
const mapResults = await Promise.all(
chunks.map((chunk, i) => processChunk(chunk, i))
);
// 3. REDUCE: sintetiza con modelo más potente
return reduceResults(mapResults, task);
}
Optimizaciones clave en producción
Usa modelos distintos en cada fase. En el map, cada chunk es una tarea simple de extracción: Haiku lo resuelve igual de bien que Sonnet a una fracción del coste. En el reduce, la síntesis requiere razonamiento más complejo: ahí sí usas el modelo más potente. Esta asignación puede reducir el coste total entre un 60% y un 70% respecto a usar Sonnet para todo.
Controla el chunk overlap. Cuando divides un documento, las frases que caen en el límite de un chunk quedan partidas. Un overlap de 200-300 tokens entre chunks consecutivos evita que pierdas información crítica en los bordes. Añade esto al loop de chunking: en lugar de i += chunkWords, usa i += chunkWords - overlap.
Filtra resultados vacíos antes del reduce. No todos los chunks tienen información relevante. Un contrato de 200 páginas tiene 40 páginas de definiciones boilerplate que no aportan nada a la extracción de cláusulas clave. Filtra antes del reduce: mapResults.filter(r => r.keyPoints.length > 0).
Maneja errores por chunk, no por documento. Si un chunk falla (timeout, error de parsing del JSON), no invalides todo el proceso. Usa Promise.allSettled() en lugar de Promise.all() y procesa los resultados exitosos, marcando los fallidos para reprocesado selectivo.
Esto es parte de lo que implementamos en los proyectos de integración IA: arquitecturas que sobreviven al mundo real, donde los documentos llegan en formatos inesperados, los timeouts ocurren, y el coste tiene que ser predecible.
Cuándo usar map-reduce vs RAG vs contexto directo
La elección depende del problema:
-
Contexto directo — documento corto (menos de 20 páginas), necesitas coherencia total entre todas las partes, no puedes permitirte pérdida de información inter-chunk. El límite real de calidad está en torno a 50-80K tokens con los modelos actuales.
-
RAG — tienes una base de conocimiento grande (cientos o miles de documentos) y el usuario hace preguntas sobre ella. Recuperas los fragmentos más relevantes por similitud semántica. No es para procesar un solo documento largo.
-
Map-reduce — documento único largo (50+ páginas), la tarea es de extracción, clasificación o resumen, y la información puede analizarse por secciones de forma independiente. La mayoría de los documentos empresariales reales caen aquí: contratos, informes, expedientes, historiales.
La trampa habitual es usar RAG para todo porque ya se tiene el pipeline montado. RAG para un único documento de 200 páginas es como usar un buscador para leer un libro: técnicamente funciona, pero no es para eso.
Si tienes un proceso donde un equipo procesa documentos largos manualmente — revisando contratos, extrayendo datos de informes, clasificando expedientes — el patrón map-reduce es probablemente la pieza que falta para automatizarlo de forma confiable y con coste predecible. Es exactamente el tipo de problema que resolvemos con el servicio de agentes e integración IA.
¿Tienes un proceso de revisión de documentos que consume horas a tu equipo? Cuéntame de qué se trata — en 30 minutos vemos si tiene sentido automatizarlo.