Módulo 3: Pipelines secuenciales
Eligiendo pipeline o supervisor
Descripción
Seis lecciones construyeron el patrón pipeline por completo: la anatomía de una etapa (02), el mecanismo de encadenar el dato (03), el pipeline de Reservo corriendo de punta a punta (04), su costo de coordinación medido contra un supervisor (05), y la guarda que lo protege cuando una etapa no alcanza (06). Esta lección cierra el criterio: ¿cuándo, de verdad, conviene un pipeline sobre un supervisor? Y hay una pregunta más incómoda, que esta lección no evita: el propio pipeline de este módulo, ¿de verdad necesitaba las tres etapas en orden fijo?
La respuesta, confirmada ejecutando el código —no supuesta— es que no. Vas a probar, con
build_stage_task tal como quedó en las lecciones 03 a 06, que dos de las tres etapas —quote y
validate_policy— no leen absolutamente nada que la otra produzca. Solo confirm depende
genuinamente de lo que dejó validate_policy. Ese hallazgo no invalida este módulo —el pipeline
sigue siendo una forma válida y más simple de correr las tres etapas—, pero es exactamente la
pregunta que abre la puerta al Módulo 4: si dos etapas no dependen entre sí, ¿por qué esperar a que
una termine para empezar la otra?
Conexión con el módulo
Esta lección reusa, sin cambios, PipelineStage, build_stage_task y PIPELINE_STAGES de las
lecciones 02 a 06. No agrega ningún mecanismo de ejecución nuevo —el fan-out real, con
ThreadPoolExecutor, es del Módulo 4—; lo que agrega es el análisis que confirma, con el código
ya escrito, cuáles dependencias son reales y cuáles son solo una decisión de diseño. El mini-proyecto
de la lección 08 cierra el módulo aplicando el pipeline completo, con su guarda, sobre escenarios
nuevos.
El criterio completo: pipeline vs. supervisor
| Criterio | Pipeline (este módulo) | Supervisor (Módulo 2) |
|---|---|---|
| El orden de los pasos | Siempre el mismo, fijo desde el diseño del sistema | Puede cambiar según qué pida cada petición |
| Quién decide "¿cuál sigue?" | Nadie — ya está escrito en PIPELINE_STAGES | Un router (reglas o el modelo), en cada petición |
| Costo de ruteo | Cero llamadas, sin importar cuántas etapas tenga (lección 05) | Una llamada por decisión — o una por etapa, si decidiera en cada una |
| Un paso que no aplica a esta petición | El pipeline lo corre igual — no hay forma de "saltarlo" sin cambiar el código | El router simplemente no lo agenda |
| Cuando un paso "falla" sin excepción | Necesita una guarda explícita (lección 06) — nada avisa gratis | Un router honesto (Módulo 2, lección 07) puede devolver None y no delegar nada |
| Ejemplo en Reservo | cotizar → validar política → confirmar, SIEMPRE en ese orden | "cotiza y reserva" vs. "¿cuál es la política?" — la petición decide el camino |
Ninguna fila de esta tabla dice "el pipeline es mejor" en abstracto. Dice, con precisión, qué gana y qué pierde cada patrón — el mismo espíritu de la lección 05, que midió el ahorro sin declarar un ganador universal.
La pregunta incómoda: ¿de verdad las tres etapas dependen entre sí?
PIPELINE_STAGES las puso en un orden fijo, pero un orden fijo en el código no prueba una
dependencia real de datos — solo prueba que alguien decidió ponerlas en ese orden. La forma de
confirmar una dependencia real es simple: ¿la función que arma la tarea de una etapa lee algo que
dejó otra? Si la respuesta es no, esa etapa correría exactamente igual sin importar cuándo se ejecute
la otra.
from dataclasses import dataclass
@dataclass
class PipelineStage:
kind: str
name: str
label: str
def build_stage_task(stage, payload):
if stage.kind == "quote":
return f"Cotiza {payload['room']} {payload['tier']} {payload['hours']}h."
if stage.kind == "validate_policy":
return (f"¿Cuál es la política de cancelación para una reserva de "
f"{payload['room']} de {payload['hours']}h, antes de confirmarla?")
if stage.kind == "confirm":
if payload.get("cleared_to_book"):
return (f"Reserva {payload['room']} {payload['tier']} {payload['hours']}h "
f"para {payload['member']} -- la política de cancelación ya se validó.")
return (f"Reserva {payload['room']} {payload['tier']} {payload['hours']}h "
f"para {payload['member']}.")
raise ValueError(f"no sé armar la tarea de la etapa {stage.kind!r}")
PIPELINE_STAGES = [
PipelineStage(kind="quote", name="booking_agent", label="cotizar"),
PipelineStage(kind="validate_policy", name="policy_agent",
label="validar la política de cancelación"),
PipelineStage(kind="confirm", name="booking_agent", label="confirmar la reserva"),
]
INITIAL_PAYLOAD = {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}
print("--- ¿validate_policy necesita el price_cents que dejó quote? ---")
task_policy_before_quote = build_stage_task(PIPELINE_STAGES[1], INITIAL_PAYLOAD)
print("tarea de validate_policy, con el payload INICIAL (sin price_cents):")
print(" ", repr(task_policy_before_quote))
payload_after_quote = dict(INITIAL_PAYLOAD)
payload_after_quote["price_cents"] = 6000
task_policy_after_quote = build_stage_task(PIPELINE_STAGES[1], payload_after_quote)
print("tarea de validate_policy, con price_cents YA en el payload:")
print(" ", repr(task_policy_after_quote))
print("¿son idénticas?", task_policy_before_quote == task_policy_after_quote)
print()
print("--- ¿confirm necesita el price_cents que dejó quote? ---")
payload_cleared_no_price = {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana", "cleared_to_book": True}
task_confirm_no_price = build_stage_task(PIPELINE_STAGES[2], payload_cleared_no_price)
print("tarea de confirm, SIN price_cents en el payload:")
print(" ", repr(task_confirm_no_price))
print()
print("--- ¿confirm necesita lo que dejó validate_policy (cleared_to_book)? ---")
payload_no_policy_yet = {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}
task_confirm_before_policy = build_stage_task(PIPELINE_STAGES[2], payload_no_policy_yet)
print("tarea de confirm, ANTES de que validate_policy corriera:")
print(" ", repr(task_confirm_before_policy))
payload_after_policy = dict(payload_no_policy_yet)
payload_after_policy["cleared_to_book"] = True
task_confirm_after_policy = build_stage_task(PIPELINE_STAGES[2], payload_after_policy)
print("tarea de confirm, DESPUÉS de que validate_policy corriera:")
print(" ", repr(task_confirm_after_policy))
print("¿cambia el texto?", task_confirm_before_policy != task_confirm_after_policy)
print()
print("--- mapa de dependencias, confirmado con las cuatro llamadas de arriba ---")
print("quote depende de: nada (solo el payload inicial)")
print("validate_policy depende de: nada (solo el payload inicial) -- NO de price_cents")
print("confirm depende de: validate_policy (cleared_to_book) -- el texto SÍ cambia")
Qué esperar:
--- ¿validate_policy necesita el price_cents que dejó quote? ---
tarea de validate_policy, con el payload INICIAL (sin price_cents):
'¿Cuál es la política de cancelación para una reserva de Focus de 3h, antes de confirmarla?'
tarea de validate_policy, con price_cents YA en el payload:
'¿Cuál es la política de cancelación para una reserva de Focus de 3h, antes de confirmarla?'
¿son idénticas? True
--- ¿confirm necesita el price_cents que dejó quote? ---
tarea de confirm, SIN price_cents en el payload:
'Reserva Focus pro 3h para Ana -- la política de cancelación ya se validó.'
--- ¿confirm necesita lo que dejó validate_policy (cleared_to_book)? ---
tarea de confirm, ANTES de que validate_policy corriera:
'Reserva Focus pro 3h para Ana.'
tarea de confirm, DESPUÉS de que validate_policy corriera:
'Reserva Focus pro 3h para Ana -- la política de cancelación ya se validó.'
¿cambia el texto? True
--- mapa de dependencias, confirmado con las cuatro llamadas de arriba ---
quote depende de: nada (solo el payload inicial)
validate_policy depende de: nada (solo el payload inicial) -- NO de price_cents
confirm depende de: validate_policy (cleared_to_book) -- el texto SÍ cambia
Ahí está el hallazgo, confirmado con código, no con intuición: task_policy_before_quote y
task_policy_after_quote son idénticas — a build_stage_task para validate_policy le da
exactamente igual si price_cents existe o no en el payload, porque nunca lo lee. En cambio,
task_confirm_before_policy y task_confirm_after_policy sí difieren — la mención a "la política
de cancelación ya se validó" solo aparece cuando cleared_to_book llegó antes. confirm depende
genuinamente de validate_policy. quote y validate_policy no dependen entre sí.
Por qué siguen en orden fijo, entonces
Si quote y validate_policy no dependen entre sí, ¿por qué PIPELINE_STAGES las puso una después
de la otra en vez de juntas? Dos razones, ninguna de ellas "porque hacía falta":
-
Simplicidad de diseño. Un pipeline con tres pasos, uno detrás del otro, es más fácil de leer, trazar y depurar que un sistema que corre dos cosas a la vez y espera a que ambas terminen antes de la tercera. Cuando el ahorro de tiempo no importa —Reservo procesando una reserva a la vez, sin presión de latencia—, el orden fijo es la opción más simple que resuelve la tarea, el mismo principio que ya guio varias decisiones de esta guía.
-
El orden fijo no es gratis, pero tampoco es caro. La lección 05 ya midió que el pipeline ahorra, no cuesta, frente a un supervisor repetido. Correr
quoteyvalidate_policyen secuencia en vez de en paralelo no le agrega ninguna llamada extra al modelo — solo le agrega tiempo de reloj, que esta guía nunca contó como parte del costo de coordinación.
Cuando ese tiempo de reloj sí importa —muchas reservas por procesar, o una interfaz esperando una
respuesta rápida—, la independencia real entre quote y validate_policy deja de ser un detalle
académico: es la justificación concreta para el fan-out del Módulo 4, que corre exactamente estas
dos etapas a la vez y agrega sus resultados antes de la tercera.
Errores comunes
-
Concluir que "no dependen entre sí" significa "el orden en
PIPELINE_STAGESestá mal". No — el pipeline de este módulo sigue siendo correcto tal como está. La independencia entrequoteyvalidate_policyes una oportunidad de optimización futura (Módulo 4), no un defecto de este módulo. -
Confundir "no hay dependencia de datos" con "no hay dependencia de negocio".
confirmno leeprice_centsenbuild_stage_task, pero eso no significa que Reservo pueda confirmar una reserva sin haberla cotizado antes — la respuesta compuesta de la lección 04 sí usaprice_centspara informarle el precio al socio. La independencia que esta lección mide es puntual: qué necesitabuild_stage_taskpara armar el texto de esa etapa, no qué necesita el proceso de negocio completo. -
Pensar que la tabla de criterio de esta lección reemplaza la del Módulo 2. Esa tabla —cuándo usar reglas, decisión del modelo, o un híbrido— responde una pregunta distinta (cómo decide un router). La tabla de esta lección responde otra (pipeline contra supervisor, en general). Son complementarias, no una versión más nueva de la otra.
-
Generalizar "dos de tres etapas son independientes" a cualquier pipeline. Es el resultado de este pipeline puntual, confirmado ejecutando su código — no una propiedad universal de todos los pipelines. Un pipeline donde la etapa 2 sí necesitara
price_centsde la etapa 1 tendría una dependencia real, y forzarlo a fan-out produciría resultados incorrectos. -
Pensar que la independencia de
quoteyvalidate_policyimplica que ya construiste fan-out. No — esta lección solo mide la independencia conbuild_stage_task, ejecutado sobre payloads armados a mano. NingúnThreadPoolExecutorcorrió estas dos etapas a la vez todavía; eso es, exactamente, lo que construye el Módulo 4.
Ejercicios
Ejercicio 1: Confirma que quote tampoco depende de validate_policy (Fácil)
El ejemplo trabajado probó que validate_policy no depende de quote. Falta la otra dirección:
confirma que build_stage_task para quote produce el mismo texto con o sin cleared_to_book en
el payload.
Ver solución
task_quote_plain = build_stage_task(PIPELINE_STAGES[0], INITIAL_PAYLOAD)
payload_with_cleared = dict(INITIAL_PAYLOAD)
payload_with_cleared["cleared_to_book"] = True
task_quote_with_cleared = build_stage_task(PIPELINE_STAGES[0], payload_with_cleared)
print("tarea de quote sin cleared_to_book:", repr(task_quote_plain))
print("tarea de quote CON cleared_to_book:", repr(task_quote_with_cleared))
print("¿son idénticas?", task_quote_plain == task_quote_with_cleared)
Salida esperada:
tarea de quote sin cleared_to_book: 'Cotiza Focus pro 3h.'
tarea de quote CON cleared_to_book: 'Cotiza Focus pro 3h.'
¿son idénticas? True
Explicación: confirma la independencia en las dos direcciones — quote no lee nada de
validate_policy, tal como validate_policy no lee nada de quote. Las dos etapas son, en los
hechos, mutuamente independientes.
Ejercicio 2: Confirma que quote tampoco depende de confirm (Medio)
Completa la matriz de dependencias: confirma que build_stage_task para quote tampoco cambia si
el payload ya trae booking_id y confirmed —los datos que deja confirm, la ÚLTIMA etapa del
pipeline—.
Ver solución
payload_with_booking = dict(INITIAL_PAYLOAD)
payload_with_booking["booking_id"] = 99
payload_with_booking["confirmed"] = True
task_quote_with_booking = build_stage_task(PIPELINE_STAGES[0], payload_with_booking)
print("tarea de quote CON booking_id/confirmed ya en el payload:", repr(task_quote_with_booking))
print("¿son idénticas?", task_quote_plain == task_quote_with_booking)
Salida esperada:
tarea de quote CON booking_id/confirmed ya en el payload: 'Cotiza Focus pro 3h.'
¿son idénticas? True
Explicación: esto era esperable —quote es la PRIMERA etapa; en un pipeline real, nunca vería
booking_id antes de correr—, pero confirmarlo con código, no solo con el orden en
PIPELINE_STAGES, es la misma disciplina de esta lección: nunca asumir una dependencia, verificarla.
Ejercicio 3: Cuenta las rondas de coordinación con fan-out (Difícil)
La lección 05 contó llamadas al modelo. Esta lección cuenta algo distinto: rondas —cuántos
turnos de reloj necesitaría el pipeline si quote y validate_policy, al ser independientes,
corrieran juntas en la misma ronda, con confirm esperando a que ambas terminen (exactamente lo que
mide el Módulo 4, sin construirlo todavía). Calcula las rondas del pipeline secuencial de este módulo
contra las de ese fan-out hipotético, y confirma que el número de llamadas al modelo no cambia
entre los dos caminos.
Ver solución
sequential_rounds = 3 # quote, DESPUÉS validate_policy, DESPUÉS confirm -- una ronda cada una
fanout_rounds = 2 # ronda 1: quote y validate_policy JUNTOS -- ronda 2: confirm
print(f"pipeline secuencial (este módulo): {sequential_rounds} rondas")
print(f"fan-out hipotético (quote + validate_policy juntos, M4): {fanout_rounds} rondas")
print("llamadas al modelo en ambos casos: 7 (las MISMAS -- fan-out no cambia CUÁNTAS, solo CUÁNDO)")
Salida esperada:
pipeline secuencial (este módulo): 3 rondas
fan-out hipotético (quote + validate_policy juntos, M4): 2 rondas
llamadas al modelo en ambos casos: 7 (las MISMAS -- fan-out no cambia CUÁNTAS, solo CUÁNDO)
Explicación: esta es la distinción exacta que separa la lección 05 de lo que mide el Módulo 4. La lección 05 comparó pipeline contra supervisor y encontró una diferencia en llamadas al modelo (7 contra 10) — un ahorro real de trabajo de coordinación. Fan-out, en cambio, no reduce cuántas llamadas hacen falta —siguen siendo las mismas 7—, reduce cuántas rondas de reloj hacen falta para completarlas, porque dos de ellas dejan de esperarse la una a la otra. Son dos tipos de ahorro distintos, y confundirlos —pensar que fan-out "ahorra llamadas" como lo hace un pipeline frente a un supervisor— es exactamente el error que el Módulo 4 va a prevenir desde su primera lección.
Resumen y siguiente paso
- El criterio completo: un pipeline gana cuando el orden es genuinamente fijo y el costo de decidir en cada paso no se justifica (lección 05); pierde la robustez "gratis" de un router honesto, y necesita una guarda explícita para sus propios modos de falla (lección 06).
- El hallazgo central de esta lección, confirmado ejecutando el código: de las tres etapas de
este módulo, solo
confirmdepende genuinamente de otra (validate_policy, porcleared_to_book).quoteyvalidate_policyno leen nada la una de la otra — son, en los hechos, independientes. - Esa independencia no invalida el pipeline de este módulo —sigue siendo la forma más simple de resolver la tarea cuando el tiempo de reloj no importa—, pero es la justificación concreta y medida, no supuesta, para el patrón que viene.
- Fan-out (Módulo 4) no reduce cuántas llamadas al modelo hacen falta —el Ejercicio 3 lo confirmó: 7 en ambos caminos—; reduce cuántas rondas de reloj hacen falta, porque etapas genuinamente independientes dejan de esperarse entre sí.
Siguiente lección: 08 — Mini-proyecto: el pipeline de Reservo. Aplicas el pipeline completo —con su guarda de la lección 06— sobre tres escenarios nuevos, dos que lo completan de punta a punta y uno que activa la guarda.
Recursos adicionales
- Anthropic — Building effective agents — La distinción entre "prompt chaining" (este módulo) y "parallelization" (el patrón que anticipa el Módulo 4), presentada ahí como dos formas de flujo distintas según exista o no dependencia real entre los pasos.
- Python —
dataclasses—PipelineStage, reusado sin cambios desde la lección 02, la base sobre la que se construyó todo el análisis de esta lección. - Anthropic — Multi-agent research system — Un sistema real donde identificar qué sub-tareas son genuinamente independientes —y cuáles no— determinó qué partes del flujo se paralelizaron y cuáles se mantuvieron secuenciales.
- Python — Funciones puras y efectos observables — El principio detrás de la prueba de esta lección: si una función produce el mismo resultado sin importar qué claves adicionales tenga su entrada, esa entrada no es una dependencia real.