TL;DR

  • POC más rápido para producir: Tal y como lo describe la documentación de langchain, “LCEL es una forma declarativa de componer cadenas fácilmente. LCEL se diseñó desde el primer día para soportar la puesta en producción de prototipos, sin cambios en el código”.

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

  • Transmisión en flujo continuo y por lotes: LCEL le ofrece capacidades batch, streaming y async de forma gratuita.

  • Interfaz unificada: Ofrece paralelización automática, capacidades de tipado y cualquier característica futura que LangChain pueda desarrollar.

  • LCEL es el futuro de LangChain: LCEL ofrece una nueva perspectiva en el desarrollo de aplicaciones basadas en LLM. Recomiendo encarecidamente utilizarlo para su próximo proyecto LLM.

LangChain se ha convertido en uno de los más utilizados Biblioteca Python para interactuar con LLMs en menos de un año, pero LangChain era sobre todo una biblioteca para POCs ya que carecía de la capacidad de crear aplicaciones complejas y escalables.
Todo cambió en agosto de 2023 cuando lanzaron Lenguaje de expresión LangChain (LCEL), una nueva sintaxis que salva las distancias entre De POC a producción. Este artículo le guiará a través de los entresijos de LCEL, mostrándole cómo simplifica la creación de cadenas personalizadas y por qué debe aprenderlo si está construyendo Solicitudes LLM!

Prompts, LLM y cadenas, refresquemos la memoria

Antes de sumergirnos en la sintaxis 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...

Pregunte a: 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 del data.

Ahora que las definiciones están fuera del camino, ¡supongamos que queremos crear una empresa! Necesitamos un nombre realmente atractivo y pegadizo, ¡y un modelo de negocio para ganar dinero!

Ejemplo - Nombre de empresa y modelo de negocio con cadenas antiguas

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 una empresa que fabrica?"
nombre_empresa_cadena = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
nombre_empresa_salida = nombre_empresa_cadena(USER_INPUT)

prompt_template_business = "Deme la mejor idea de modelo de negocio para mi empresa llamada: "
cadena_modelo_empresa = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_empresa))
business_model_output = business_model_chain(nombre_empresa_output["texto"])

print(nombre_empresa_salida)
print(salida_modelo_de_negocio)

>>>
>>>

Esto es bastante fácil de seguir, podemos ver un poco de redundancia, pero es manejable.

Añadamos algo de personalización gestionando 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)

# -- El mismo código que antes
prompt_template_product = "¿Cuál es un buen nombre para una empresa que fabrica?"
nombre_empresa_cadena = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = "Deme la mejor idea de modelo de negocio para mi empresa llamada: "
cadena_modelo_empresa = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_empresa))

# -- Nuevo código

prompt_template_is_product = (
"Su objetivo es encontrar si la entrada del usuario es un nombre de producto plausible"
"Las preguntas, los saludos, las frases largas, las celebridades u otras entradas no relevantes no se consideran productosn"
"entrada: n"
"Responda sólo con 'Verdadero' o 'Falso' y nada másen"
)

prompt_template_cannot_respond = (
"No puede responder a la entrada del usuario: n"
"Pida al usuario que introduzca el nombre de un producto para que usted cree una empresa a partir de él.n"
)

cadena_no_puede_responder = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_no_puede_responder))
nombre_empresa_cadena = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
cadena_modelo_empresa = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_empresa))
is_a_product_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_is_product))

# Si usamos bool en una str no vacía será True, por lo que necesitamos `literal_eval`.
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
si es_un_producto:
nombre_empresa_salida = nombre_empresa_cadena(USER_INPUT)
business_model_output = business_model_chain(nombre_empresa_output["texto"])
print(salida_modelo_de_negocio)
si no:
print(cadena_no_puede_responder(USER_INPUT))

Esto se hace un poco más difícil de entender, resumámoslo:

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

  • Implementamos condiciones if/else para bifurcarnos entre las cadenas.

Comienzan a surgir múltiples problemas:

  • El código es un poco redundante ya que hay mucho boilerplate.

  • Es difícil distinguir qué LLMChain está vinculada a qué LLMChain, necesitamos rastrear las entradas y salidas para entenderlo.

  • Podemos cometer errores fácilmente en los tipos de salida de las cadenas, por ejemplo, la salida de es_una_cadena_de_productos() es una str que debería evaluarse posteriormente como bool.

¿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 comprender la nueva sintaxis reescribiendo la cadena de antes.

Ejemplo - Nombre de la empresa 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(temperatura=0)

prompt_template_product = "¿Cuál es un buen nombre para una empresa que fabrica?"
prompt_template_business = "Deme la mejor idea de modelo de negocio para mi empresa llamada: "

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

business_model_output = chain.invoke()

Una gran cantidad de código inusual, en tan sólo unas pocas líneas :

  • ¡Existe un extraño operador | entre un PromptTemplate, un llm y un diccionario?! El operador | sirve simplemente para decir: “tome el diccionario de la izquierda y páselo como entrada del objeto de la derecha”.

  • ¿Por qué pasamos la variable producto dentro de un diccionario en lugar de una cadena como antes? Si ha leído #1 sabrá que el operador | espera las entradas en forma de diccionario, por lo que daremos al argumento producto dentro de un diccionario.

  • ¿Por qué hay un nombre de función RunnablePassthrough() en lugar del nombre de la empresa? El RunnablePassthrough() es un marcador de posición para decir: “por ahora no tenemos el nombre de la empresa, pero cuando lo tengamos, colóquelo aquí”. Explicaré lo que significa el término “Runnable” en las próximas partes, por ahora está bien ignorarlo.

  • ¿Por qué necesitamos un método específico .invoke() en lugar de escribir chain() ?Lo entenderemos en la siguiente parte, pero es un anticipo de por qué LCEL facilita la industrialización.

Pero, ¿es realmente más componible crear cadenas de esta forma?
Pongámoslo a prueba añadiendo la función is_a_product_chain() y la ramificación si la entrada del usuario no es correcta. Incluso podemos escribir la cadena con Python Typing, hagámoslo como una 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(temperatura=0)

prompt_template_product = "¿Cuál es un buen nombre para una empresa que fabrica?"
prompt_template_cannot_respond = (
"No puede responder a la entrada del usuario: n"
"Pida al usuario que introduzca el nombre de un producto para que usted cree una empresa a partir de él.n"
)
prompt_template_business = "Deme la mejor idea de modelo de negocio para mi empresa llamada: "
prompt_template_is_product = (
"Su objetivo es encontrar si la entrada del usuario es un nombre de producto plausible"
"Las preguntas, los saludos, las frases largas, las celebridades u otras entradas no relevantes no se consideran productosn"
"entrada: n"
"Responda sólo con 'Verdadero' o 'Falso' y nada másen"
)

cadena_usuario_respuesta = (
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_cadena_producto = (
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(
(cadena_producto, cadena_usuario_respuesta),
cadena_no_puede_responder
).with_types(input_type=Dict[str, str], output_type=str)

print(cadena_completa.invocar())

Enumeremos las diferencias:

  • La sintaxis es diferente, ok eso es un hecho.

  • Hay cadenas intermedias definidas y llamadas en una cadena mayor, casi como funciones.

  • Las entradas y salidas están tipificadas, casi como las funciones.

  • Esto no parece python.

¿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.

Fuera de la caja invocar, flujo y lote

Al utilizar LCEL su cadena dispone automáticamente de:

  • .invoke(): Usted quiere pasar su entrada y obtener la salida, ni más ni menos.

  • .batch(): Si desea pasar múltiples entradas para obtener múltiples salidas, la paralelización se gestiona por usted (más rápido que llamar a invocar 3 veces).

  • .stream(): Esto le permite empezar a imprimir el principio de la finalización antes de que la finalización completa haya terminado.

mi_cadena = prompt | llm

# ---invoke--- #
resultado_con_invocar = mi_cadena.invocar(“¡hola mundo!”)

# ---lote--- #
resultado_con_lote = mi_cadena.lote([“hola”, “mundo”, “!”])

# ---stream--- #
for chunk in mi_cadena.stream(“¡hola mundo!”):
print(chunk, flush=Verdadero, end=””)

Cuando itere, puede utilizar el método invocar para facilitar el proceso de desarrollo. Pero cuando muestre la salida de su cadena en una interfaz de usuario, querrá transmitir la respuesta. Ahora puede utilizar el método stream sin reescribir nada.

Métodos asíncronos fuera de la caja

La mayoría de las veces, el frontend y el backend de su aplicación estarán separados, lo que significa que el frontend hará una petición al backend. Si tiene varios usuarios, puede que necesite gestionar varias peticiones en su backend al mismo tiempo.

Dado que la mayor parte del código en LangChain es simplemente esperar entre llamadas a la API, podemos aprovechar el código asíncrono para mejorar la escalabilidad de la API, si quiere entender por qué es importante le recomiendo que lea el documento hamburguesas concurrentes historia de la documentación FastAPI.
No hay necesidad de preocuparse por la implementación, porque los métodos asíncronos ya están disponibles si utiliza LCEL:

.ainvoke() / .abatch() / .astream: versiones asíncronas de invocar, lote y flujo.

También recomiendo leer el Por qué utilizar la página LCEL de la documentación LangChain con ejemplos para cada método sync / async.

Langchain consiguió esas características “listas para usar” creando una interfaz unificada llamada “Ejecutable”. Ahora, para aprovechar plenamente LCEL, tenemos que sumergirnos en lo que es esta nueva interfaz Runnable.

La interfaz Runnable

Todos los objetos que hemos utilizado en la sintaxis LCEL hasta ahora son Runnables. Se trata de un Objeto python creado por LangChain, este objeto hereda automáticamente todas las características de las que hemos hablado antes y muchas más. Al utilizar la sintaxis LCEL, componemos un nuevo Runnable en cada paso, lo que significa que el objeto final creado será también un Runnable. Puede obtener más información 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

número_de_cadena_uno = (
PromptTemplate.from_template(prompt_template_product)
| llm
| # <- ESTO CAMBIARÁ
| PromptTemplate.from_template(prompt_template_business)
| llm
)

número_de_cadena_dos = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(empresa=RunnablePassthrough()) # <- ESTO CAMBIÓ
| 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 tiene 3 pasos independientes en su Runnable, se ejecutarán al mismo tiempo en diferentes hilos de su máquina, ¡mejorando la velocidad de su cadena de forma gratuita!

Inconvenientes de LCEL

A pesar de sus ventajas, LCEL presenta algunos inconvenientes potenciales:

  • No cumple totalmente el PEP: LCEL no respeta totalmente PEP20, el Zen de Python, que establece que “lo explícito es mejor que lo implícito”. (Para comprobar PEP20 puede ejecutar import this en Python.) Además, la sintaxis de LCEL no se considera “Pythónica” ya que se siente como un lenguaje diferente, esto podría hacer que LCEL sea menos intuitivo para algunos desarrolladores Pyhton que podrían negarse a utilizarlo.

  • LCEL es un lenguaje específico de dominio (DSL): Se espera que los usuarios tengan algún conocimiento de las indicaciones, las cadenas o los LLM para poder aprovechar la sintaxis de forma eficaz.

  • Dependencias de entrada / salida: Las entradas intermedias y las salidas finales deben pasarse de principio a fin. Por ejemplo, si desea utilizar la salida de un paso intermedio como salida final, debe llevarla a través de todos los pasos posteriores. Esto puede dar lugar a argumentos extra en la mayoría de sus cadenas, que puede que no se utilicen pero que son necesarios si quiere acceder a ellos a través de la salida.

Conclusión

En conclusión, LangChain Expression Language (LCEL) es una potente herramienta que aporta una nueva perspectiva a la creación de aplicaciones Python. A pesar de su sintaxis poco convencional, recomiendo encarecidamente el uso de LCEL por las siguientes razones :

  • Interfaz unificada: Proporciona una interfaz coherente para todas las cadenas, lo que facilita la industrialización de su código con stream out of the box, async, modelos fallback, tipado, configuraciones en tiempo de ejecución, etc.

  • Paralelización automática: Ejecute automáticamente varias tareas en paralelo, mejorando la velocidad de ejecución de sus cadenas y la experiencia del usuario.

  • Composibilidad: Le permite componer y modificar cadenas fácilmente, haciendo que su código sea más flexible y adaptable.

Para ir más lejos...

La abstracción ejecutable

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

Puede 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 chained_func(*args, **kwargs):
# self.func está a la izquierda, other está a la derecha
return other(self.func(*args, **kwargs))
return Runnable(función_encadenada)

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

def sumar_diez(x):
devolver x + 10

def dividir_entre_dos(x):
devolver 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!

Así que, ¿por qué no prueba LCEL en su próximo proyecto?

Si quiere saber más, le recomiendo encarecidamente que eche un vistazo a Libro de cocina LangChain en LCEL.

Medio Blog por Artefact.

Este artículo se publicó inicialmente en Medium.com.
¡Síganos en nuestro Medium Blog !