Inicio / Artículos

Composición de pipelines de LangChain con LCEL: lineales, paralelos y ramificados.

Aprende a conectar prompts, modelos y analizadores en pipelines lineales, de múltiples etapas, paralelos y condicionales de LangChain mediante el operador pipe, RunnableParallel y RunnableBranch.

Editor: REACTAPP.TOP41 min de lectura9455 palabras

Fuentes: Chains in LangChain: Simple, Sequential, Parallel, and Conditional — Part 7

Las funciones reales de los LLM casi nunca se limitan a una sola llamada al modelo. Formulas un prompt, llamas al modelo, procesas los resultados que obtienes, introduces esos resultados en otro prompt, a veces ejecutas varias solicitudes simultáneamente y, ocasionalmente, eliges un camino diferente según la entrada. Conectar manualmente cada paso se convierte rápidamente en código complejo y difícil de leer y modificar. Esta guía explica cuatro patrones de composición en LangChain, desarrollados con el Lenguaje de Expresiones de LangChain (LCEL): pipelines simples, pipelines multietapa, distribución con RunnableParallel y enrutamiento con RunnableBranch. Cada patrón se muestra dos veces, una vez con Google Gemini y otra con OpenAI, para que puedas ver exactamente qué partes cambian al cambiar de proveedor y cuáles permanecen igual.

Qué es realmente una cadena

Una cadena es una secuencia de componentes de LangChain en la que cada paso recibe lo que produjo el paso anterior. Los componentes típicos son plantillas de prompt, modelos de chat o de completación, y analizadores de salida, pero cualquier elemento que implemente la interfaz Runnable de LangChain puede formar parte de ella. La secuencia más sencilla y útil se ve así:

Prompt → LLM → Output Parser → Output

Cada etapa tiene una función específica. La plantilla de prompt toma las variables de entrada y las convierte en instrucciones para el modelo. El modelo lee esas instrucciones y genera una respuesta. El analizador de salida da forma a esa respuesta según lo que requiere la aplicación, por ejemplo como una cadena simple o un objeto validado. Lo que sale del analizador es el resultado de toda la secuencia.

La ventaja de la abstracción es que ya no tiene que transferir los datos usted mismo entre esas etapas. Declara el orden una sola vez, y LangChain se encarga de llamar a cada componente con la entrada adecuada. Conceptualmente, está uniendo pequeños bloques para formar una unidad más grande:

Input
  ↓
Component 1
  ↓
Component 2
  ↓
Component 3
  ↓
Output

Eso tiene poca importancia en un script de dos pasos, pero se vuelve esencial cuando una aplicación crece hasta tener una docena de pasos. Un pipeline declarado mantiene el flujo de datos visible en un solo lugar, en lugar de dispersarlo entre funciones auxiliares. El modelo mental es un ritmo repetido de entrada, procesamiento, salida y luego procesamiento nuevamente sobre esa salida, hasta que se obtiene el valor final.

Por qué la composición supera al cableado manual

Cuando cada valor intermedio se transfiere de forma explícita, el código se vuelve ruidoso y frágil. Las cadenas resuelven tres problemas prácticos.

Flujos de datos sin código adicional

La promesa fundamental es que el resultado de un paso se convierte automáticamente en el argumento del siguiente. Tomemos nuevamente la secuencia familiar:

Prompt → LLM → Output Parser → Output

Si se hiciera a mano, habría que formatear la solicitud, llamar al modelo con el valor formatado, extraer el texto del objeto de respuesta y entregárselo al analizador, manteniendo una variable para cada paso. Con LCEL, el mismo flujo se reduce a una sola expresión construida con el operador de tubería:

chain = prompt | llm | parser

El operador | funciona porque las solicitudes, los modelos y los analizadores implementan todos la misma interfaz Runnable, por lo que cada uno sabe cómo aceptar la salida del componente anterior. Menos variables temporales significa menos posibilidades de pasar un valor incorrecto, y la cadena de procesamiento se lee de izquierda a derecha en el mismo orden en que se ejecuta.

La lógica de múltiples pasos sigue siendo legible

Las funcionalidades de producción a menudo requieren varias llamadas al modelo con diferentes instrucciones. Una forma común es generar algo a partir de un tema y luego procesar posteriormente ese resultado intermedio con una segunda instrucción:

Topic
  ↓
Prompt 1
  ↓
LLM
  ↓
Output Parser
  ↓
Prompt 2
  ↓
LLM
  ↓
Output Parser
  ↓
Final Output

Si se gestiona manualmente, este tipo de flujo se vuelve cada vez más complejo a medida que se añaden más pasos. Al expresarse como una cadena, las etapas permanecen en una sola declaración, y la regla de que cada etapa alimente a la siguiente es aplicada por el framework en lugar de por una disciplina interna.

Los componentes pueden intercambiarse

Dado que cada etapa es un componente independiente, se puede reemplazar uno sin afectar a los demás. Supongamos que un pipeline actualmente funciona con Gemini:

chain = prompt | gemini | parser

Al pasar a OpenAI, solo es necesario cambiar el componente del modelo:

chain = prompt | openai | parser

La solicitud y el analizador permanecen tal como están. Esto es útil cuando cambian los requisitos, cuando un equipo compara proveedores en cuanto a costo o calidad, o cuando se desea un modelo más económico en el desarrollo y uno más potente en producción.

Beneficios a primera vista

  • Menos código de enlace: los resultados fluyen automáticamente al siguiente paso.
  • Flujos de trabajo estructurados en múltiples pasos: varias solicitudes, modelos y analizadores coexisten en un único pipeline declarado.
  • Evolución más sencilla: los componentes pueden reutilizarse o reemplazarse, incluido el proveedor del modelo, sin tener que rediseñar el flujo.

Cuatro patrones de composición

Es útil agrupar los pipelines de LangChain en cuatro categorías. Estrictamente hablando, estas no son clases separadas en el LangChain moderno; son patrones que se construyen a partir de las mismas primitivas Runnable. Los nombres siguen siendo un vocabulario útil:

  • Cadena simple: una única línea recta desde el prompt hasta el analizador.
  • Cadena secuencial: varias líneas de este tipo unidas una tras otra.
  • Cadena paralela: varios sub-cadenas independientes que procesan el mismo input al mismo tiempo.
  • Cadena condicional: un punto de decisión que selecciona qué sub-cadena manejará el input.

Juntos abarcan desde un prompt único hasta flujos de trabajo que se expanden y toman decisiones. Cada uno se explica a continuación con un ejemplo funcional en dos proveedores.

Cadenas simples: un proceso directo

La tubería más simple conecta sus componentes en línea recta. La salida de cada componente es la entrada del siguiente, y no hay ramificaciones ni decisiones en el camino:

Input
  ↓
Prompt Template
  ↓
LLM
  ↓
Output Parser
  ↓
Output

El ejemplo en ejecución es una herramienta de estudio: dado un tema, el modelo debe generar unos pocos hechos fáciles de comprender sin adentrarse en detalles técnicos complejos. La cadena para ello es lo más corta posible:

prompt → llm → parser

En palabras simples, un valor de topic entra en el template del prompt, el prompt formateado se envía al modelo, y la respuesta del modelo pasa por un analizador de salida antes de ser devuelta. Técnicamente el analizador es opcional, pero incluirlo permite obtener un tipo de retorno predecible.

Concretamente, el ejemplo utiliza cricket como tema y solicita cinco hechos interesantes. Los datos se mueven de la siguiente manera:

{'topic': 'cricket'}
        ↓
PromptTemplate
        ↓
LLM
        ↓
StrOutputParser
        ↓
Five interesting facts about cricket

La versión Gemini

Aquí está el programa completo que utiliza Gemini de Google a través de la integración langchain_google_genai:

from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

load_dotenv()

llm = GoogleGenerativeAI(model="gemini-2.5-flash")

prompt = PromptTemplate(
    template='Generate 5 interesting facts about {topic}',
    input_variables=['topic']
)

parser = StrOutputParser()

chain = prompt | llm | parser

result = chain.invoke({'topic':'cricket'})
# print(result)

chain.get_graph().print_ascii()

Se crea un modelo GoogleGenerativeAI basado en gemini-2.5-flash, un prompt de una variable y un StrOutputParser; luego se combinan con la secuencia prompt | llm | parser. La última línea imprime un diagrama en ASCII de la cadena, el cual se explica más adelante. Tenga en cuenta que la línea print(result) está comentada en esta versión, por lo que el script solo muestra el grafo a menos que se vuelva a activar.

Análisis del código de Gemini

Las importaciones incluyen los cuatro componentes que necesita el script:

from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

GoogleGenerativeAI es el contenedor que rodea al modelo Gemini. PromptTemplate define un prompt con un slot {topic} que se llenará en tiempo de ejecución. StrOutputParser convierte la salida del modelo en una cadena de texto estándar de Python. load_dotenv lee un archivo local .env y lo convierte en variables de entorno.

Llamar a load_dotenv hace que la clave de API y cualquier otra configuración del archivo .env estén disponibles para el proceso antes de crear cualquier cliente:

load_dotenv()

La integración con Gemini obtiene su clave del entorno, por lo que esta llamada debe realizarse antes de instanciar el modelo. Es práctica habitual guardar las claves en .env (y fuera del control de versiones).

A continuación, se crea el propio modelo, especificando gemini-2.5-flash como versión:

llm = GoogleGenerativeAI(model="gemini-2.5-flash")

El plantilla de prompt declara tanto el texto como las variables que espera:

prompt = PromptTemplate(
    template='Generate 5 interesting facts about {topic}',
    input_variables=['topic']
)

En el momento de la llamada, el marcador de posición {topic} se rellena con los datos del diccionario de entrada. Al pasar esta información:

{'topic': 'cricket'}

se genera el siguiente prompt concreto:

Generate 5 interesting facts about cricket

El analizador se crea sin argumentos:

parser = StrOutputParser()

Su función es devolver la respuesta del modelo como cadena de texto. Con un modelo de tipo completado como GoogleGenerativeAI, que ya devuelve texto, su función es casi idéntica a una transmisión directa. Con un modelo de chat, que devuelve un objeto de mensaje, extrae su contenido textual. En cualquier caso, el siguiente consumidor recibe una simple str.

Luego viene la línea que define todo el proceso:

chain = prompt | llm | parser

El operador de tubería conecta los tres componentes de modo que el mensaje formateado se envíe al modelo y la respuesta del modelo se envíe al analizador:

PromptTemplate → Gemini 2.5 Flash → StrOutputParser

Finalmente, la cadena se ejecuta con un diccionario que contiene el tema:

result = chain.invoke({'topic':'cricket'})

Esa única llamada activa cada etapa en orden, sin variables intermedias en el código:

cricket
   ↓
PromptTemplate
   ↓
Gemini 2.5 Flash
   ↓
StrOutputParser
   ↓
Final Result

Qué se devuelve

El valor devuelto es una cadena que contiene cinco hechos sobre el críquet, algo similar a esto:

1. Cricket originated in England and has been played for several centuries.
2. A cricket team consists of 11 players.
3. Test cricket matches can last up to five days.
4. The Cricket World Cup is one of the major international cricket tournaments.
5. Cricket is especially popular in countries such as India, Australia, England, and New Zeakand.

Dado que el texto se genera, la redacción y los hechos específicos varían de una ejecución a otra, e incluso la salida de ejemplo contiene un error tipográfico (“Zeakand”). Considere las salidas de ejemplo como ilustraciones de la estructura, no como resultados fijos. Si necesita un formato determinista, esa es la función de un analizador más estricto o de una salida estructurada, no de esperar que el modelo se repita.

El mismo pipeline en OpenAI

Ahora el mismo asistente de estudio, ejecutándose en OpenAI. Lo notable es lo poco que ha cambiado:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
prompt = PromptTemplate(
    template='Generate 5 interesting facts about {topic}',
    input_variables=['topic']
)
parser = StrOutputParser()
chain = prompt | llm | parser
result = chain.invoke({'topic':'cricket'})
print(result)
chain.get_graph().print_ascii()

El modelo ahora es ChatOpenAI con gpt-4o-mini. El prompt, el analizador y la composición son exactamente los mismos que antes, y esta versión imprime tanto el resultado como el gráfico.

La única novedad es el objeto del modelo:

llm = ChatOpenAI(model="gpt-4o-mini")

ChatOpenAI de langchain_openai sustituye a GoogleGenerativeAI. Es un modelo de chat, por lo que devuelve un objeto de mensaje en lugar de una cadena raw, y esa es precisamente la razón por la cual vale la pena mantener el analizador en la cadena.

El template del prompt no ha cambiado:

prompt = PromptTemplate(
    template='Generate 5 interesting facts about {topic}',
    input_variables=['topic']
)

Tampoco lo ha hecho el analizador:

parser = StrOutputParser()

Y lo mismo ocurre con la composición:

chain = prompt | llm | parser

La estructura es la misma línea de tres etapas, con OpenAI en el medio:

PromptTemplate → OpenAI → StrOutputParser

La invocación de la cadena y la impresión del resultado se ven así:

result = chain.invoke({'topic':'cricket'})
print(result)

La ejecución en OpenAI también devuelve una cadena con cinco hechos sobre el críquet, por ejemplo:

1. Cricket is believed to have originated in England.
2. Each cricket team has 11 players.
3. A Test match can be played over a maximum of five days.
4. The Cricket World Cup is one of the most important international cricket tournaments.
5. Cricket has a particularly large following in India and several other countries.

Nuevamente, la redacción exacta variará entre ejecuciones y entre modelos.

Impresión de la estructura de la cadena

Ambos scripts terminan con una llamada que muestra el pipeline como un diagrama de texto:

chain.get_graph().print_ascii()

get_graph() devuelve una representación gráfica del Runnable, y print_ascii() la dibuja en la terminal. Para la cadena Gemini se obtiene algo similar a esto:

PromptInput
     *
     *
PromptTemplate
     *
     *
GoogleGenerativeAI
     *
     *
StrOutputParser
     *
     *
StrOutputParserOutput

Los nodos PromptInput y StrOutputParserOutput son los puntos de entrada y salida derivados del esquema; los tres nodos intermedios corresponden a sus componentes. Se trata de una verificación rápida para asegurarse de que la cadena esté conectada como se espera. En la mayoría de los entornos, print_ascii() depende del paquete opcional grandalf; por lo tanto, instálelo si la llamada genera un error de importación.

Solo cambió el modelo

Si coloca las dos cadenas de procesamiento una al lado de la otra, la diferencia radica en un único nodo. Con Gemini:

Input
  ↓
PromptTemplate
  ↓
GoogleGenerativeAI
  ↓
StrOutputParser
  ↓
Output

Con OpenAI:

Input
  ↓
PromptTemplate
  ↓
ChatOpenAI
  ↓
StrOutputParser
  ↓
Output

En el código, toda la diferencia se reduce a qué objeto de modelo se construye:

# Google Gemini
llm = GoogleGenerativeAI(model="gemini-2.5-flash")

# OpenAI
llm = ChatOpenAI(model="gpt-4o-mini")

Todo lo que lo rodea puede permanecer igual. Ese es el beneficio práctico de construir sobre componentes estandarizados: la elección del proveedor se convierte en una decisión de configuración en lugar de una reescritura. En la práctica todavía existen pequeñas diferencias que vale la pena observar, como los modelos de chat frente a los de completado, los nombres de los parámetros y la forma en que cada proveedor maneja las instrucciones del sistema, pero el propio pipeline es portable.

La ruta completa de los datos

En términos generales, una cadena simple es esta:

Input
  ↓
PromptTemplate
  ↓
LLM
  ↓
StrOutputParser
  ↓
Output

Al rastrear con la entrada de cricket, cada valor intermedio se vuelve visible, desde el diccionario hasta el prompt renderizado, la respuesta bruta del modelo y la cadena final:

{'topic': 'cricket'}
        ↓
PromptTemplate
        ↓
"Generate 5 interesting facts about cricket"
        ↓
LLM
        ↓
Raw LLM Response
        ↓
StrOutputParser
        ↓
Final String Output

Esa es toda la idea. Cada componente hace una cosa, y su resultado se convierte en la entrada del siguiente componente, en un único proceso continuo.

Cadenas secuenciales: pasar una llamada de modelo a la siguiente

Una cadena secuencial extiende la misma idea a través de varias etapas. Una cadena simple suele contener una sola secuencia de este tipo:

Prompt → LLM → Output Parser

Una cadena secuencial puede contener varios modelos de prompts, varias llamadas al modelo y varios analizadores, todos unidos entre sí. La regla que rige no cambia: lo que produce la etapa actual es lo que consume la siguiente. Todavía no hay ramificaciones; las etapas siempre se ejecutan en el mismo orden fijo. Una versión de dos etapas se ve así:

Input
  ↓
Prompt Template 1
  ↓
LLM 1
  ↓
Output Parser
  ↓
Prompt Template 2
  ↓
LLM 2
  ↓
Output Parser
  ↓
Output

Ejemplo: un informe detallado, luego un resumen

La tarea de ejemplo tiene dos fases. Primero, generar un informe detallado sobre un tema. Segundo, condensar ese informe en un resumen de cinco puntos. La segunda fase nunca ve directamente el tema original; solo trabaja a partir del informe producido en la primera fase:

Input
  ↓
Explanation Prompt
  ↓
LLM
  ↓
StrOutputParser
  ↓
Detailed Report
  ↓
Summary Prompt
  ↓
LLM
  ↓
StrOutputParser
  ↓
5-Point Summary

Dos plantillas de prompt se encargan de la tarea. explaination_prompt (la ortografía incorrecta está en el código, así que consérvela si copias los fragmentos) solicita un informe detallado. summary_prompt toma ese informe como entrada y pide cinco puntos de resumen.

La versión Gemini

Aquí está el programa completo en Gemini:

from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

from dotenv import load_dotenv
load_dotenv()
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
# Prompt 1 : Explanation
explaination_prompt = PromptTemplate(
    template='Generate a detailed report on {topic}',
    input_variables=['topic']
)
# Prompt 2 : Summary
summary_prompt = PromptTemplate(
    template='Generate a 5 pointer summary from the following text \n {text}',
    input_variables=['text']
)
parser = StrOutputParser()
chain = explaination_prompt | llm | parser | summary_prompt | llm | parser
result = chain.invoke({"topic": "Generative AI"})
# print(result)
chain.get_graph().print_ascii()

Una instancia de GoogleGenerativeAI sirve para ambas etapas, y el flujo de trabajo es una sola expresión con seis componentes: dos prompts, el modelo dos veces y el analizador dos veces.

Análisis del código de Gemini

Las importaciones coinciden con la cadena simple:

from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

GoogleGenerativeAI para el modelo, PromptTemplate para los dos prompts reutilizables, StrOutputParser para convertir cada respuesta en una cadena, y load_dotenv para la configuración.

Las variables de entorno se cargan primero, exactamente como antes:

load_dotenv()

El modelo se crea una sola vez:

llm = GoogleGenerativeAI(model="gemini-2.5-flash")

Reutilizar un único objeto de modelo para ambas etapas está perfectamente bien. Los envoltorios de modelos en LangChain no tienen estado con respecto a las llamadas individuales, por lo que la misma instancia puede referenciarse cualquier número de veces en una cadena.

El primer prompt acepta un topic y solicita un informe detallado:

explaination_prompt = PromptTemplate(
    template='Generate a detailed report on {topic}',
    input_variables=['topic']
)

Dada esta entrada:

{"topic": "Generative AI"}

se muestra como:

Generate a detailed report on Generative AI

El segundo prompt es donde tiene lugar el intercambio interesante:

summary_prompt = PromptTemplate(
    template='Generate a 5 pointer summary from the following text \n {text}',
    input_variables=['text']
)

Exige una variable llamada text, pero nada en el diccionario de entrada se denomina text. El valor proviene de la primera etapa: el informe generado por la primera llamada al modelo es lo que llena ese espacio.

Eso funciona gracias al analizador que se encuentra entre las etapas:

parser = StrOutputParser()

Después de la primera llamada al modelo, el analizador genera una cadena simple. Cuando un PromptTemplate con exactamente una variable de entrada recibe una cadena sin formato en lugar de un diccionario, LCEL asigna esa cadena a dicha variable. Por lo tanto, el informe queda como {text} sin ninguna asignación explícita. Si el segundo prompt tuviera dos variables, sería necesario crear el diccionario uno mismo, por ejemplo con un pequeño diccionario de Runnables o mediante RunnablePassthrough.assign.

Toda la secuencia se ejecuta en una sola línea:

chain = explaination_prompt | llm | parser | summary_prompt | llm | parser

Al leerla de izquierda a derecha se obtiene el orden de ejecución:

Explanation Prompt
        ↓
      Gemini
        ↓
 StrOutputParser
        ↓
  Summary Prompt
        ↓
      Gemini
        ↓
 StrOutputParser
        ↓
     Final Output

Cada componente recibe la salida del componente anterior. Nada en su código modifica el informe intermedio.

Para ejecutarlo se necesita un único tema:

result = chain.invoke({"topic": "Generative AI"})

Detrás de esa llamada, ambas etapas se ejecutan una tras otra:

"Generative AI"
       ↓
Explanation Prompt
       ↓
Gemini
       ↓
StrOutputParser
       ↓
Detailed Report
       ↓
Summary Prompt
       ↓
Gemini
       ↓
StrOutputParser
       ↓
Final 5-Point Summary

Qué se devuelve

El valor final es un resumen de cinco puntos sobre la IA generativa, por ejemplo:

1. Generative AI is a branch of artificial intelligence that creates new content.
2. It can generate text, images, audio, video, and code.
3. Foundation models are an important part of modern Generative AI systems.
4. Generative AI is used across areas such as education, software development, and customer support.
5. It is enabling applications that can automate and simplify many creative and knowledge-based tasks.

Lo importante de notar es que este resumen no se elabora directamente a partir del tema. Gemini primero genera un informe completo, y solo ese informe se envía al prompt de resumen. Este enfoque en dos pasos tiende a proporcionar material más rico para comprimir en la segunda llamada, a costa de una llamada adicional al modelo y de la latencia y los tokens que esto conlleva. Como siempre, las palabras exactas varían de una ejecución a otra.

El mismo proceso en OpenAI

La variante de OpenAI mantiene el flujo de trabajo intacto:

from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI()
explaination_prompt = PromptTemplate(
    template="Generate a detailed report on {topic}",
    input_variables=["topic"]
)
summary_prompt = PromptTemplate(
    template="Generate a 5 pointer summary from the following text \n {text}",
    input_variables=["text"],
)
parser = StrOutputParser()
chain = explaination_prompt | llm | parser | summary_prompt | llm | parser
result = chain.invoke({"topic": "Generative AI"})
print(result)
chain.get_graph().print_ascii()

Se trata de la misma estructura en dos etapas: prompt para el informe, modelo, analizador; prompt de resumen, modelo, analizador. El script también importa load_dotenv dos veces, lo cual no es perjudicial pero se puede optimizar.

Esta vez el modelo se construye sin argumentos:

llm = ChatOpenAI()

Al no haber un argumento model, ChatOpenAI recurre al modelo predeterminado de la biblioteca, el cual ha cambiado en las diferentes versiones de langchain_openai. En proyectos reales, especifique explícitamente el nombre del modelo para que una actualización no modifique silenciosamente su comportamiento o costos.

El prompt para el informe es idéntico al de la versión Gemini:

explaination_prompt = PromptTemplate(
    template="Generate a detailed report on {topic}",
    input_variables=["topic"]
)

Lo mismo ocurre con el prompt de resumen, que nuevamente espera que el informe esté en {text}:

summary_prompt = PromptTemplate(
    template="Generate a 5 pointer summary from the following text \n {text}",
    input_variables=["text"],
)

Aquí es donde el patrón secuencial resulta útil: el segundo prompt se completa automáticamente a partir de la salida de la primera etapa.

El analizador es el mismo StrOutputParser:

parser = StrOutputParser()

Y la composición es, carácter por carácter, idéntica:

chain = explaination_prompt | llm | parser | summary_prompt | llm | parser

En términos de flujo, se lee así:

Prompt 1
   ↓
OpenAI
   ↓
StrOutputParser
   ↓
Prompt 2
   ↓
OpenAI
   ↓
StrOutputParser
   ↓
Final Output

Solo el nodo del modelo difiere de la versión Gemini.

La invocación pasa el tema una vez más. Tenga en cuenta que el fragmento mostrado carece de su paréntesis de cierre; el programa completo incluye la línea correcta:

result = chain.invoke({"topic": "Generative AI"}

Luego, la cadena ejecuta ambas etapas por separado.

Qué se devuelve

El resultado es nuevamente un resumen de cinco puntos derivado del informe intermedio:

1. Generative AI creates new content by learning patterns from existing data.
2. It is powered by large foundation models trained on diverse datasets.
3. Generative AI can produce text, images, audio, video, and code.
4. It is being used in many industries and applications.
5. It is becoming an important part of modern AI-powered software.

Los detalles variarán entre ejecuciones.

Imprimir la estructura de la cadena

La herramienta de gráficos también funciona con cadenas más largas:

chain.get_graph().print_ascii()

En el caso de la cadena secuencial Gemini, el diagrama muestra ambas etapas apiladas verticalmente:

PromptInput
     *
     *
PromptTemplate
     *
     *
GoogleGenerativeAI
     *
     *
StrOutputParser
     *
     *
PromptTemplate
     *
     *
GoogleGenerativeAI
     *
     *
StrOutputParser
     *
     *
StrOutputParserOutput

El nodo del modelo aparece dos veces porque se utiliza el mismo objeto de modelo en dos posiciones dentro del pipeline, y el grafo refleja las posiciones, no los objetos únicos.

Comparación de las dos implementaciones

Ambas versiones siguen la misma estructura secuencial; el proveedor es la única diferencia real. El pipeline de Gemini:

Input
  ↓
Explanation Prompt
  ↓
GoogleGenerativeAI
  ↓
StrOutputParser
  ↓
Summary Prompt
  ↓
GoogleGenerativeAI
  ↓
StrOutputParser
  ↓
Output

El pipeline de OpenAI:

Input
  ↓
Explanation Prompt
  ↓
ChatOpenAI
  ↓
StrOutputParser
  ↓
Summary Prompt
  ↓
ChatOpenAI
  ↓
StrOutputParser
  ↓
Output

Los prompts, el analizador y la disposición de la cadena se comparten. La construcción del modelo es la única línea que cambia:

# Google Gemini
llm = GoogleGenerativeAI(model="gemini-2.5-flash")

# OpenAI
llm = ChatOpenAI()

La cadena compuesta en sí permanece idéntica entre los diferentes proveedores:

chain = explaination_prompt | llm | parser | summary_prompt | llm | parser

De forma abstracta, una cadena secuencial se ve así:

Prompt 1
   ↓
LLM 1
   ↓
Parser
   ↓
Prompt 2
   ↓
LLM 2
   ↓
Parser
   ↓
Output

La diferencia con una cadena simple es que existen múltiples etapas, y cada etapa posterior depende de la anterior.

El camino completo de los datos

En términos generales, la cadena secuencial mueve los datos de esta manera:

Input
  ↓
PromptTemplate 1
  ↓
LLM 1
  ↓
StrOutputParser
  ↓
Detailed Report
  ↓
PromptTemplate 2
  ↓
LLM 2
  ↓
StrOutputParser
  ↓
Final Output

Se sigue con la entrada de la IA generativa, incluidos los prompts procesados:

{"topic": "Generative AI"}
            ↓
    Explanation Prompt
            ↓
      "Generate a detailed
       report on Generative AI"
            ↓
           LLM
            ↓
    StrOutputParser
            ↓
      Detailed Report
            ↓
      Summary Prompt
            ↓
    "Generate a 5 pointer
     summary from the following text"
            ↓
           LLM
            ↓
    StrOutputParser
            ↓
     5-Point Summary

La primera llamada al modelo es un paso intermedio y no la respuesta final. Su salida se convierte en materia prima para la siguiente operación. Puedes seguir acumulando etapas cuando un flujo de trabajo las necesite en mayor cantidad:

Prompt
  ↓
LLM
  ↓
Parser
  ↓
Prompt
  ↓
LLM
  ↓
Parser
  ↓
Prompt
  ↓
LLM
  ↓
Output

Por más largo que sea el pipeline, el orden permanece fijo y cada etapa espera a la anterior. Eso también significa que la latencia se acumula: tres llamadas al modelo en secuencia cuestan aproximadamente la suma de sus tiempos de respuesta. También es conveniente añadir una protección contra salidas intermedias excesivas, ya que un informe muy largo se integra directamente en el siguiente prompt e incrementa su cantidad de tokens.

Cadenas paralelas: distribución sobre la misma entrada

Una cadena paralela ejecuta varias sub-cadenas uno al lado del otro. Mientras que una cadena secuencial espera a que cada etapa termine antes de comenzar la siguiente, una cadena paralela inicia las sub-cadenas independientes al mismo tiempo:

                    ┌──→ Sub-Chain 1 ──→ Output 1
                    │
Input ──────────────┼──→ Sub-Chain 2 ──→ Output 2
                    │
                    └──→ Sub-Chain 3 ──→ Output 3
                                      ↓
                              Combined Output

Dado que las sub-cadenas son independientes, ninguna de ellas ve la salida de la otra. La mecánica es sencilla:

  1. Cada sub-cadena recibe la misma entrada y genera su propia salida.
  2. Todas las sub-cadenas se inician de forma concurrente.
  3. La etapa paralela espera a que todas las sub-cadenas hayan terminado.
  4. Sus resultados se combinan en un único valor.

Ese valor combinado es un diccionario. Cada clave es el nombre que le asignaste a una sub-cadena, y cada valor es la salida de dicha sub-cadena:

{
    "facts": "...",
    "definition": "...",
    "use_case": "..."
}

Ejemplo: tres perspectivas de un mismo tema

Al dar un tema como Inteligencia Artificial, el objetivo es generar tres elementos a partir de la misma entrada: cinco hechos interesantes, una definición concisa y un caso de uso en el mundo real. Ninguno de estos depende de los demás, por lo que ejecutarlos uno tras otro solo sería una pérdida de tiempo. En su lugar, se ejecutan en paralelo:

                         Input
                           ↓
                    RunnableParallel
                     /      |      \
                    /       |       \
                   ↓        ↓        ↓
               Facts    Definition  Use Case
                 ↓         ↓          ↓
                LLM       LLM        LLM
                 ↓         ↓          ↓
               Parser    Parser     Parser
                    \       |       /
                     \      |      /
                      ↓     ↓     ↓
                     Combined Result

Tres subprocesos realizan el trabajo: facts para los cinco hechos, definition para la definición breve y use_case para la aplicación práctica. Se combinan mediante RunnableParallel. La primera implementación utiliza Gemini para los tres casos; la segunda mezcla OpenAI y Anthropic e incluye un paso de fusión.

Por qué ejecutar subprocesos en paralelo

Existen dos razones para adoptar este patrón.

El primero es la velocidad. Las llamadas a los modelos están determinadas por el tiempo de red y de generación, por lo que las llamadas independientes se benefician enormemente de la concurrencia. Imagine tres subprocesos que cada uno tarda unos dos segundos. Si se ejecutan uno tras otro, su tiempo total se suma:

Chain 1 → 2 seconds
Chain 2 → 2 seconds
Chain 3 → 2 seconds

Total ≈ 6 seconds

Si se ejecutan de forma concurrente, el tiempo total es aproximadamente el de el más lento:

Chain 1 ─┐
Chain 2 ─┼→ execute together → ≈ 2 seconds
Chain 3 ─┘

El ahorro real depende de los límites de velocidad del proveedor y de cuán uniformemente finalizan las llamadas, pero en el caso de tareas independientes la mejora suele ser considerable.

La segunda razón es estructural: las tareas comparten una misma entrada, pero no necesitan los resultados de las demás. Por ejemplo, a partir de un único documento podría quererse un resumen, una lista de temas clave y un conjunto de preguntas para estudiar. Cada tarea puede tomar el documento directamente:

                    Document
                       ↓
               ┌───────┼───────┐
               ↓       ↓       ↓
            Summary  Topics  Questions

El resumen no necesita integrarse con la extracción de temas ni con el generador de preguntas. Cuando las tareas están relacionadas de esta manera, forzarlas en una secuencia aumenta la latencia y vincula pasos que no tienen una dependencia real. Cuando sí dependen entre sí, el paralelismo es la herramienta inadecuada, y una cadena secuencial es la representación adecuada.

La versión Gemini

Aquí se muestra la distribución en tres ramas en Gemini:

from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel

from dotenv import load_dotenv
load_dotenv()

llm = GoogleGenerativeAI(model="gemini-2.5-flash")
parser = StrOutputParser()

# Prompt 1: Facts
fact_prompt = PromptTemplate(
    input_variables=["topic"],
    template="Give five interesting short facts about: {topic}"
)

# Prompt 2: Definition
definition_prompt = PromptTemplate(
    input_variables=["topic"],
    template="Give a concise definition of: {topic}"
)

# Prompt 3: Use Case
usecase_prompt = PromptTemplate(
    input_variables=["topic"],
    template="What is a real-world use case of: {topic}?"
)

# Run all chains in parallel
parallel_chain = RunnableParallel({
    "facts": fact_prompt | llm | parser,
    "definition": definition_prompt | llm | parser,
    "use_case": usecase_prompt | llm | parser
})

# Invoke parallel chain
result = parallel_chain.invoke({"topic": "Artificial Intelligence"})

# print(type(result))
# print(result)
# print("\n📌 Facts:\n", result["facts"])
# print("\n📘 Definition:\n", result["definition"])
# print("\n🛠️ Use Case:\n", result["use_case"])

parallel_chain.get_graph().print_ascii()

Tres plantillas de prompt, un modelo y un analizador se combinan en tres cadenas pequeñas, las cuales RunnableParallel ejecuta simultáneamente con la entrada Inteligencia Artificial. Varias líneas print están comentadas; activarlas muestra el tipo de resultado y cada campo.

Análisis del código de Gemini

Las importaciones añaden un nuevo componente:

from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel

Además del envoltorio del modelo, PromptTemplate y StrOutputParser, el script importa RunnableParallel de langchain_core.runnables, que es la parte encargada de ejecutar las sub-cadenas de forma concurrente.

Se crea un único modelo Gemini 2.5 Flash:

llm = GoogleGenerativeAI(model="gemini-2.5-flash")

Todas las tres sub-cadenas lo comparten. Compartir un único objeto de modelo entre ramas paralelas es seguro; cada llamada es independiente.

También se comparte un único analizador:

parser = StrOutputParser()

La primera petición solicita cinco hechos cortos:

fact_prompt = PromptTemplate(
    input_variables=["topic"],
    template="Give five interesting short facts about: {topic}"
)

La segunda solicita una definición concisa:

definition_prompt = PromptTemplate(
    input_variables=["topic"],
    template="Give a concise definition of: {topic}"
)

La tercera solicita una aplicación en el mundo real:

usecase_prompt = PromptTemplate(
    input_variables=["topic"],
    template="What is a real-world use case of: {topic}?"
)

Todas las tres peticiones utilizan el mismo nombre de variable:

{topic}

Ese nombre compartido es lo que permite que un único diccionario de entrada alimente cada rama. Las tareas en sí permanecen completamente separadas.

Ahora, la pieza central: la cadena paralela:

parallel_chain = RunnableParallel({
    "facts": fact_prompt | llm | parser,
    "definition": definition_prompt | llm | parser,
    "use_case": usecase_prompt | llm | parser
})

RunnableParallel recibe un diccionario cuyos valores son objetos Runnable. Las claves se convierten en las claves de la salida:

facts
definition
use_case

Cada valor es una pequeña cadena completa por sí mismo:

fact_prompt → LLM → parser

definition_prompt → LLM → parser

usecase_prompt → LLM → parser

Dado que ninguno de ellos depende del otro, todos pueden ejecutarse al mismo tiempo. Con un invoke síncrono, LangChain ejecuta las ramas en un pool de hilos; con ainvoke, se ejecutan como tareas asíncronas concurrentes.

La cadena se invoca una sola vez:

result = parallel_chain.invoke({"topic": "Artificial Intelligence"})

con una única entrada:

{"topic": "Artificial Intelligence"}

Ese diccionario se pasa a cada rama. Conceptualmente:

Artificial Intelligence
          ↓
    RunnableParallel
     /      |       \
    ↓       ↓        ↓
  Facts  Definition  Use Case
    ↓       ↓        ↓
   LLM     LLM      LLM
    ↓       ↓        ↓
 Parser   Parser   Parser
     \      |       /
      \     |      /
       Combined Result

Qué devuelve

El resultado es un diccionario con una entrada por cada sub-cadena:

{
    "facts": "1. Artificial Intelligence enables machines to perform tasks that normally require human intelligence.\n2. ...",
    "definition": "Artificial Intelligence is the field of creating systems that can perform tasks requiring human-like intelligence.",
    "use_case": "A real-world use case of AI is customer support chatbots that can understand and respond to user queries."
}

El texto dentro de cada campo variará en cada ejecución, pero su estructura permanece estable, y eso es de lo que depende tu código:

result["facts"]
result["definition"]
result["use_case"]

Cada clave devuelve el resultado de la rama correspondiente, de modo que el código posterior puede obtener exactamente la información que necesita.

Mezclar proveedores y fusionar los resultados

La segunda implementación muestra dos ideas adicionales: diferentes ramas pueden utilizar proveedores distintos, y un paso paralelo puede ser a su vez una etapa de un pipeline secuencial más amplio. En este caso, una rama utiliza OpenAI, la otra utiliza Anthropic, y una llamada final a OpenAI fusiona ambos resultados en un único documento:

from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.schema.runnable import RunnableParallel

load_dotenv()

model1 = ChatOpenAI()

model2 = ChatAnthropic(model_name='claude-3-7-sonnet-20250219')

prompt1 = PromptTemplate(
    template='Generate short and simple notes from the following text \n {text}',
    input_variables=['text']
)

prompt2 = PromptTemplate(
    template='Generate 5 short question answers from the following text \n {text}',
    input_variables=['text']
)

prompt3 = PromptTemplate(
    template='Merge the provided notes and quiz into a single document \n notes -> {notes} and quiz -> {quiz}',
    input_variables=['notes', 'quiz']
)

parser = StrOutputParser()

parallel_chain = RunnableParallel({
    'notes': prompt1 | model1 | parser,
    'quiz': prompt2 | model2 | parser
})

merge_chain = prompt3 | model1 | parser

chain = parallel_chain | merge_chain

text = """
Support vector machines (SVMs) are a set of supervised learning methods used for classification, regression and outliers detection.

The advantages of support vector machines are:

Effective in high dimensional spaces.

Still effective in cases where number of dimensions is greater than the number of samples.

Uses a subset of training points in the decision function (called support vectors), so it is also memory efficient.

Versatile: different Kernel functions can be specified for the decision function. Common kernels are provided, but it is also possible to specify custom kernels.

The disadvantages of support vector machines include:

If the number of features is much greater than the number of samples, avoid over-fitting in choosing Kernel functions and regularization term is crucial.

SVMs do not directly provide probability estimates, these are calculated using an expensive five-fold cross-validation (see Scores and probabilities, below).

The support vector machines in scikit-learn support both dense (numpy.ndarray and convertible to that by numpy.asarray) and sparse (any scipy.sparse) sample vectors as input. However, to use an SVM to make predictions for sparse data, it must have been fit on such data. For optimal performance, use C-ordered numpy.ndarray (dense) or scipy.sparse.csr_matrix (sparse) with dtype=float64.
"""

result = chain.invoke({'text': text})

print(result)

chain.get_graph().print_ascii()

La entrada es un texto sobre máquinas de vectores de soporte. Una rama extrae de él notas de estudio, otra crea un cuestionario de cinco preguntas, y la cadena de fusión combina las notas y el cuestionario en un único documento.

Dos detalles de esta descripción requieren una aclaración. Importa RunnableParallel desde langchain.schema.runnable, una ruta antigua anterior a la división de langchain_core; las versiones actuales lo exponen desde langchain_core.runnables, como en el ejemplo de Gemini, y la ruta antigua podría no existir en versiones más recientes. También se especifica claude-3-7-sonnet-20250219, un identificador de modelo obsoleto; los proveedores retiran gradualmente las versiones antiguas, por lo que es necesario verificar la lista actual de modelos de Anthropic antes de reutilizarlo.

Se crean dos objetos de modelo:

model1 = ChatOpenAI()

model2 = ChatAnthropic(model_name='claude-3-7-sonnet-20250219')

ChatOpenAI escribe las notas y posteriormente realiza la fusión. ChatAnthropic elabora el cuestionario. En RunnableParallel no hay requisito de que cada rama utilice el mismo modelo, lo cual es útil cuando un proveedor es mejor o más económico para una tarea específica.

El prompt de notas convierte el texto de entrada en notas cortas y sencillas:

prompt1 = PromptTemplate(
    template='Generate short and simple notes from the following text \n {text}',
    input_variables=['text']
)

El prompt del cuestionario genera cinco pares cortos de preguntas y respuestas a partir del mismo texto:

prompt2 = PromptTemplate(
    template='Generate 5 short question answers from the following text \n {text}',
    input_variables=['text']
)

Ninguna de las tareas necesita la salida de la otra, por lo que son ramas paralelas naturales.

El tercer prompt es diferente. Se ejecuta después de la división en ramas y necesita ambos resultados:

prompt3 = PromptTemplate(
    template='Merge the provided notes and quiz into a single document \n notes -> {notes} and quiz -> {quiz}',
    input_variables=['notes', 'quiz']
)

Declara dos variables de entrada:

notes
quiz

Esos nombres no son arbitrarios. Deben coincidir con las claves del paso paralelo, ya que el diccionario generado por la expansión es exactamente lo que recibe esta instrucción.

El paso paralelo define las dos ramas:

parallel_chain = RunnableParallel({
    'notes': prompt1 | model1 | parser,
    'quiz': prompt2 | model2 | parser
})

Su estructura es una división bidireccional que se une en un diccionario:

                Input Text
                    ↓
             RunnableParallel
                /        \
               ↓          ↓
          Notes Chain   Quiz Chain
               ↓          ↓
          OpenAI        Anthropic
               ↓          ↓
            Parser      Parser
                \        /
                 ↓      ↓
                Dictionary

Conceptualmente, el valor intermedio se ve así:

{
    "notes": "...",
    "quiz": "..."
}

La cadena de fusión es una cadena simple regular:

merge_chain = prompt3 | model1 | parser

Consume el diccionario del paso paralelo, llena {notes} y {quiz}, y pide a OpenAI que compile un documento.

Finalmente, las dos etapas se unen con otro tubo:

chain = parallel_chain | merge_chain

El resultado es un flujo de trabajo híbrido: paralelo al inicio, secuencial al final:

                    Input Text
                        ↓
                 RunnableParallel
                  /            \
                 ↓              ↓
              Notes            Quiz
                 ↓              ↓
              OpenAI         Anthropic
                 ↓              ↓
              Parser          Parser
                  \            /
                   ↓          ↓
                 Combined Dictionary
                        ↓
                  Merge Prompt
                        ↓
                      OpenAI
                        ↓
                      Parser
                        ↓
                  Final Output

El detalle comportamental clave es que las dos ramas se ejecutan de forma independiente, pero la fase de fusión no puede comenzar hasta que ambas hayan finalizado. Por lo tanto, la rama más lenta determina el ritmo de todo el proceso en forma de abanico. Si una rama falla, el paso paralelo se interrumpe y la fusión nunca tiene lugar; así que rodee a los proveedores inestables con intentos repetidos o soluciones alternativas cuando esto sea importante.

Todo el pipeline se invoca con el siguiente texto:

result = chain.invoke({'text': text})

Las ramas dedicadas a las notas y al cuestionario procesan el texto de forma concurrente. Cuando ambas terminan, sus resultados se integran en la cadena de fusión, y su resultado final en forma de cadena es lo que devuelve invoke.

Imprimir la estructura de la cadena

La herramienta gráfica también permite visualizar el proceso en forma de abanico:

parallel_chain.get_graph().print_ascii()

En la cadena Gemini de tres ramas, el diagrama muestra cómo se divide y luego se une el flujo de datos:

                 Parallel<...>Input
                   /      |      \
                  /       |       \
          PromptTemplate PromptTemplate PromptTemplate
               ↓              ↓              ↓
              LLM            LLM            LLM
               ↓              ↓              ↓
          StrOutputParser StrOutputParser StrOutputParser
                  \          |          /
                   \         |         /
                    Parallel<...>Output

Los nodos Parallel<...>Input y Parallel<...>Output indican dónde se distribuye la entrada y dónde se recogen los resultados en un diccionario.

Comparación de las dos implementaciones

Ambas versiones demuestran la misma idea: sub-cadenas independientes que operan al mismo tiempo. La versión Gemini es un diseño puramente de tipo fan-out con tres ramas en un único modelo:

Input
  ↓
RunnableParallel
  ├── Facts → Gemini → Parser
  ├── Definition → Gemini → Parser
  └── Use Case → Gemini → Parser
              ↓
       Dictionary Output

La versión con proveedores mixtos se divide en dos ramas que utilizan proveedores diferentes y luego añade una etapa de fusión:

Input
  ↓
RunnableParallel
  ├── Notes → OpenAI → Parser
  └── Quiz → Anthropic → Parser
              ↓
       Dictionary Output
              ↓
         Merge Chain
              ↓
            OpenAI
              ↓
            Output

Debajo de ambas existe la misma estructura subyacente:

RunnableParallel({
    "chain_1": chain_1,
    "chain_2": chain_2,
    "chain_3": chain_3
})

Los nombres de las claves designan las sub-cadenas; los valores son las propias sub-cadenas. Como atajo, cuando aparece un diccionario simple de Runnables dentro de una expresión de tubería, LCEL lo convierte automáticamente en RunnableParallel, por lo que a menudo verá el diccionario escrito sin la clase explícita.

La ruta de datos completa

En su forma general, una cadena paralela distribuye una entrada y recopila un diccionario:

                         Input
                           ↓
                    RunnableParallel
                    /       |       \
                   ↓        ↓        ↓
               Sub-chain Sub-chain Sub-chain
                   ↓        ↓        ↓
                 Output   Output   Output
                    \       |       /
                     \      |      /
                      ↓     ↓     ↓
                        Dictionary

En el ejemplo de Inteligencia Artificial en Gemini:

{"topic": "Artificial Intelligence"}
                ↓
        RunnableParallel
          /      |      \
         ↓       ↓       ↓
      Facts  Definition  Use Case
         ↓       ↓       ↓
       Gemini  Gemini   Gemini
         ↓       ↓       ↓
      Parser  Parser   Parser
          \      |      /
           \     |     /
             Dictionary

El valor final tiene una clave por cada ramificación:

{
    "facts": "...",
    "definition": "...",
    "use_case": "..."
}

Ese es el contraste esencial con una cadena secuencial. En una cadena secuencial, cada etapa consume la salida de la etapa anterior:

A → B → C → D

En una cadena paralela, las etapas independientes se ejecutan simultáneamente con la misma entrada y sus resultados se fusionan:

       ┌→ A ─┐
Input ─┼→ B ─┼→ Combined Output
       └→ C ─┘

Utilice el paralelismo para tareas que comparten entradas pero no resultados, y mantenga la composición secuencial para tareas que realmente se basan en pasos anteriores. Mezclar ambos, como lo hace el ejemplo de notas y cuestionario, es común y perfectamente idiomático.

Cadenas condicionales: enrutamiento de la entrada a la subcadena adecuada

Una cadena condicional elige su siguiente paso en tiempo de ejecución. Una cadena secuencial siempre ejecuta cada etapa en un orden fijo, mientras que una cadena paralela ejecuta todas las ramas al mismo tiempo. Una cadena condicional evalúa una condición en la entrada y ejecuta solo la rama que coincide:

Input
  ↓
Condition Check
  ↓
 ┌───────────────┐
 │               │
Yes             No
 ↓               ↓
Chain 1        Chain 2
 ↓               ↓
Output 1       Output 2
  \             /
   \           /
    Final Output

En LangChain esto se expresa con RunnableBranch. Se le proporciona una lista de pares, donde cada par combina una condición con un Runnable, además de un Runnable por defecto al final. Las condiciones se prueban en orden; la primera que devuelva True determina qué Runnable se ejecutará, y si ninguno coincide, el por defecto se encarga de procesar la entrada.

Ejemplo: enrutamiento según la longitud del texto

El ejemplo trata el texto de manera diferente según su longitud:

  • Si el texto tiene más de 50 palabras, se genera un resumen breve.
  • De lo contrario, se desarrolla la idea contenida en el texto.

El flujo de trabajo resultante:

Input Text
    ↓
Condition Check
    ↓
Is word count > 50?
    ↓
 ┌───────┴───────┐
Yes              No
 ↓                ↓
Summary          Extend
Chain            Chain
 ↓                ↓
LLM              LLM
 ↓                ↓
Parser           Parser
  \                /
   \              /
    Final Output

Hay dos prompts que dirigen las ramas: summary_prompt solicita un resumen breve, mientras que extend_prompt pide al modelo que desarrolle la idea. Una función sencilla, is_long_text(), cuenta las palabras, y RunnableBranch utiliza su resultado para elegir la cadena a seguir. Al igual que con los otros patrones, existe una versión de Gemini y otra de OpenAI.

En su forma general, un pipeline condicional bidireccional se ve así:

User Input
    ↓
Condition Check
    ↓
 ┌───────┴───────┐
Yes              No
 ↓                ↓
Prompt 1        Prompt 2
 ↓                ↓
LLM              LLM
 ↓                ↓
Parser           Parser
  \                /
   └──────┬───────┘
           ↓
      Final Output

Para cualquier entrada dada, solo se ejecuta una rama.

Rastreo de una entrada concreta

La entrada de ejemplo es una definición de una sola oración sobre el aprendizaje automático, describiéndolo como un ramo de la IA en el que los métodos estadísticos permiten a las computadoras aprender de los datos.

Esa oración tiene 22 palabras, muy por debajo del umbral de 50 palabras, por lo que la condición devuelve False y se elige la cadena de extensión:

Input Text
    ↓
Condition Check
    ↓
22 > 50?
    ↓
   No
    ↓
Extend Chain
    ↓
Gemini 2.5 Flash
    ↓
StrOutputParser
    ↓
Extended Text

Si se introduce un texto más largo de 50 palabras, la condición cambia a True, lo que hace que la entrada sea enviada a la cadena de resumen en su lugar.

Cuándo es útil el enrutamiento condicional

El enrutamiento resulta útil siempre que el mismo tipo de entrada requiera un tratamiento diferente según alguna propiedad:

  • Longitud: resumir textos largos y detallar los textos cortos.
  • Sentimiento: enviar comentarios positivos y negativos a cadenas de respuesta distintas.
  • Tipo de solicitud: dirigir cada categoría de solicitud a una cadena especializada.

La propiedad importante es que no se pagan costos por ramas que no se necesitan. Solo se ejecuta el camino seleccionado:

Input
  ↓
Condition
  ↓
Choose one branch
  ↓
Execute selected chain
  ↓
Output

Eso hace que el flujo de trabajo sea adaptable de una manera en la que una secuencia fija no puede serlo, y además ahorra llamadas al modelo en comparación con ejecutar todas las ramas y descartar los resultados.

La versión Gemini

Aquí está el programa de enrutamiento completo en Gemini:

from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableBranch
from dotenv import load_dotenv

load_dotenv()

llm = GoogleGenerativeAI(model="gemini-2.5-flash")

parser = StrOutputParser()

# Prompt 1 : Summary
summary_prompt = PromptTemplate(
    input_variables=["text"],
    template="Summarize the following text briefly:\n\n{text}"
)

# Prompt 2 : Extend
extend_prompt = PromptTemplate(
    input_variables=["text"],
    template="Extend the idea in the following long text:\n\n{text}"
)

# Chain 1 : Summary
summary_chain = summary_prompt | llm | parser

# Chain 2 : Extend
extend_chain = extend_prompt | llm | parser

def is_long_text(input: dict) -> bool:
    return len(input["text"].split()) > 50

conditional_chain = RunnableBranch(
    (lambda x: is_long_text(x), summary_chain),
    extend_chain
)

text_input = {
    "text": "Machine learning is a field of artificial intelligence that uses statistical techniques to give computer systems the ability to learn from data."
}

result = conditional_chain.invoke(text_input)

# print("\nOutput:\n")
# print(result)

conditional_chain.get_graph().print_ascii()

Define dos cadenas pequeñas, summary_chain y extend_chain, además de un predicado para contar palabras, y luego permite que RunnableBranch elija entre las cadenas según ese predicado.

Análisis del código de Gemini

El modelo sigue siendo Gemini 2.5 Flash una vez más:

llm = GoogleGenerativeAI(model="gemini-2.5-flash")

Un StrOutputParser convierte cada respuesta en una cadena de texto:

parser = StrOutputParser()

La solicitud de resumen pide un breve resumen:

summary_prompt = PromptTemplate(
    input_variables=["text"],
    template="Summarize the following text briefly:\n\n{text}"
)

La solicitud de extensión se utiliza en el otro camino:

extend_prompt = PromptTemplate(
    input_variables=["text"],
    template="Extend the idea in the following long text:\n\n{text}"
)

Observe atentamente su redacción: se refiere a “el siguiente texto largo”, sin embargo, esta rama solo se ejecuta con entradas cortas. La discrepancia no rompe nada, pero envía al modelo una indicación engañosa. Reescribirla como “Desarrolle la idea en el siguiente texto” es una pequeña corrección que vale la pena hacer en su propia versión.

Cada prompt se combina con el modelo y el analizador en su propia cadena:

summary_chain = summary_prompt | llm | parser

extend_chain = extend_prompt | llm | parser

Esto genera dos rutas independientes:

Summary Prompt → Gemini → Parser

Extend Prompt  → Gemini → Parser

El paso condicional decide cuál de ellas se ejecuta.

La condición es una función ordinaria de Python:

def is_long_text(input: dict) -> bool:
    return len(input["text"].split()) > 50

Divide input[“text”] en espacios en blanco y cuenta las partes. Su contrato es simple:

True  → if word count > 50
False → otherwise

Dividir el texto por espacios en blanco da un conteo aproximado de palabras; la puntuación permanece unida a las palabras y los términos con guiones se consideran uno solo. Esto es adecuado para el enrutamiento por longitud, pero si realmente te importan los límites del modelo, cuenta los tokens en lugar de las palabras. También ten en cuenta que el nombre del parámetro input sobrescribe una función incorporada de Python; funciona, pero un nombre como data es más claro.

Con todas las partes en su lugar, RunnableBranch monta el enrutador:

conditional_chain = RunnableBranch(
    (lambda x: is_long_text(x), summary_chain),
    extend_chain
)

El primer argumento es una tupla que funciona como regla:

If is_long_text(x) is True
        ↓
Execute summary_chain

El argumento final, que no está envuelto en una tupla, es el valor por defecto:

Otherwise
        ↓
Execute extend_chain

No se requiere el envoltorio lambda. RunnableBranch acepta cualquier función llamable como condición, por lo que pasar is_long_text directamente también funciona igual de bien. En conjunto, el enrutamiento se ve así:

                    Input
                      ↓
              is_long_text()
                 /        \
              True        False
               ↓            ↓
        summary_chain   extend_chain
               ↓            ↓
             Gemini       Gemini
               ↓            ↓
             Parser       Parser
                \          /
                 \        /
                  Output

Al ejecutarlo, se utiliza el diccionario de entrada:

result = conditional_chain.invoke(text_input)

Primero se verifica la condición, y solo la rama seleccionada realiza la llamada al modelo.

Qué devuelve

Con la entrada de muestra de 22 palabras, el predicado devuelve:

is_long_text() → False

por lo que extend_chain se encarga de procesar la entrada. Conceptualmente:

Input Text
    ↓
Word Count Check
    ↓
False
    ↓
extend_chain
    ↓
Gemini 2.5 Flash
    ↓
StrOutputParser
    ↓
Extended Text

Si se proporciona texto de más de 50 palabras, el predicado devuelve:

is_long_text() → True

lo que cambia la ruta a:

Input Text
    ↓
Word Count Check
    ↓
True
    ↓
summary_chain
    ↓
Gemini 2.5 Flash
    ↓
StrOutputParser
    ↓
Short Summary

Un pipeline, dos rutas posibles, decididas por la propia entrada.

El mismo router en OpenAI

La versión de OpenAI mantiene la lógica de enrutamiento y solo cambia el modelo:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableBranch
from dotenv import load_dotenv

load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()
# Prompt 1 : Summary
summary_prompt = PromptTemplate(
    input_variables=["text"],
    template="Summarize the following text briefly:\n\n{text}"
)
# Prompt 2 : Extend
extend_prompt = PromptTemplate(
    input_variables=["text"],
    template="Extend the idea in the following long text:\n\n{text}"
)
# Chain 1 : Summary
summary_chain = summary_prompt | llm | parser
# Chain 2 : Extend
extend_chain = extend_prompt | llm | parser
def is_long_text(input: dict) -> bool:
    return len(input["text"].split()) > 50
conditional_chain = RunnableBranch(
    (lambda x: is_long_text(x), summary_chain),
    extend_chain
)
text_input = {
    "text": "Machine learning is a field of artificial intelligence that uses statistical techniques to give computer systems the ability to learn from data."
}
result = conditional_chain.invoke(text_input)
print(result)
conditional_chain.get_graph().print_ascii()

Dos cadenas, un predicado, una RunnableBranch, igual que en Gemini.

El modelo es gpt-4o-mini:

llm = ChatOpenAI(model="gpt-4o-mini")

Los dos modelos de prompt permanecen sin cambios: uno para resumir y otro para ampliar:

summary_prompt = PromptTemplate(
    input_variables=["text"],
    template="Summarize the following text briefly:\n\n{text}"
)

extend_prompt = PromptTemplate(
    input_variables=["text"],
    template="Extend the idea in the following long text:\n\n{text}"
)

Las cadenas se componen exactamente como antes:

summary_chain = summary_prompt | llm | parser

extend_chain = extend_prompt | llm | parser

Ahora ambos caminos pasan por OpenAI:

Summary Prompt → OpenAI → StrOutputParser

Extend Prompt → OpenAI → StrOutputParser

El predicado sigue siendo la misma verificación de 50 palabras:

def is_long_text(input: dict) -> bool:
    return len(input["text"].split()) > 50

El enrutador se construye de la misma manera:

conditional_chain = RunnableBranch(
    (lambda x: is_long_text(x), summary_chain),
    extend_chain
)

Un resultado True envía la entrada a summary_chain; todo lo demás va a extend_chain.

Se invoca la cadena y se imprime el resultado:

result = conditional_chain.invoke(text_input)
print(result)

Para la oración de ejemplo, el predicado vuelve a evaluarse como:

is_long_text() → False

por lo que se ejecuta la cadena de ampliación y se devuelve una explicación más detallada de la entrada, similar a esto:

Machine learning enables computer systems to learn patterns
from data and improve their performance without being explicitly
programmed for every task. It is widely used in applications
such as recommendation systems, image recognition, and predictive
analytics.

El texto exacto depende del modelo y cambiará entre ejecuciones.

Imprimir la estructura de la cadena

El mismo ayudante de gráficos funciona para los enrutadores:

conditional_chain.get_graph().print_ascii()

El elemento destacado del diagrama generado es el nodo Branch. Es el punto de decisión donde LangChain evalúa las condiciones y pasa la entrada a una de las cadenas candidatas.

Cómo difieren las dos ramas

Cada rama tiene su propia responsabilidad. La rama de resumen:

summary_prompt
      ↓
Gemini
      ↓
StrOutputParser
      ↓
Short Summary

La rama de extensión:

extend_prompt
      ↓
Gemini
      ↓
StrOutputParser
      ↓
Extended Text

El predicado elige entre ellas:

                 Input
                   ↓
            is_long_text()
              /         \
           True         False
            ↓             ↓
       Summary Chain   Extend Chain

Este es el contraste clave con una cadena paralela: las ramas no se ejecutan ambas. Solo se ejecuta el camino cuya condición se cumple, lo que mantiene los costos y la latencia proporcionales al trabajo realmente necesario.

Enrutamiento según la propia clasificación del modelo

Las condiciones no se limitan a controles simples como el recuento de palabras. Un patrón común y más eficaz consiste en permitir que una primera cadena clasifique la entrada y luego redirigir según esa clasificación. En un ejemplo de análisis de sentimientos, los comentarios recibidos se etiquetan como uno de dos valores:

positive
negative

y RunnableBranch los envía a la cadena de respuestas correspondiente a esa etiqueta:

Feedback
   ↓
Sentiment Classifier
   ↓
 ┌───────────────┐
 │               │
Positive       Negative
 │               │
 ↓               ↓
Positive       Negative
Response       Response
 │               │
 └───────┬───────┘
         ↓
      Output

Una forma sólida de crear el clasificador es hacer que el modelo devuelva una salida estructurada validada por un modelo Pydantic, por ejemplo, un campo restringido a positive o negative. Las condiciones de las ramas luego verifican ese campo en lugar de analizar texto sin formato, lo que evita sorpresas cuando el modelo responde “Positive.” con mayúscula y punto. Siempre mantenga una rama por defecto sensata, ya que el clasificador eventualmente devolverá algo que no esperaba.

Esto demuestra que una condición puede depender de la información generada por una cadena anterior, no solo de la entrada bruta. Para enrutamientos más complejos, la documentación actual de LangChain también describe el enrutamiento mediante una función personalizada envuelta en RunnableLambda que devuelve la cadena a ejecutar; vale la pena verificar qué enfoque recomiendan los documentos para su versión.

La ruta completa de datos

El patrón condicional general es:

                       Input
                         ↓
                  Condition Check
                         ↓
                 ┌───────┴───────┐
                 ↓               ↓
              Condition       Condition
               True            False
                 ↓               ↓
             Chain 1          Chain 2
                 ↓               ↓
                LLM             LLM
                 ↓               ↓
              Parser          Parser
                 \               /
                  \             /
                   └─────┬───────┘
                         ↓
                       Output

En el ejemplo de longitud de texto:

Machine Learning Text
          ↓
   is_long_text()
          ↓
     Word Count > 50?
        /       \
      Yes       No
       ↓         ↓
   Summary     Extend
    Chain       Chain
       ↓         ↓
    Gemini     Gemini
       ↓         ↓
    Parser     Parser
        \       /
         \     /
          Output

Las entradas cortas siguen una ruta:

Word Count ≤ 50
       ↓
Extend Chain

Las entradas largas siguen la otra:

Word Count > 50
       ↓
Summary Chain

De forma general, aquí están las cuatro formas una al lado de la otra:

Simple Chain
A → B → C

Sequential Chain
A → B → C → D → E

Parallel Chain
      ┌→ B ─┐
A ────┼→ C ─┼→ Output
      └→ D ─┘

Conditional Chain
       ↓
      A
       ↓
   Condition
    /     \
   B       C

Una cadena condicional permite que el flujo de trabajo del LLM tome decisiones, ejecutando diferentes lógicas según la entrada o un valor calculado anteriormente en el proceso.

Notas prácticas antes de enviar

Hay algunos problemas recurrentes que vale la pena tener en cuenta al pasar de ejemplos a código real:

  • Asigne nombres de modelo. Los constructores sin argumento de modelo heredan el valor predeterminado que utilice la integración instalada, y los identificadores obsoletos terminan siendo retirados.
  • Mantenga el analizador. Los modelos de chat devuelven objetos de mensaje; StrOutputParser proporciona un valor str predecible para cada paso posterior.
  • Preste atención a las transferencias. Una cadena simple solo llena automáticamente un prompt cuando dicho prompt tiene exactamente una variable. Para casos más complejos, construya el diccionario de forma explícita.
  • Coordine las claves con las variables. En un proceso de despliegue paralelo seguido de una fusión, las claves deben coincidir con los nombres de las variables del prompt de fusión.
  • Planifique para casos de fallo. Un branch que falla en un paso paralelo hace que falle todo el paso; agregue intentos repetidos o soluciones alternativas para llamadas poco fiables.
  • Utilice importaciones modernas. Obtenga los Runnables de langchain_core.runnables, no de las rutas heredadas de langchain.schema.
  • Visualice. get_graph().print_ascii() (con grandalf instalado) es una forma sencilla de confirmar que una cadena compleja está conectada como se pretende.
  • Para un recorrido más amplio que muestra estas cadenas junto con la recuperación de información, herramientas y agentes que funcionan con modelos locales, consulte LangChain 1.x en la práctica: cadenas, RAG, herramientas y agentes localmente.

    Puntos clave

    • Una cadena es una tubería declarada en la que la salida de cada componente alimenta al siguiente; el operador | la construye a partir de cualquier elemento Runnable.
    • Las cadenas simples implican un único paso; las cadenas secuenciales acumulan varios pasos, lo que añade latencia en cada etapa.
    • RunnableParallel ejecuta sub-cadenas independientes de forma concurrente y devuelve un diccionario indexado por el nombre de la rama, que las etapas posteriores pueden utilizar mediante el nombre de la variable.
    • RunnableBranch evalúa las condiciones en orden y ejecuta solo la primera rama que cumpla con los requisitos, recayendo en una opción predeterminada si no hay coincidencia.
    • Las condiciones pueden ser comprobaciones simples en la entrada o decisiones basadas en la clasificación estructurada de un modelo anterior.
    • Dado que el modelo es solo un componente, cambiar entre Gemini, OpenAI o Anthropic generalmente implica modificar una sola línea, mientras que los prompts, los analizadores y la estructura permanecen iguales.

    Todos los cuatro patrones se basan en la misma base: la interfaz Runnable, que es lo que permite componer prompts, modelos, analizadores y funciones propias desde un principio. Comprender directamente esa interfaz es el siguiente paso lógico una vez que estas estructuras resulten familiares.

    Lectura relacionada

  • Fortalecimiento de un agente de Python LangChain con siete middlewares incorporados — Aprenda cómo los middlewares de LangChain 1.0 añaden funciones de resumen, límites de llamadas, reintentos, solución de fallback para modelos, eliminación de información personal sensible y aprobación humana a un agente Gemini sin modificar su lógica principal.
  • Construir un refinador y generador personal de promptes con artefactos de Claude — Cómo transformar una conversación en Claude en una herramienta de promptes reutilizable que mejora los promptes débiles y expande las ideas básicas en versiones simples, avanzadas y expertas.
  • Fortaleciendo un agente de LangChain en Python con siete middleware integrados — Aprenda cómo el middleware de LangChain 1.0 agrega funcionalidades como resumen, límites de llamadas, reintentos, solución de fallback para modelos, eliminación de información personal sensible y aprobación humana a un agente Gemini sin afectar su lógica principal.
  • Prompt inverso: Transformar una buena sesión con un LLM en un prompt reutilizable — Aprenda cómo extraer un prompt de uso único a partir de una conversación exitosa con un LLM en múltiples turnos, y cómo la inversión de prompts permite recuperarlos cuando solo se dispone de las respuestas.
  • Seis primitivas de LangGraph y el modo de fallo oculto en cada una — Aprenda sobre el estado de LangGraph, sus nodos, aristas, enrutamiento condicional, puntos de control e interrupciones a través de los errores específicos que cada uno genera y cómo evitarlos.