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:
- 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". - 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 solicitudCuéntame un chiste sobre el helado
. - Pasar la solicitud
Cuéntame un chiste sobre el helado
almodelo
(modelo gpt4). - Pasar el resultado devuelto por el
modelo
al analizador de salidaoutput_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:
- Primero, crear un objeto
RunnableParallel
que contenga dos entradas. La primera entradacontexto
incluirá los resultados de documentos extraídos por el recuperador. La segunda entradapregunta
contendrá la pregunta original del usuario. Para pasar la pregunta, usamosRunnablePassthrough
para copiar esta entrada. - 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 unPromptValue
. - 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 objetoChatMessage
. - Finalmente, el componente
analizador_salida
toma unChatMessage
, lo convierte a una cadena en Python, y lo devuelve desde el métodoinvoke
.