TL;DR
LangChain se ha convertido en una de las librerías Python más utilizadas para interactuar con LLMs en menos de un año, pero LangChain era sobre todo una librería para POCs ya que carecía de la capacidad de crear aplicaciones complejas y escalables.
Todo cambió en agosto de 2023 cuando lanzaron LangChain Expression Language (LCEL), una nueva sintaxis que salva la distancia entre POC y 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 creando aplicaciones LLM.
Prompts, LLM y cadenas, refresquemos la memoria
Antes de sumergirnos en la sintaxis de LCEL, creo que es beneficioso refrescar nuestra memoria sobre conceptos de LangChain como LLM y Prompt o incluso una Cadena.
LLM: En langchain, llm es una abstracción en torno al modelo utilizado para realizar las terminaciones como openai gpt3.5, claude, etc...
Pregunta: Es la entrada del objeto LLM, que formulará las preguntas LLM y dará sus objetivos.
Cadena: Se refiere a una secuencia de llamadas a un LLM, o a cualquier paso de procesamiento de data .

¡Ahora que las definiciones están fuera del camino, supongamos que queremos crear un Compañia! Necesitamos un nombre chulo y pegadizo y un modelo de negocio para ganar dinero.
Ejemplo - Compañia name & Business model with Old Chains
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
USER_INPUT = "calcetines de colores"
llm = OpenAI(temperatura=0)
prompt_template_product = "¿Cuál es un buen nombre para un Compañia que fabrica {producto}?"
Compañia_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
Compañia_name_output = Compañia_name_chain(USER_INPUT)
prompt_template_business = “Give me the best business model idea for my company named: {company}”
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(company_name_output[“text”])
print(Compañia_nombre_salida)
print(salida_modelo_de_negocio)
>>> {‘product’: ‘colorful socks’, ‘text’: ‘Socktastic!’}
>>> {‘company’: ‘Socktastic!’,’text’: “A subscription-based service offering a monthly delivery…”}
Esto es bastante fácil de seguir, podemos ver un poco de redundancia, pero es manejable.
Añadamos un poco de personalización manejando los casos en los que el usuario no está utilizando nuestra cadena como se esperaba.
¿Puede que 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
importar ast
USER_INPUT = "Harrison Chase"
llm = OpenAI(temperatura=0)
# -- Mismo código que antes
prompt_template_product = "¿Cuál es un buen nombre para un Compañia que fabrica {producto}?"
Compañia_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
prompt_template_business = “Give me the best business model idea for my company named: {company}”
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
# -- Nuevo código
prompt_template_is_product = (
“Your goal is to find if the input of the user is a plausible product namen”
“Questions, greetings, long sentences, celebrities or other non relevant inputs are not considered productsn”
“input: {product}n”
“Answer only by ‘True’ or ‘False’ and nothing moren”
)
prompt_template_cannot_respond = (
“You cannot respond to the user input: {product}n”
“Ask the user to input the name of a product in order for you to make a company out of it.n”
)
cadena_no_puede_responder = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_no_puede_responder))
Compañiacadena_nombre = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
cadena_modelo_empresa = 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 usamos bool en una cadena no vacía 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_nombre_salida = Compañia_nombre_cadena(USER_INPUT)
business_model_output = business_model_chain(Compañia_name_output["text"])
print(salida_modelo_empresa)
si no
print(cadena_no_puede_responder(USER_INPUT))
Esto se hace un poco más difícil de entender, resumámoslo:
Comienzan a surgir múltiples problemas:
¿Qué es el Lenguaje de Expresión LangChain (LCEL)?
LCEL es una interfaz unificada y una sintaxis para escribir cadenas listas para la producción componibles, hay mucho que desentrañar para entender lo que significa.
Primero intentaremos entender la nueva sintaxis reescribiendo la cadena de antes.
Ejemplo - Compañia name & Business model with 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(temperatura=0)
prompt_template_product = “What is a good name for a company that makes {product}?”
prompt_template_business = “Give me the best business model idea for my company named: {company}”
chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()}
| PromptTemplate.from_template(prompt_template_business)
| llm
)
business_model_output = chain.invoke({‘product’: ‘colorful socks’})
Un montón de código inusual, en sólo unas pocas líneas :
Pero, ¿es realmente más componible crear cadenas de esta manera?
Pongámoslo a prueba añadiendo is_a_product_chain() y la bifurcación si la entrada del usuario no es correcta. Podemos incluso escribir la cadena con Python Typing, hagámoslo como una buena práctica.
Ejemplo - Personalización y enrutamiento con LCEL
de 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(temperatura=0)
prompt_template_product = “What is a good name for a company that makes {product}?”
prompt_template_cannot_respond = (
“You cannot respond to the user input: {product}n”
“Ask the user to input the name of a product in order for you to make a company out of it.n”
)
prompt_template_business = “Give me the best business model idea for my company named: {company}”
prompt_template_is_product = (
“Your goal is to find if the input of the user is a plausible product namen”
“Questions, greetings, long sentences, celebrities or other non relevant inputs are not considered productsn”
“input: {product}n”
“Answer only by ‘True’ or ‘False’ and nothing moren”
)
answer_user_chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()}
| PromptTemplate.from_template(prompt_template_business)
| llm
).with_types(input_type=Dict[str, str], output_type=str)
is_producto_cadena = (
PromptTemplate.from_template(prompt_template_is_product)
| llm
| BooleanOutputParser(true_val='Verdadero', false_val='Falso')
).with_types(input_type=Dict[str, str], output_type=bool)
cadena_no_puede_responder = (
PromptTemplate.from_template(prompt_template_cannot_respond) | llm
).with_types(input_type=Dict[str, str], output_type=str)
cadena_completa = rama_ejecutable(
(is_product_chain, answer_user_chain),
cadena_no_puede_responder
).with_types(input_type=Dict[str, str], output_type=str)
print(full_chain.invoke({‘product’: USER_INPUT}))
Enumeremos las diferencias:
¿Por qué LCEL es mejor para la industrialización?
Si estuviera leyendo este artículo hasta este punto exacto, y alguien me preguntara si me convence LCEL, probablemente diría que no. La sintaxis es demasiado diferente, y probablemente pueda organizar mi código en funciones para obtener casi exactamente el mismo código. Pero estoy aquí, escribiendo este artículo, así que debe haber algo más.
Invocación, flujo y lote listos para usar
Al utilizar LCEL, su cadena dispone automáticamente de:
mi_cadena = prompt | llm
# ---invoke--- #
result_with_invoke = mi_cadena.invoke("¡hola mundo!")
# ---batch--- #
resultado_con_lote = mi_cadena.lote(["hola", "mundo", "!"])
# ---stream--- #
for chunk in mi_cadena.stream("¡hola mundo!"):
print(chunk, flush=True, end="")
Cuando iteras, puedes utilizar el método invoke para facilitar el proceso de desarrollo. Pero al mostrar la salida de su cadena en una interfaz de usuario, que desea transmitir la respuesta. Ahora puedes utilizar el método stream sin reescribir nada.
Métodos asíncronos listos para usar
La mayoría de las veces, el frontend y el backend de tu aplicación estarán separados, lo que significa que el frontend hará una petición al backend. Si tienes varios usuarios, puede que necesites gestionar varias peticiones en tu backend al mismo tiempo.
Dado que la mayor parte del código en LangChain es sólo 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 necesidad de preocuparse por la implementación, porque los métodos asíncronos ya están disponibles si usas LCEL:
.ainvoke() / .abatch() / .astream: versiones asíncronas de invoke, batch y stream.
También recomiendo leer la página Why use LCEL de la documentación de LangChain con ejemplos para cada método sync / async.
Langchain consiguió esas características "out of the box" creando una interfaz unificada llamada "Runnable". Ahora, para aprovechar plenamente LCEL, tenemos que sumergirnos en lo que es esta nueva interfaz Runnable.
La interfaz Runnable
Todos los objetos que hemos usado en la sintaxis LCEL hasta ahora son Runnables. Es un objeto python creado por LangChain, este objeto hereda automaticamente todas las caracteristicas de las que hemos hablado antes y muchas mas. Usando la sintaxis LCEL, componemos un nuevo Runnable en cada paso, lo que significa que el objeto final creado será también un Runnable. Puedes aprender más sobre la interfaz en la documentación oficial.
Todos los objetos del código siguiente son Runnables o diccionarios que se convierten automáticamente en un 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
| {‘company’: RunnablePassthrough()} # <— 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(número_de_cadena_uno == número_de_cadena_dos)
>>> Verdadero
¿Por qué utilizamos RunnableParallel() y no simplemente Runnable()?
Porque, cada Runnable dentro de un RunnableParallel se ejecuta en paralelo. Esto significa que si tienes 3 pasos independientes en tu Runnable, se ejecutarán al mismo tiempo en diferentes hilos de tu máquina, ¡mejorando la velocidad de tu cadena de forma gratuita!
Inconvenientes de LCEL
A pesar de sus ventajas, LCEL tiene algunos inconvenientes potenciales:
Conclusión
En conclusión, LangChain Expression Language (LCEL) es una poderosa herramienta que aporta una nueva perspectiva a la construcción de aplicaciones Python. A pesar de su sintaxis poco convencional, recomiendo encarecidamente el uso de LCEL por las siguientes razones :
Para ir más lejos...
La abstracción ejecutable
En algunos casos, creo que es importante entender la abstracción que LangChain ha implementado para hacer funcionar la sintaxis LCEL.
Puedes reimplementar las funcionalidades básicas de Runnable fácilmente de la siguiente manera:
clase Runnable:
def __init__(self, func):
self.func = func
def __or__(self, other):
def funcion_encadenada(*args, **kwargs):
# self.func está a la izquierda, other está a la derecha
return otro(auto.func(*cargas, **cargas))
return Runnable(función_encadenada)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def sumar_diez(x):
return x + 10
def divide_entre_dos(x):
return x / 2
runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
cadena = runnable_add_ten | runnable_divide_by_two
resultado = cadena(8) # (8+10) / 2 = 9,0 debería ser la respuesta
print(resultado)
>>> 9.0
Un Runnable es simplemente un objeto python en el que se ha sobrescrito el método .__or__().
En la práctica, LangChain ha añadido un montón de funcionalidades como la conversión de diccionarios a Runnable, capacidades de tipado, capacidades de configuración, ¡y métodos invoke, batch, stream y async!
¿Por qué no prueba LCEL en su próximo proyecto?
Si desea obtener más información, le recomiendo encarecidamente que consulte el libro de cocina LangChain en LCEL.