Introducción a LCEL

LCEL (LangChain Expression Language) es una potente herramienta de orquestación de flujos de trabajo que te permite construir cadenas de tareas complejas a partir de componentes básicos, y soporta funciones integradas como procesamiento en streaming, procesamiento paralelo y registro de eventos.

Ejemplo básico: Prompt + Modelo + Analizador de resultados

En este ejemplo, demostraremos cómo usar LCEL (LangChain Expression Language) para enlazar tres componentes - plantilla de prompt, modelo y analizador de resultados - para formar un flujo de trabajo completo para implementar la tarea de "contar chistes". El código demuestra cómo crear cadenas, usar el símbolo de tubería | para conectar diferentes componentes e introduce el rol de cada componente junto con los resultados obtenidos.

Primero, veamos cómo conectar la plantilla de prompt y el modelo para generar un chiste sobre un tema específico:

Instala las dependencias

%pip install --upgrade --quiet langchain-core langchain-community langchain-openai
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_template("Cuéntame un chiste sobre {tema}")
model = ChatOpenAI(model="gpt-4")
output_parser = StrOutputParser()

chain = prompt | model | output_parser

chain.invoke({"tema": "helado"})

Resultado

"¿Por qué las fiestas no invitan al helado? ¡Porque se derrite cuando hace calor!"

En este código, usamos LCEL para conectar diferentes componentes en una cadena:

chain = prompt | model | output_parser

El símbolo | aquí es similar al operador de tuberías de Unix, que conecta diferentes componentes y pasa la salida de un componente como entrada al siguiente componente.

En esta cadena, la entrada del usuario se pasa a la plantilla de prompt, luego la salida de la plantilla de prompt se pasa al modelo, y finalmente la salida del modelo se pasa al analizador de resultados. Echemos un vistazo a cada componente por separado para entender mejor lo que está sucediendo.

1. Prompt

prompt es una BasePromptTemplate que acepta un diccionario de variables de plantilla y genera un PromptValue. PromptValue es un objeto envuelto que contiene el prompt, que se puede pasar a LLM (como entrada en forma de cadena) o a ChatModel (como entrada en forma de secuencias de mensajes). Se puede usar con cualquier tipo de modelo de lenguaje porque define la lógica para generar BaseMessage y generar cadenas.

prompt_value = prompt.invoke({"tema": "helado"})
prompt_value

Resultado

ChatPromptValue(messages=[HumanMessage(content='Cuéntame un chiste sobre helado')])

A continuación, convertimos el resultado con formato de prompt al formato de mensaje utilizado por los modelos de chat:

prompt_value.to_messages()

Resultado

[HumanMessage(content='Cuéntame un chiste sobre helado')]

También se puede convertir directamente a una cadena:

prompt_value.to_string()

Resultado

'Humano: Cuéntame un chiste sobre helado.'

2. Modelo

A continuación, pasa el PromptValue al modelo. En este ejemplo, nuestro modelo es un ChatModel, lo que significa que generará un BaseMessage.

Intenta llamar al modelo directamente:

mensaje = model.invoke(prompt_value)
mensaje

Devuelve:

AIMessage(content="¿Por qué el helado nunca es invitado a las fiestas?\n\n¡Porque siempre se derrite cuando las cosas se calientan!")

Si nuestro modelo está definido como un tipo LLM, generará una cadena.

from langchain_openai.llms import OpenAI

llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(prompt_value)

El modelo devuelve:

'\n\nBot: ¿Por qué se averió el camión de helados? ¡Porque sufrió un derretimiento!'

3. Analizador de resultados

Finalmente, pasa la salida de nuestro modelo al analizador de resultados, que es un BaseOutputParser, lo que significa que acepta una cadena o BaseMessage como entrada. StrOutputParser convierte específicamente cualquier entrada en una cadena simple.

output_parser.invoke(mensaje)
¿Por qué el helado nunca es invitado a las fiestas? ¡Porque siempre se derrite cuando las cosas se calientan!

4. Todo el Proceso

El proceso de ejecución es el siguiente:

  1. Llamar a chain.invoke({"topic": "helado"}), que es equivalente a iniciar el flujo de trabajo que definimos y pasar el parámetro {"topic": "helado"} para generar un chiste sobre "helado".
  2. Pasar el parámetro de llamada {"topic": "helado"} al primer componente de la cadena, prompt, que formatea la plantilla de la solicitud para obtener la solicitud Cuéntame un chiste sobre el helado.
  3. Pasar la solicitud Cuéntame un chiste sobre el helado al modelo (modelo gpt4).
  4. Pasar el resultado devuelto por el modelo al analizador de salida output_parser, que formatea el resultado del modelo y devuelve el contenido final.

Si está interesado en la salida de algún componente, puede probar una versión más pequeña de la cadena en cualquier momento, como prompt o prompt | modelo, para ver los resultados intermedios:

input = {"topic": "helado"}

prompt.invoke(input)

(prompt | modelo).invoke(input)

Ejemplo de Búsqueda RAG

A continuación, explicaremos un ejemplo un poco más complejo de LCEL. Ejecutaremos un ejemplo de recuperación mejorada para generar cadenas, con el fin de agregar información de fondo al responder preguntas.

from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings

vectorstore = DocArrayInMemorySearch.from_texts(
    ["harrison trabajó en kensho", "a los osos les gusta comer miel"],
    embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()

plantilla = """Responda la pregunta basándose solo en el siguiente contexto:
{context}

Pregunta: {question}
"""
prompt = ChatPromptTemplate.from_template(plantilla)
modelo = ChatOpenAI()
analizador_salida = StrOutputParser()

configuracion_y_recuperacion = RunnableParallel(
    {"contexto": retriever, "pregunta": RunnablePassthrough()}
)
cadena = configuracion_y_recuperacion | prompt | modelo | analizador_salida

cadena.invoke("¿Dónde trabajó Harrison?")

En este caso, la cadena compuesta es:

cadena = configuracion_y_recuperacion | prompt | modelo | analizador_salida

En resumen, la plantilla de solicitud anterior acepta contexto y pregunta como valores para reemplazar en la solicitud. Antes de construir la plantilla de solicitud, queremos recuperar documentos relevantes para usar como parte del contexto.

Como prueba, usamos DocArrayInMemorySearch para simular una base de datos vectorial basada en la memoria, definiendo un recuperador que puede recuperar documentos similares en función de consultas. Este también es un componente de ejecución encadenable, pero también puede probar su ejecución por separado:

retriever.invoke("¿Dónde trabajó Harrison?")

Luego, usamos RunnableParallel para preparar la entrada para la solicitud, buscar documentos utilizando el recuperador y pasar la pregunta del usuario utilizando RunnablePassthrough:

configuracion_y_recuperacion = RunnableParallel(
    {"contexto": retriever, "pregunta": RunnablePassthrough()}
)

En resumen, la cadena completa es:

configuracion_y_recuperacion = RunnableParallel(
    {"contexto": retriever, "pregunta": RunnablePassthrough()}
)
cadena = configuracion_y_recuperacion | prompt | modelo | analizador_salida

El proceso es el siguiente:

  1. Primero, crear un objeto RunnableParallel que contenga dos entradas. La primera entrada contexto incluirá los resultados de documentos extraídos por el recuperador. La segunda entrada pregunta contendrá la pregunta original del usuario. Para pasar la pregunta, usamos RunnablePassthrough para copiar esta entrada.
  2. Pasar el diccionario del paso anterior al componente prompt. Acepta la entrada del usuario (es decir, pregunta) así como los documentos recuperados (es decir, contexto), construye una solicitud y produce un PromptValue.
  3. El componente modelo toma la solicitud generada y la pasa al modelo LLM de OpenAI para su evaluación. La salida generada por el modelo es un objeto ChatMessage.
  4. Finalmente, el componente analizador_salida toma un ChatMessage, lo convierte a una cadena en Python, y lo devuelve desde el método invoke.