Módulo 6: Prompt Composition y Chaining
3. Decomposition de Tareas Complejas
Descripción
La decomposition es el proceso sistemático de dividir una tarea compleja en sub-tareas manejables, cada una con un input claro, un prompt especializado, y un output con schema validable. Es la habilidad fundamental para diseñar pipelines de LLM escalables.
En esta cápsula aprenderás el framework completo de decomposition: cómo identificar los límites naturales de las sub-tareas, cómo diseñar interfaces entre prompts, y cómo manejar dependencias para permitir paralelización cuando sea posible.
El Framework de Decomposition
Los 4 Pasos
1. IDENTIFICAR sub-tareas
└── ¿Qué pasos son independientes? ¿Cuáles dependen de otros?
2. DEFINIR interfaces
└── Input/output de cada etapa (schema con tipos de datos)
3. DISEÑAR prompts
└── Un prompt especializado por sub-tarea
4. ORDENAR con dependencias
└── Qué puede correr en paralelo vs qué debe ser secuencial
Cuándo dividir una tarea
Una tarea debe dividirse cuando:
- Tiene múltiples objetivos distintos (extraer + analizar + reportar)
- El output de una parte es necesario como input de otra
- Partes de la tarea pueden optimizarse independientemente
- El error handling debe ser granular (reintentar solo la parte fallida)
- Partes independientes pueden ejecutarse en paralelo
Ejemplo Completo: Research → Analyze → Write
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Optional
import json
client = OpenAI()
# ============================================================
# SCHEMAS DE INTERFACES (Pydantic)
# ============================================================
class HallazgoResearch(BaseModel):
"""Output de la sub-tarea de research."""
hallazgos: list[str] = Field(description="Lista de hallazgos clave")
fuentes_tipo: list[str] = Field(description="Tipo de fuente de cada hallazgo")
confianza: float = Field(ge=0, le=1, description="Confianza general en los hallazgos")
class AnalisisOutput(BaseModel):
"""Output de la sub-tarea de análisis."""
tendencias: list[str] = Field(description="Tendencias identificadas")
evidencia: dict[str, list[str]] = Field(description="Evidencia por tendencia")
contradicciones: list[str] = Field(description="Contradicciones encontradas")
conclusion_principal: str = Field(description="La conclusión más importante")
class SeccionInforme(BaseModel):
"""Una sección del informe final."""
titulo: str
contenido: str
citas: list[str]
class InformeOutput(BaseModel):
"""Output final del pipeline."""
titulo: str
resumen_ejecutivo: str
secciones: list[SeccionInforme]
conclusiones: list[str]
recomendaciones: list[str]
# ============================================================
# PROMPTS ESPECIALIZADOS
# ============================================================
PROMPT_RESEARCH = """Eres un investigador experto. Recopila información sobre el siguiente tema.
TEMA: {query}
Proporciona 8-12 hallazgos clave basados en tu conocimiento. Para cada hallazgo:
- Sé específico y basado en hechos conocidos
- Indica el tipo de fuente (estadística, estudio académico, tendencia de mercado, ejemplo práctico)
Responde en JSON:
{{
"hallazgos": [
"hallazgo 1",
"hallazgo 2"
],
"fuentes_tipo": [
"estadística",
"estudio"
],
"confianza": 0.85
}}"""
PROMPT_ANALYZE = """Eres un analista senior. Sintetiza los siguientes hallazgos en insights accionables.
HALLAZGOS:
{hallazgos_str}
TEMA ORIGINAL: {query}
Identifica:
1. Tendencias principales (3-5 tendencias claras)
2. Evidencia que soporta cada tendencia
3. Contradicciones o tensiones entre hallazgos
4. La conclusión más importante
Responde en JSON:
{{
"tendencias": ["tendencia 1", "tendencia 2"],
"evidencia": {{
"tendencia 1": ["evidencia A", "evidencia B"],
"tendencia 2": ["evidencia C"]
}},
"contradicciones": ["contradicción si existe"],
"conclusion_principal": "La conclusión más importante en 2 oraciones"
}}"""
PROMPT_WRITE = """Eres un redactor técnico. Genera un informe profesional basado en este análisis.
TEMA: {query}
CONCLUSIÓN PRINCIPAL: {conclusion}
TENDENCIAS IDENTIFICADAS:
{tendencias_str}
El informe debe:
1. Tener título descriptivo
2. Resumen ejecutivo (2-3 oraciones)
3. 2-3 secciones principales (una por tendencia clave)
4. Conclusiones (3 puntos)
5. Recomendaciones concretas (3-5 acciones)
Responde en JSON según el schema provisto."""
# ============================================================
# IMPLEMENTACIÓN DEL PIPELINE
# ============================================================
def sub_tarea_research(query: str) -> HallazgoResearch:
"""
Sub-tarea 1: Research - Recopilar información sobre el tema.
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": PROMPT_RESEARCH.format(query=query)}],
temperature=0,
max_tokens=800,
response_format={"type": "json_object"}
)
datos = json.loads(response.choices[0].message.content)
return HallazgoResearch(**datos)
def sub_tarea_analyze(query: str, research: HallazgoResearch) -> AnalisisOutput:
"""
Sub-tarea 2: Analysis - Sintetizar hallazgos en insights.
Depende de: sub_tarea_research
"""
hallazgos_str = "\n".join([f"- {h}" for h in research.hallazgos])
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": PROMPT_ANALYZE.format(
hallazgos_str=hallazgos_str,
query=query
)
}],
temperature=0,
max_tokens=800,
response_format={"type": "json_object"}
)
datos = json.loads(response.choices[0].message.content)
return AnalisisOutput(**datos)
def sub_tarea_write(query: str, analisis: AnalisisOutput) -> InformeOutput:
"""
Sub-tarea 3: Write - Generar informe estructurado.
Depende de: sub_tarea_analyze
"""
tendencias_str = "\n".join([
f"- {t}: {', '.join(analisis.evidencia.get(t, [])[:2])}"
for t in analisis.tendencias
])
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": PROMPT_WRITE.format(
query=query,
conclusion=analisis.conclusion_principal,
tendencias_str=tendencias_str
)
}],
temperature=0.2,
max_tokens=1000,
response_format={"type": "json_object"}
)
datos = json.loads(response.choices[0].message.content)
# Construir InformeOutput (manejo flexible del schema)
secciones = [SeccionInforme(**s) for s in datos.get("secciones", [])]
return InformeOutput(
titulo=datos.get("titulo", "Informe"),
resumen_ejecutivo=datos.get("resumen_ejecutivo", ""),
secciones=secciones,
conclusiones=datos.get("conclusiones", []),
recomendaciones=datos.get("recomendaciones", [])
)
def pipeline_research_analyze_write(query: str, verbose: bool = True) -> dict:
"""
Pipeline completo: Research → Analyze → Write
Args:
query: El tema a investigar y sobre el que escribir
verbose: Si True, imprime el progreso
Returns:
dict con los outputs de las 3 etapas
"""
import time
t_total = time.time()
if verbose:
print(f"📚 Investigando: {query}\n")
# Etapa 1: Research
t0 = time.time()
research = sub_tarea_research(query)
if verbose:
print(f"✓ Research completado ({time.time()-t0:.1f}s)")
print(f" {len(research.hallazgos)} hallazgos, confianza: {research.confianza:.0%}")
# Etapa 2: Analysis
t0 = time.time()
analisis = sub_tarea_analyze(query, research)
if verbose:
print(f"✓ Análisis completado ({time.time()-t0:.1f}s)")
print(f" {len(analisis.tendencias)} tendencias identificadas")
print(f" Conclusión: {analisis.conclusion_principal[:80]}...")
# Etapa 3: Write
t0 = time.time()
informe = sub_tarea_write(query, analisis)
if verbose:
print(f"✓ Informe generado ({time.time()-t0:.1f}s)")
print(f" Título: {informe.titulo}")
print(f" {len(informe.secciones)} secciones, {len(informe.recomendaciones)} recomendaciones")
print(f"\nTiempo total: {time.time()-t_total:.1f}s")
return {
"research": research.model_dump(),
"analisis": analisis.model_dump(),
"informe": informe.model_dump()
}
# Ejemplo:
if __name__ == "__main__":
resultado = pipeline_research_analyze_write(
"El impacto de los LLMs en el mercado laboral de programación en 2025-2026"
)
print("\n=== INFORME FINAL ===")
informe = resultado["informe"]
print(f"# {informe['titulo']}\n")
print(f"## Resumen Ejecutivo\n{informe['resumen_ejecutivo']}\n")
print("## Recomendaciones")
for r in informe["recomendaciones"]:
print(f"- {r}")
Framework de Decomposition: Patrones Comunes
Patrón 1: Extract → Transform → Load (ETL para LLMs)
class ETLPipeline:
"""
Patrón ETL adaptado para pipelines de LLM:
- Extract: Extraer información del input (entidades, datos, estructura)
- Transform: Transformar/analizar los datos extraídos
- Load: Formatear el output en el schema deseado
"""
def __init__(self, dominio: str):
self.dominio = dominio
def extract(self, documento: str) -> dict:
"""Extraer información estructurada del documento."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"""Extrae la información clave de este {self.dominio}:
{documento}
Responde en JSON con los campos relevantes para este tipo de documento."""}],
temperature=0,
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
def transform(self, datos_extraidos: dict, objetivo: str) -> dict:
"""Transformar/analizar los datos según el objetivo."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"""Analiza estos datos para el objetivo: {objetivo}
Datos: {json.dumps(datos_extraidos, ensure_ascii=False)}
Genera insights, calcula métricas relevantes, identifica patrones.
Responde en JSON."""}],
temperature=0,
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
def load(self, datos_transformados: dict, formato: str) -> str:
"""Formatear el output en el formato deseado."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"""Formatea estos datos en {formato}:
{json.dumps(datos_transformados, ensure_ascii=False)}"""}],
temperature=0.2
)
return response.choices[0].message.content
def run(self, documento: str, objetivo: str, formato_salida: str) -> dict:
extraido = self.extract(documento)
transformado = self.transform(extraido, objetivo)
output = self.load(transformado, formato_salida)
return {"extraido": extraido, "transformado": transformado, "output": output}
# Ejemplo: Análisis de contrato laboral
etl = ETLPipeline("contrato laboral")
resultado = etl.run(
documento="Contrato de trabajo. Empleado: Juan García. Salario: €45,000/año. Jornada: 40h/semana...",
objetivo="identificar condiciones no estándar o cláusulas de riesgo",
formato_salida="resumen ejecutivo en bullet points"
)
print(resultado["output"])
Patrón 2: Map-Reduce para Documentos Largos
from typing import Any
def map_reduce_documentos(
documentos: list[str],
prompt_map: str,
prompt_reduce: str,
batch_size: int = 5
) -> str:
"""
Procesa múltiples documentos con Map-Reduce:
- Map: Aplica prompt_map a cada documento individualmente
- Reduce: Sintetiza todos los resultados en uno
Args:
documentos: Lista de textos a procesar
prompt_map: Template para procesar cada documento. Usa {documento}
prompt_reduce: Template para sintetizar. Usa {resultados}
batch_size: Procesar en lotes si hay muchos documentos
Returns:
Síntesis final de todos los documentos
"""
# MAP: Procesar cada documento
resultados_map = []
for i, doc in enumerate(documentos):
print(f"Map: procesando documento {i+1}/{len(documentos)}...")
result = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt_map.format(documento=doc)}],
temperature=0,
max_tokens=400
).choices[0].message.content
resultados_map.append(result)
# Si hay muchos resultados, hacer reduce jerárquico
while len(resultados_map) > batch_size:
nuevos_resultados = []
for i in range(0, len(resultados_map), batch_size):
lote = resultados_map[i:i+batch_size]
lote_str = "\n---\n".join([f"Resultado {i+j+1}:\n{r}" for j, r in enumerate(lote)])
reduce_parcial = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt_reduce.format(resultados=lote_str)}],
temperature=0,
max_tokens=400
).choices[0].message.content
nuevos_resultados.append(reduce_parcial)
resultados_map = nuevos_resultados
# REDUCE: Síntesis final
todos_str = "\n---\n".join([f"Resultado {i+1}:\n{r}" for i, r in enumerate(resultados_map)])
sintesis_final = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt_reduce.format(resultados=todos_str)}],
temperature=0.1,
max_tokens=600
).choices[0].message.content
return sintesis_final
# Ejemplo: Analizar múltiples reseñas de clientes
resenas = [
"El producto es excelente pero el soporte técnico es pésimo.",
"Llegó tarde pero la calidad superó mis expectativas.",
"No recomendaría. El manual está en inglés y no hay versión en español.",
"Perfecto en todos los aspectos. Vale cada euro.",
"Funciona bien pero se calienta demasiado después de 2 horas de uso.",
]
prompt_map_resenas = "Extrae en 1-2 oraciones el problema o elogio principal de esta reseña: {documento}"
prompt_reduce_resenas = "Sintetiza estos análisis de reseñas en un resumen ejecutivo con: tendencias positivas, tendencias negativas, y 3 acciones prioritarias.\n\n{resultados}"
sintesis = map_reduce_documentos(resenas, prompt_map_resenas, prompt_reduce_resenas)
print("Síntesis de reseñas:", sintesis)
Patrón 3: Decomposition con Paralelización Inteligente
import asyncio
from openai import AsyncOpenAI
from dataclasses import dataclass
from typing import set
@dataclass
class SubTareaConfig:
nombre: str
prompt_template: str
dependencias: list[str] # Nombres de sub-tareas de las que depende
puede_paralelizar: bool = True
def construir_dag(sub_tareas: list[SubTareaConfig]) -> dict[str, set]:
"""Construye un DAG (Directed Acyclic Graph) de dependencias."""
dag = {t.nombre: set(t.dependencias) for t in sub_tareas}
return dag
def orden_topologico(dag: dict[str, set]) -> list[list[str]]:
"""
Calcula el orden topológico del DAG.
Retorna grupos de tareas que pueden ejecutarse en paralelo.
"""
orden_grupos = []
dag_copia = {k: set(v) for k, v in dag.items()}
while dag_copia:
# Tareas sin dependencias pendientes (pueden ejecutarse en este paso)
sin_deps = [nombre for nombre, deps in dag_copia.items() if not deps]
if not sin_deps:
raise ValueError("Ciclo detectado en el DAG de dependencias")
orden_grupos.append(sin_deps)
# Remover estas tareas del grafo
for nombre in sin_deps:
del dag_copia[nombre]
# Remover estas tareas de las dependencias de las restantes
for nombre in list(dag_copia.keys()):
dag_copia[nombre] -= set(sin_deps)
return orden_grupos
async def ejecutar_con_paralelizacion(
sub_tareas: list[SubTareaConfig],
input_inicial: str,
verbose: bool = True
) -> dict[str, str]:
"""
Ejecuta sub-tareas respetando dependencias y paralelizando cuando es posible.
"""
client_async = AsyncOpenAI()
dag = construir_dag(sub_tareas)
grupos = orden_topologico(dag)
configs_map = {t.nombre: t for t in sub_tareas}
resultados = {"_input_inicial": input_inicial}
if verbose:
print(f"Plan de ejecución: {len(grupos)} grupos")
for i, grupo in enumerate(grupos, 1):
print(f" Grupo {i}: {grupo} ({'paralelo' if len(grupo) > 1 else 'secuencial'})")
for i, grupo in enumerate(grupos, 1):
if verbose:
print(f"\n[Grupo {i}] Ejecutando: {grupo}")
async def ejecutar_subtarea(nombre: str) -> tuple[str, str]:
config = configs_map[nombre]
# Construir context con resultados disponibles
context = {**resultados}
context["input"] = resultados.get(config.dependencias[-1], input_inicial) if config.dependencias else input_inicial
prompt = config.prompt_template.format(**context)
response = await client_async.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=400
)
return nombre, response.choices[0].message.content
# Ejecutar grupo en paralelo
tareas_async = [ejecutar_subtarea(nombre) for nombre in grupo]
nuevos_resultados = await asyncio.gather(*tareas_async)
resultados.update(dict(nuevos_resultados))
if verbose:
for nombre, resultado in nuevos_resultados:
print(f" ✓ {nombre}: {resultado[:80]}...")
return {k: v for k, v in resultados.items() if not k.startswith("_")}
# Ejemplo de uso con DAG:
sub_tareas_analisis = [
SubTareaConfig(
nombre="extraccion_entidades",
prompt_template="Extrae personas, organizaciones y fechas de: {input}",
dependencias=[] # No depende de nada
),
SubTareaConfig(
nombre="resumen",
prompt_template="Resume en 3 oraciones: {input}",
dependencias=[] # No depende de nada (paralela a extraccion_entidades)
),
SubTareaConfig(
nombre="analisis_sesgo",
prompt_template="Analiza el sesgo de este texto considerando las entidades: {extraccion_entidades}\n\nTexto: {input}",
dependencias=["extraccion_entidades"] # Necesita las entidades
),
SubTareaConfig(
nombre="informe_final",
prompt_template="Genera un informe basado en: Resumen: {resumen} | Sesgo: {analisis_sesgo} | Entidades: {extraccion_entidades}",
dependencias=["resumen", "analisis_sesgo", "extraccion_entidades"] # Necesita todo
)
]
# asyncio.run(ejecutar_con_paralelizacion(sub_tareas_analisis, "El texto a analizar..."))
Diseño de Interfaces entre Prompts
El Principio de Interface Contracts
from pydantic import BaseModel, field_validator
from typing import Literal
class InterfaceExtraccion(BaseModel):
"""
Contrato de interface entre el prompt de extracción y el de análisis.
Cada campo tiene un tipo explícito y validaciones.
"""
entidades: list[str]
fechas: list[str]
numeros_clave: list[float]
sentimiento: Literal["positivo", "negativo", "neutral", "mixto"]
@field_validator("numeros_clave", mode="before")
@classmethod
def limpiar_numeros(cls, v):
"""Normalizar números: remover comas, porcentajes, etc."""
resultado = []
for item in v:
if isinstance(item, (int, float)):
resultado.append(float(item))
elif isinstance(item, str):
cleaned = item.replace(",", "").replace("%", "").strip()
try:
resultado.append(float(cleaned))
except ValueError:
pass
return resultado
class InterfaceAnalisis(BaseModel):
"""
Contrato de interface entre análisis y generación de informe.
"""
conclusiones: list[str]
nivel_riesgo: Literal["bajo", "medio", "alto", "crítico"]
acciones_requeridas: list[str]
datos_clave: dict[str, float]
def prompt_con_interface_contract(
texto: str,
schema: type[BaseModel]
) -> BaseModel:
"""
Ejecuta un prompt y valida el output contra un schema de interface.
Reintenta si el formato es incorrecto.
"""
schema_json = schema.model_json_schema()
prompt = f"""Analiza el siguiente texto y responde en JSON según este schema:
{json.dumps(schema_json, indent=2)}
Texto: {texto}
IMPORTANTE: El JSON debe ser válido y contener todos los campos requeridos."""
for intento in range(3):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
response_format={"type": "json_object"}
).choices[0].message.content
try:
datos = json.loads(response)
return schema(**datos)
except Exception as e:
if intento < 2:
prompt += f"\n\nError en intento anterior: {e}. Por favor corrige."
else:
raise ValueError(f"No se pudo parsear el output después de 3 intentos: {e}")
# Ejemplo:
texto_noticia = "Apple reportó ingresos de $124.3 millones el 15 de enero de 2026. Tim Cook destacó el crecimiento del 23% en servicios."
interface_result = prompt_con_interface_contract(texto_noticia, InterfaceExtraccion)
print(f"Entidades: {interface_result.entidades}")
print(f"Números: {interface_result.numeros_clave}")
print(f"Sentimiento: {interface_result.sentimiento}")
Troubleshooting de Decomposition
Problema 1: Sub-tareas demasiado grandes
Síntoma: Una sub-tarea tiene múltiples outputs distintos y el prompt es demasiado complejo.
Señal de alerta: Un prompt que usa más de 5 "extrae/genera/analiza" en la misma instrucción.
Solución:
# ❌ SUB-TAREA DEMASIADO GRANDE
prompt_gigante = """Analiza este contrato.
Extrae todas las partes involucradas.
Identifica las cláusulas de riesgo.
Calcula las penalizaciones.
Compara con estándares del sector.
Genera recomendaciones.
Traduce el resumen al inglés."""
# ✓ DIVIDIR EN 5 SUB-TAREAS ESPECIALIZADAS
prompts_especializados = {
"partes": "Identifica todas las partes (personas y organizaciones) en este contrato: {input}",
"clausulas_riesgo": "Lista las cláusulas que representan riesgo legal o financiero: {input}",
"penalizaciones": "Extrae y calcula las penalizaciones mencionadas en el contrato: {input}",
"comparacion": "Compara estas cláusulas {clausulas_riesgo} con estándares estándar del sector",
"recomendaciones": "Genera 5 recomendaciones basadas en el análisis de riesgo: {clausulas_riesgo}"
}
Problema 2: Interfaces ambiguas
Síntoma: La etapa 2 no sabe exactamente qué formato esperar de la etapa 1.
Solución:
# ❌ INTERFACE AMBIGUA
prompt_vago = "Extrae los datos importantes del texto."
# ✓ INTERFACE CON SCHEMA EXPLÍCITO
prompt_especifico = """Extrae del texto:
1. Fecha (formato: DD/MM/YYYY)
2. Importe total (solo número, sin símbolos)
3. Partes (lista de nombres propios)
4. Tipo de documento (contrato/factura/carta/otro)
Responde en JSON:
{{"fecha": "DD/MM/YYYY o null", "importe": number o null, "partes": [str], "tipo": str}}"""
Problema 3: Pérdida de contexto entre sub-tareas
Síntoma: La etapa 3 no tiene suficiente contexto del problema original porque solo recibe el output de la etapa 2.
Solución: Pasar el input original más los outputs intermedios relevantes:
def prompt_con_contexto_completo(
input_original: str,
outputs_previos: dict,
template_actual: str
) -> str:
"""
Construye un prompt que incluye contexto del problema original
y los outputs relevantes de etapas anteriores.
"""
context = f"CONTEXTO ORIGINAL: {input_original[:500]}\n\n"
for nombre, output in outputs_previos.items():
context += f"RESULTADO DE {nombre.upper()}:\n{output[:200]}\n\n"
return context + template_actual.format(input=outputs_previos.get("ultimo", input_original))
Ejercicios
Ejercicio 1: Decomposition de análisis de datos financieros
Tienes que analizar un reporte anual de una empresa. Descompón esta tarea en sub-tareas, define las interfaces, y especifica las dependencias.
Ver solución
sub_tareas_financieras = [
SubTareaConfig(
nombre="extraccion_metricas",
prompt_template="""Extrae los siguientes números del reporte financiero:
- Revenue total
- EBITDA
- Net income
- Deuda neta
- Empleados
Responde en JSON: {{"revenue": float, "ebitda": float, "net_income": float, "deuda_neta": float, "empleados": int}}
Reporte: {input}""",
dependencias=[]
),
SubTareaConfig(
nombre="extraccion_narrativa",
prompt_template="Extrae los 5 puntos clave de la narrativa del CEO y el mensaje estratégico: {input}",
dependencias=[] # Paralelo a extraccion_metricas
),
SubTareaConfig(
nombre="calculo_ratios",
prompt_template="""Calcula estos ratios financieros con los datos:
{extraccion_metricas}
- Margen EBITDA (EBITDA/Revenue)
- ROE si hay equity disponible
- Apalancamiento (Deuda/EBITDA)
Responde en JSON con valores y interpretación.""",
dependencias=["extraccion_metricas"]
),
SubTareaConfig(
nombre="sintesis_estrategica",
prompt_template="""Genera un análisis estratégico usando:
Métricas: {extraccion_metricas}
Ratios: {calculo_ratios}
Narrativa: {extraccion_narrativa}
Incluye: salud financiera, riesgos, oportunidades.""",
dependencias=["extraccion_metricas", "calculo_ratios", "extraccion_narrativa"]
)
]
Ejercicio 2: Implementar Map-Reduce para FAQ
Tienes 20 preguntas frecuentes de clientes. Usa Map-Reduce para:
- (Map) Clasificar cada pregunta por categoría
- (Reduce) Generar una respuesta consolidada por categoría
Ver solución
faqs = [
"¿Cómo cambio mi contraseña?",
"¿Por qué no puedo iniciar sesión?",
"¿Dónde está mi pedido?",
"¿Cuánto tarda el envío?",
"¿Cómo devuelvo un producto?",
"¿Tienen aplicación móvil?",
"¿Cómo cancelo mi suscripción?",
]
prompt_map = """Clasifica esta pregunta y extrae la intención:
Pregunta: {documento}
JSON: {{"categoria": "cuenta|pedidos|devoluciones|app|suscripcion|otro", "intencion": str}}"""
prompt_reduce = """Tienes estas clasificaciones de FAQs:
{resultados}
Genera:
1. Resumen por categoría (N preguntas por categoría)
2. Las 3 categorías más frecuentes
3. Gaps de información que podrían convertirse en FAQs adicionales"""
resultado = map_reduce_documentos(faqs, prompt_map, prompt_reduce)
print(resultado)
Ejercicio 3: Diseñar interfaces con Pydantic
Para un pipeline de análisis de código Python, diseña los schemas Pydantic para las interfaces entre:
- Análisis de complejidad
- Detección de code smells
- Sugerencias de refactoring
Ver solución
from pydantic import BaseModel, Field
from typing import Literal
class AnalisisComplejidad(BaseModel):
complejidad_ciclomatica: int = Field(ge=1, description="Complejidad ciclomática de la función")
lineas_codigo: int
funciones: list[str]
nivel: Literal["simple", "moderado", "complejo", "muy_complejo"]
class CodeSmell(BaseModel):
tipo: str
descripcion: str
linea: Optional[int] = None
severidad: Literal["baja", "media", "alta"]
class AnalisisCodeSmells(BaseModel):
smells: list[CodeSmell]
deuda_tecnica_estimada_horas: float
codigo_limpio_score: float = Field(ge=0, le=10)
class SugerenciaRefactoring(BaseModel):
titulo: str
descripcion: str
impacto: Literal["bajo", "medio", "alto"]
esfuerzo: Literal["bajo", "medio", "alto"]
codigo_sugerido: Optional[str] = None
class PlanRefactoring(BaseModel):
sugerencias: list[SugerenciaRefactoring]
prioridad_recomendada: list[str] # Nombres de sugerencias en orden
beneficio_esperado: str
Resumen
- Decomposition framework: Identificar sub-tareas → definir interfaces → diseñar prompts → ordenar con dependencias
- Patterns: ETL para procesamiento, Map-Reduce para colecciones, DAG para dependencias complejas
- Interfaces: Usar Pydantic para schemas de interface. Hace el pipeline robusto y validable.
- Paralelización: Calcular el orden topológico del DAG y ejecutar grupos en paralelo con asyncio
- Sub-tareas bien diseñadas: Una responsabilidad clara, schema de input/output explícito, < 300 tokens de prompt