En resumen

  • Pasar más rápido del prototipo a producción: tal y como se describe en la documentación de Langchain, «LCEL es una forma declarativa de combinar cadenas fácilmente. LCEL se diseñó desde el primer momento para permitir llevar los prototipos a producción sin necesidad de modificar el código».

  • Creación de cadenas personalizadas: LCEL simplifica el proceso de creación de cadenas personalizadas con una nueva sintaxis.

  • Transmisión y procesamiento por lotes listos para usar: LCEL te ofrece funciones de procesamiento por lotes, transmisión y asíncrono de forma gratuita.

  • Interfaz unificada: Servicios paralelización Servicios , capacidades de tipado y cualquier función futura que LangChain pueda desarrollar.

  • LCEL es el futuro de LangChain: LCEL ofrece una nueva perspectiva sobre el desarrollo de aplicaciones basadas en modelos de lenguaje grande (LLM). Te recomiendo encarecidamente que lo utilices en tu próximo proyecto con LLM.

LangChain se ha convertido en menos de un año en una de las bibliotecas de Python más utilizadas para interactuar con los modelos de lenguaje grande (LLM), pero, en su mayoría, se trataba de una biblioteca destinada a pruebas de concepto (POC), ya que carecía de la capacidad para crear aplicaciones complejas y escalables.
Todo cambió en agosto de 2023, cuando lanzaron LangChain Expression Language (LCEL), una nueva sintaxis que salva la brecha entre las pruebas de concepto y la producción. Este artículo te guiará a través de los entresijos de LCEL, mostrándote cómo simplifica la creación de cadenas personalizadas y por qué debes aprenderlo si estás desarrollando aplicaciones de LLM.

Prompts, modelos de lenguaje grande (LLM) y cadenas: refresquemos la memoria

Antes de profundizar en la sintaxis de LCEL, creo que conviene refrescar la memoria sobre conceptos de LangChain como LLM, «prompt» o incluso «chain».

LLM: En LangChain, «llm» es una abstracción del modelo utilizado para generar las completaciones, como OpenAI GPT-3.5, Claude, etc.

Indicación: Esta es la entrada del objeto LLM, que formulará preguntas al LLM y le indicará sus objetivos.

Cadena: Se refiere a una secuencia de llamadas a un modelo de lenguaje grande (LLM) o a cualquier paso data .

class="lazyload

Ahora que ya hemos aclarado las definiciones, ¡supongamos que queremos crear una Compañia! ¡Necesitamos un nombre realmente genial y pegadizo, y un modelo de negocio para ganar dinero!

Ejemplo: Compañia y modelo de negocio con cadenas tradicionales

from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI

USER_INPUT = "calcetines de colores"
llm = OpenAI(temperature=0)

prompt_template_product = "¿Qué nombre sería adecuado para una Compañia se dedica a?"
Compañia= LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
Compañia= Compañia(USER_INPUT)

prompt_template_business = "Dame la mejor idea de modelo de negocio para mi Compañia : "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(Compañia["text"])

print(Compañia)
print(business_model_output)

>>>
>>>

Es bastante fácil de seguir; se aprecia cierta redundancia, pero es manejable.

Añadamos un poco de personalización para gestionar los casos en los que el usuario no utilice nuestra cadena como se espera.
¿Quizás el usuario introduzca algo que no tenga nada que ver con el objetivo de nuestra cadena? En ese caso, queremos detectarlo y responder adecuadamente.

Ejemplo: personalización y enrutamiento con cadenas antiguas

from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
import ast

USER_INPUT = "Harrison Chase"
llm = OpenAI(temperature=0)

# —- El mismo código que antes
prompt_template_product = "¿Qué nombre le pondrías a una Compañia se dedica a ?"
Compañia= LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = "Dame la mejor idea de modelo de negocio para mi Compañia : "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))

# —- Código nuevo

prompt_template_is_product = (
"Tu objetivo es determinar si la entrada del usuario es un nombre de producto plausible"
"Las preguntas, los saludos, las frases largas, los nombres de famosos u otras entradas no relevantes no se consideran nombres de producto"
"Entrada: n"
"Responde solo con 'Verdadero' o 'Falso' y nada más"
)

prompt_template_cannot_respond = (
"No puedes responder a la entrada del usuario: n"
"Pide al usuario que introduzca el nombre de un producto para que puedas crear una Compañia de él.n"
)

cannot_respond_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_cannot_respond))
Compañia= LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
is_a_product_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_is_product))

# Si utilizamos «bool» con una cadena no vacía, el resultado será «True», por lo que necesitamos «literal_eval»
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
if is_a_product:
Compañia= Compañia(USER_INPUT)
business_model_output = business_model_chain(Compañia["text"])
print(business_model_output)
else:
print(cannot_respond_chain(USER_INPUT))

Esto se vuelve un poco más difícil de entender, así que resumamos:

  • Hemos creado una nueva cadena is_real_product_chain() que detecta si la entrada del usuario puede considerarse un producto.

  • Utilizamos condiciones «if/else» para ramificar entre las cadenas.

Empiezan a surgir varios problemas:

  • El código es un poco redundante, ya que contiene mucho código repetitivo.

  • Es difícil distinguir a qué LLMChain se refiere cada LLMChain; para entenderlo, tenemos que rastrear las entradas y las salidas.

  • Es fácil cometer errores con los tipos de salida de las cadenas; por ejemplo, la salida de `is_a_product_chain()` es una cadena de caracteres que posteriormente debe evaluarse como un valor booleano.

¿Qué es el lenguaje de expresiones LangChain (LCEL)?

LCEL es una interfaz y una sintaxis unificadas para escribir cadenas modulables y listas para su implementación; hay mucho que analizar para comprender lo que esto significa.

En primer lugar, intentaremos comprender la nueva sintaxis reescribiendo la cadena anterior.

Ejemplo: Compañia y modelo de negocio con LCEL

from langchain_core.runnables import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI

USER_INPUT = "calcetines de colores"
llm = OpenAI(temperature=0)

prompt_template_product = "¿Qué nombre le pondrías a una Compañia ?"
prompt_template_business = "Dame la mejor idea de modelo de negocio para mi Compañia : "

chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
)

business_model_output = chain.invoke()

Mucho código inusual, en solo unas pocas líneas:

  • ¿Hay un extraño operador | entre un PromptTemplate, un LLM y un diccionario? El operador | simplemente sirve para indicar: «toma el diccionario de la izquierda y pásalo como entrada al objeto de la derecha».

  • ¿Por qué pasamos la variable «product» dentro de un diccionario en lugar de como una cadena, como antes? Si has leído el punto n.º 1, sabrás que el operador «|» espera que los argumentos sean diccionarios; por eso pasamos el argumento «product» dentro de un diccionario.

  • ¿Por qué aparece el nombre de la función RunnablePassthrough() en lugar del Compañia ? RunnablePassthrough() es un marcador de posición que significa: «por ahora no tenemos el Compañia , pero cuando lo tengamos, lo pondremos aquí». Explicaré qué significa el término «Runnable» en las siguientes secciones; por ahora, puedes ignorarlo.

  • ¿Por qué necesitamos un método específico como .invoke() en lugar de escribir chain()?Lo entenderemos en la siguiente parte, ¡pero esto ya nos da una idea de por qué LCEL facilita la industrialización!

¿Pero es realmente más fácil de combinar crear cadenas de esta manera?
Pongámoslo a prueba añadiendo la función `is_a_product_chain()` y la ramificación en caso de que la entrada del usuario no sea correcta. Incluso podemos definir el tipo de la cadena con Python Typing; hagámoslo como buena práctica.

Ejemplo: personalización y enrutamiento con LCEL

from typing import Dict
from langchain_core.runnables import RunnablePassthrough, RunnableBranch
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.output_parsers import BooleanOutputParser
from langchain_community.llms import OpenAI

USER_INPUT = "Harrrison Chase"
llm = OpenAI(temperature=0)

prompt_template_product = "¿Qué nombre le pondrías a una Compañia fabrica ?"
prompt_template_cannot_respond = (
"No puedes responder a la entrada del usuario: n"
"Pide al usuario que introduzca el nombre de un producto para que puedas crear una Compañia de él.n"
)
prompt_template_business = "Dame la mejor idea de modelo de negocio para mi Compañia : "
prompt_template_is_product = (
"Tu objetivo es determinar si la entrada del usuario es un nombre de producto plausible"
"Las preguntas, los saludos, las frases largas, los nombres de famosos u otras entradas no relevantes no se consideran nombres de productos"
"entrada: n"
"Responde solo con 'Verdadero' o 'Falso' y nada más"
)

answer_user_chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
).with_types(input_type=Dict[str, str], output_type=str)

is_product_chain = (
PromptTemplate.from_template(prompt_template_is_product)
| llm
| BooleanOutputParser(true_val='True', false_val='False')
).with_types(input_type=Dict[str, str], output_type=bool)

cannot_respond_chain = (
PromptTemplate.from_template(prompt_template_cannot_respond) | llm
).with_types(input_type=Dict[str, str], output_type=str)

full_chain = RunnableBranch(
(is_product_chain, answer_user_chain),
cannot_respond_chain
).with_types(input_type=Dict[str, str], output_type=str)

print(full_chain.invoke())

Enumeremos las diferencias:

  • La sintaxis es diferente, vale, eso ya está claro.

  • Hay cadenas intermedias definidas y llamadas dentro de una cadena más grande, casi como si fueran funciones.

  • Las entradas y salidas tienen un tipo, casi como las funciones.

  • Esto no parece Python.

¿Por qué es LCEL más adecuado para la industrialización?

Si hubiera leído este artículo hasta este preciso momento y alguien me preguntara si me ha convencido LCEL, probablemente diría que no. La sintaxis es demasiado diferente, y seguramente podría organizar mi código en funciones para obtener prácticamente el mismo resultado. Pero aquí estoy, escribiendo este artículo, así que debe de haber algo más.

Invocación, transmisión y procesamiento por lotes listos para usar

Al utilizar LCEL, su cadena cuenta automáticamente con:

  • .invoke(): Quieres pasar tus datos de entrada y obtener el resultado, ni más ni menos.

  • .batch(): Si deseas pasar varias entradas para obtener varias salidas, la paralelización se gestiona automáticamente (es más rápido que llamar a invoke tres veces).

  • .stream(): Esto te permite empezar a mostrar el inicio de la sugerencia antes de que se haya completado por completo.

my_chain = prompt | llm

# ———invoke——— #
result_with_invoke = my_chain.invoke(«hello world!»)

# ———lote——— #
result_with_batch = my_chain.batch([“hola”, “mundo”, “!”])

# ———stream——— #
for chunk in my_chain.stream(“hello world!”):
print(chunk, flush=True, end=””)

Al realizar iteraciones, puedes utilizar el método `invoke` para facilitar el proceso de desarrollo. Sin embargo, cuando se muestra el resultado de la cadena en una interfaz de usuario, es preferible transmitir la respuesta. Ahora puedes utilizar el método `stream` sin necesidad de reescribir nada.

Métodos asíncronos integrados

En la mayoría de los casos, el frontend y el backend de tu aplicación estarán separados, lo que significa que el frontend enviará una solicitud al backend. Si tienes varios usuarios, es posible que tengas que gestionar varias solicitudes en tu backend al mismo tiempo.

Dado que la mayor parte del código de LangChain consiste simplemente en esperar entre llamadas a la API, podemos aprovechar el código asíncrono para mejorar la escalabilidad de la API; si quieres entender por qué es importante, te recomiendo leer la historia de las «hamburguesas concurrentes» de la documentación de FastAPI.
No hay por qué preocuparse por la implementación, ya que los métodos asíncronos ya están disponibles si utilizas LCEL:

.ainvoke() / .abatch() / .astream: versiones asíncronas de invoke, batch y stream.

También recomiendo leer la página «¿Por qué usar LCEL?» de la documentación de LangChain, que incluye ejemplos para cada método síncrono y asíncrono.

Langchain logró esas funciones «listas para usar» creando una interfaz unificada denominada «Runnable». Ahora, para sacar el máximo partido a LCEL, debemos profundizar en qué consiste esta nueva interfaz Runnable.

La interfaz Runnable

Todos los objetos que hemos utilizado hasta ahora en la sintaxis LCEL son Runnables. Se trata de un objeto de Python creado por LangChain, que hereda automáticamente todas las características de las que hemos hablado anteriormente y muchas más. Al utilizar la sintaxis LCEL, creamos un nuevo Runnable en cada paso, lo que significa que el objeto final creado también será un Runnable. Puedes obtener más información sobre la interfaz en la documentación oficial.

Todos los objetos del código siguiente son de tipo «Runnable» o diccionarios que se convierten automáticamente en «Runnable»:

from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI

chain_number_one = (
PromptTemplate.from_template(prompt_template_product)
| llm
| # <— THIS WILL CHANGE
| PromptTemplate.from_template(prompt_template_business)
| llm
)

chain_number_two = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(company=RunnablePassthrough()) # <— THIS CHANGED
| PromptTemplate.from_template(prompt_template_business)
| llm
)

print(cadena_número_uno == cadena_número_dos)
>>> True

¿Por qué utilizamos RunnableParallel() y no simplemente Runnable()?

Esto se debe a que todos los Runnable dentro de un RunnableParallel se ejecutan en paralelo. Esto significa que, si tu Runnable contiene tres pasos independientes, estos se ejecutarán al mismo tiempo en diferentes subprocesos de tu máquina, ¡lo que mejora la velocidad de tu cadena sin ningún esfuerzo adicional!

Inconvenientes del LCEL

A pesar de sus ventajas, el LCEL presenta algunos posibles inconvenientes:

  • No cumple totalmente con la PEP: LCEL no respeta plenamente la PEP 20, el «Zen de Python», que establece que «lo explícito es mejor que lo implícito». (Para comprobar la PEP 20, puedes ejecutar «import this» en Python). Además, la sintaxis de LCEL no se considera «pythónica», ya que parece un lenguaje diferente, lo que podría hacer que LCEL resultara menos intuitivo para algunos desarrolladores de Python, que podrían negarse a utilizarlo.

  • LCEL es un lenguaje específico de dominio (DSL): se espera que los usuarios tengan ciertos conocimientos sobre las indicaciones, las cadenas o los modelos de lenguaje grande (LLM) para poder utilizar la sintaxis de manera eficaz.

  • Dependencias de entrada/salida: Las entradas intermedias y las salidas finales deben transmitirse desde el principio hasta el final. Por ejemplo, si deseas utilizar la salida de un paso intermedio como salida final, debes mantenerla a lo largo de todos los pasos posteriores. Esto puede dar lugar a argumentos adicionales en la mayoría de tus cadenas, que quizá no se utilicen, pero que son necesarios si deseas acceder a ellos a través de la salida.

Conclusión

En conclusión, el lenguaje de expresiones LangChain (LCEL) es una potente herramienta que aporta una nueva perspectiva al desarrollo de aplicaciones en Python. A pesar de su sintaxis poco convencional, recomiendo encarecidamente utilizar LCEL por las siguientes razones:

  • Interfaz unificada: ofrece una interfaz coherente para todas las cadenas, lo que facilita la industrialización del código gracias a modelos de flujo, asíncronos y de reserva listos para usar, tipificación, configuraciones en tiempo de ejecución, etc.

  • Paralelización automática: ejecuta automáticamente varias tareas en paralelo, lo que mejora la velocidad de ejecución de tus cadenas y la experiencia del usuario.

  • Componibilidad: te permite crear y modificar cadenas con facilidad, lo que hace que tu código sea más flexible y adaptable.

Para profundizar…

La abstracción ejecutable

En algunos casos, creo que es importante comprender la abstracción que LangChain ha implementado para que la sintaxis LCEL funcione.

Puedes reimplementar fácilmente las funcionalidades básicas de `Runnable` de la siguiente manera:

class Runnable:
def __init__(self, func):
self.func = func

def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func va a la izquierda, other va a la derecha
return other(self.func(*args, **kwargs))
return Runnable(chained_func)

def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)

def add_ten(x):
return x + 10

def dividir_por_dos(x):
return x / 2

runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
chain = runnable_add_ten | runnable_divide_by_two
result = chain(8) # (8+10) / 2 = 9.0 debería ser la respuesta
print(result)
>>> 9.0

Un Runnable es simplemente un objeto de Python en el que se ha sobrescrito el método .__or__().
En la práctica, LangChain ha añadido muchas funcionalidades, como la conversión de diccionarios a Runnable, capacidades de tipificación, capacidades de configuración y métodos de invocación, procesamiento por lotes, streaming y asíncronos.

Entonces, ¿por qué no pruebas LCEL en tu próximo proyecto?

Si quieres saber más, te recomiendo encarecidamente que eches un vistazo al libro de recetas de LangChain en LCEL.

class="lazyload

Blog de Medium de Artefact.

Este artículo se publicó inicialmente en Medium.com.
¡Síguenos en nuestro blog de Medium!