¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.dev
Leticia Martín-Fuertes y Miguel López Campos 18/11/2024 Cargando comentarios…
Trabajar con IA generativa tiene sus diferencias y parecidos con los sistemas clásicos de machine learning. Por ejemplo:
A día de hoy, las dos herramientas más conocidas de LLM engineering son LangSmith y Langfuse. En esta serie de posts compararemos algunas de sus principales funcionalidades, viendo cómo funcionan en cada plataforma. Para ello usaremos Python y lo integraremos con LangChain, que, como os contamos hace unos meses, es el framework de facto para trabajar con LLMs.
Antes de empezar, hay que tener en cuenta que LangSmith requiere de licencia para poder explotarlo comercialmente, aunque hay una versión gratuita muy interesante. Por otro lado, Langfuse te permite alojarlo en tu propio servidor o también pagar licencia para no tener que preocuparte por eso. En este post, se va a usar una licencia gratuita de LangSmith y un Langfuse autoalojado.
En LangSmith, es tan sencillo como obtener las API keys de LangChain (con el que LangSmith está totalmente integrado) y de OpenAI (porque en este ejemplo vamos a usar un modelo de OpenAI) y configurar el resto de variables de entorno pertinentes:
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = # a rellenar por el usuario
os.environ["LANGCHAIN_PROJECT"] = "default"
os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
os.environ["OPENAI_API_KEY"] = # a rellenar por el usuario
En Langfuse es un poco más complicado. Hay que seguir los siguientes pasos:
>>>git clone https://github.com/langfuse/langfuse.git
>>>cd langfuse
>>>docker compose up
import os
os.environ["LANGFUSE_HOST"] = "http://localhost:3000"
os.environ["LANGFUSE_PUBLIC_KEY"] = # a rellenar por el usuario
os.environ["LANGFUSE_SECRET_KEY"] = # a rellenar por el usuario
os.environ["OPENAI_API_KEY"] = # a rellenar por el usuario
En cualquier aplicación basada en un modelo de ML, existe una etapa de experimentación para elegir el mejor modelo y una etapa de control del ciclo de vida del modelo. En un flujo MLOps, un modelo de ML es evaluado en ambas etapas y versionado según su funcionamiento, permitiendo un control de versiones de los distintos modelos a lo largo del tiempo.
Si hacemos un símil con las soluciones basadas en LLMs, el equivalente a un modelo de ML sería un prompt. Este prompt debería pasar también por una primera etapa de experimentación, para poner en producción un primer prompt, y posteriormente por una etapa de control de su funcionamiento para, en el caso de ser necesario, crear nuevos prompts o volver a versiones anteriores.
Tanto LangSmith como Langfuse permiten almacenar prompts, versionarlos y referenciarlos desde la aplicación para acceder a sus distintas versiones.
Para subir un prompt en LangSmith solo tenemos que usar el método .push_prompt() del cliente, al que tenemos que pasarle un PromptTemplate (objeto de LangChain) como object. Para ello vamos a construir un ChatPromptTemplate a partir de una plantilla para el sistema y de otra plantilla donde se insertará la pregunta del humano. Debemos darle un nombre y podemos añadir una descripción y todos los tags que queramos.
from langsmith import Client
from langchain_core.prompts import ChatPromptTemplate
system_prompt_template = """
Eres un bot que responde a preguntas según un contexto proporcionado por el usuario. El mensaje del usuario tendrá
el siguiente formato:
## CONTEXTO ##
Conocimiento que usarás para contestar a la pregunta
## PREGUNTA ##
Pregunta que debes de contestar
Debes tener en cuenta lo siguiente:
- Contesta de la forma más natural posible. No digas "según el
contexto proporcionado" o frases similares.
- Limita tu respuesta únicamente al contexto que te proporcione
el usuario.
- Si es necesario, usa información de toda la conversación para
contestar.
"""
user_prompt_template = """
## CONTEXTO ##
{contexto}
## PREGUNTA ##
{pregunta}
"""
client = Client()
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt_template),
("user", user_prompt_template)
])
client.push_prompt(
"prompt_rag",
object = prompt,
description = "Este es un prompt de prueba",
tags = ["primer_prompt", "prompt_prueba"]
)
Ahora podemos subir una nueva versión del prompt. Imagina que queremos quitarle la parte de ”Debes tener en cuenta lo siguiente:” de la plantilla del sistema:
system_prompt_template =
"""
Eres un bot que responde a preguntas según un contexto
proporcionado por el usuario. El mensaje del usuario tendrá
el siguiente formato:
## CONTEXTO ##
Conocimiento que usarás para contestar a la pregunta
## PREGUNTA ##
Pregunta que debes de contestar
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt_template),
("user", user_prompt_template)
])
client.push_prompt(
"prompt_rag",
object = prompt,
description = "Prompt para RAG",
tags = ["rag"]
)
Para recuperar el prompt, es tan sencillo como usar el método .pull_prompt(). Por defecto, cuando recuperemos el prompt se recuperará la última versión subida, pero podemos recuperar una versión anterior especificando el commit al que pertenece.
client.pull_prompt("prompt_rag")
client.pull_prompt("prompt_rag:5e12e879")
En LangSmith, la funcionalidad de versionado de prompts permite tener un histórico de los distintos prompts usados para las distintas tasks dentro de una aplicación. Este traceo de prompts permite acceder a distintas versiones de un mismo prompt, almacenarlos con tags... pero no permite estructurarlos según proyecto, por lo que hay que tenerlo en cuenta a la hora de definir su nomenclatura de manera que sea característica o indicarlo mediante tags.
En Langfuse, prácticamente solo cambia el objeto que le pasamos al método .create_prompt(). En este caso, es una lista de diccionarios con las claves role y content. Los valores de content serán las plantillas que hemos definido. Además, deberemos especificar para el prompt un nombre, para qué configuración está pensado (como el modelo, la temperatura, las lenguas soportadas…) y las labels, donde por defecto se le asignará la de production, porque es obligatoria y marca el prompt que está en producción. También se le pueden asignar tags que, a diferencia de las labels, se pueden repetir entre prompts, y un type dependiendo de si es para texto o para chat.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import HumanMessagePromptTemplate, SystemMessagePromptTemplate
from langfuse import Langfuse
langfuse = Langfuse()
system_prompt_template = """
Eres un bot que responde a preguntas según un contexto
proporcionado por el usuario. El mensaje del usuario tendrá
el siguiente formato:
## CONTEXTO ##
Conocimiento que usarás para contestar a la pregunta
## PREGUNTA ##
Pregunta que debes de contestar
Debes tener en cuenta lo siguiente:
- Contesta de la forma más natural posible. No digas "según el
contexto proporcionado" o frases similares.
- Limita tu respuesta únicamente al contexto que te proporcione
el usuario.
- Si es necesario, usa información de toda la conversación para
contestar.
"""
user_prompt_template = """
## CONTEXTO ##
{{contexto}}
## PREGUNTA ##
{{pregunta}}
"""
prompt_registry = {
"name": "prompt_rag",
"prompt": [
{"role": "system", "content": system_prompt_template},
{"role": "user", "content": user_prompt_template}],
"config": {
"model": "gpt-4o",
"temperature": 0.1,
"supported_languages": ["es"]
},
"labels": ["production", "staging", "latest"],
"tags": ["RAG"],
"type": "chat"
}
langfuse.create_prompt(
**prompt_registry
)
Para subir una nueva versión del prompt, como en Langsmith, basta con escribir la nueva, generar el objeto y volver a usar el método:
system_prompt_template = """
Eres un bot que responde a preguntas según un contexto proporcionado por el usuario. El mensaje del usuario tendrá
el siguiente formato:
## CONTEXTO ##
Conocimiento que usarás para contestar a la pregunta
## PREGUNTA ##
Pregunta que debes de contestar
"""
prompt_registry = {
"name": "prompt_rag",
"prompt": [
{"role": "system", "content": system_prompt_template},
{"role": "user", "content": user_prompt_template}],
"config": {
"model": "gpt-4o",
"temperature": 0.1,
"supported_languages": ["es"]
},
"labels": ["production", "staging", "latest"],
"tags": ["RAG"],
"type": "chat"
}
langfuse.create_prompt(
**prompt_registry
)
También se accede por defecto a la versión más nueva del prompt, pero se pueden obtener versiones anteriores:
langfuse.get_prompt("prompt_rag")
langfuse.get_prompt("prompt_rag", version = 1)
Este prompt no es un prompt de LangChain (como lo era en LangSmith), por lo que para usarlo en LangChain tendremos que convertirlo, pero es sencillo:
from langchain_core.prompts import ChatPromptTemplate
langchain_prompt = ChatPromptTemplate.from_messages(
langfuse.get_prompt("prompt_rag").get_langchain_prompt()
)
El traceo es el registro de todas las interacciones hechas con el LLM. Es interesante porque puedes ver cuánto se está usando la aplicación, si hay muchos errores, si hay un usuario en concreto que está usándola más de lo normal (lo cual es útil para detectar posibles intentos de prompt hacking), cuánto están tardando las llamadas, cuánto están costando…
También se puede "personalizar" la traza: añadir metadatos (como el nombre del LLM) en cada llamada para distinguir los usos de LLMs y hacer embeddings o métricas de aquellos que generan una respuesta y así trazar cada uso por separado, por ejemplo. Tanto Langsmith como Langfuse ofrecen este traceo.
En LangSmith basta con invocar una chain o un llm para que el traceo se realice de forma directa. Vamos a hacer el "Hola, mundo" del LLM engineering:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model = "gpt-4o")
llm.invoke("Hello, world!")
AIMessage(content='Hello! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 11, 'total_tokens': 20, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_3537616b13', 'finish_reason': 'stop', 'logprobs': None}, id='run-cf556208-5a03-4597-95b7-809623ec5a79-0', usage_metadata={'input_tokens': 11, 'output_tokens': 9, 'total_tokens': 20})
En la UI de LangSmith se ve así:
Podemos hacer algo más complejo, como cargar el cuento de Caperucita Roja en caperucita_roja.txt y preguntarle sobre él, un caso de uso que se conoce como Retrieval-Augmented Generation (RAG). Tendremos que invocar una chain, pasarle el texto troceado y la pregunta:
from langchain.chains.question_answering import load_qa_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document
with open("caperucita_roja.txt", "r") as f:
caperucita_story = f.read()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 500, chunk_overlap = 50
)
chunks = text_splitter.split_documents(
[Document(page_content = caperucita_story)]
)
langchain_prompt = client.pull_prompt("prompt_rag")
chain = load_qa_chain(
llm = llm,
chain_type = "stuff",
prompt = langchain_prompt,
document_variable_name = "contexto"
)
response = chain.invoke({
"input_documents": chunks,
"pregunta": "¿De qué color es la caperucita?"
})
Las trazas desde el portal de LangSmith se verían como en las dos siguientes figuras. Se trata de cada una de las etapas de la cadena invocada. En el interior podemos ver los fragmentos enviados al LLM y su respuesta.
Langsmith ofrece ciertas funcionalidades adaptadas a la naturaleza conversacional de muchas aplicaciones basadas en LLM, permitiendo agrupar trazas en hilos según ids de sesiones.
Langfuse, por supuesto, también permite registrar las trazas de llamadas a LLM y ofrece un detalle del tiempo de ejecución y coste de las llamadas. Para usarlo de forma integrada en LangChain, es necesario explicitar el callback handler en LangChain o bien usar el decorador observe, que hace que se observe todo lo ejecutado dentro de la función a la que se le aplica y que es la opción elegida en este ejemplo:
from langfuse.decorators import langfuse_context, observe
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains.question_answering import load_qa_chain
prompt = langfuse.get_prompt("question_and_answer")
langchain_prompt = ChatPromptTemplate.from_messages(
prompt.get_langchain_prompt()
)
@observe()
def generate_response(llm_model, query, chunks, model_kwargs):
langfuse_handler = langfuse_context.get_current_langchain_handler()
llm = ChatOpenAI(model = llm_model, **model_kwargs)
chain = load_qa_chain(
llm = llm,
chain_type = "stuff",
prompt = langchain_prompt,
document_variable_name = "contexto"
)
response = chain.invoke(
{"input_documents": chunks, "pregunta": query},
config = {"callbacks": [langfuse_handler]}
)
return response
generate_response(
"gpt-4o",
"¿De qué color es la caperucita?",
chunks,
model_kwargs = {"temperature": 0.2}
)
Las trazas se pueden ver en lista como en LangSmith y también acceder a ellas para ver el detalle.
Hasta ahora, hemos visto las principales funcionalidades en ambas plataformas. Si quieres saber cómo se gestionan los datasets y cómo se ejecutan experimentos y evaluaciones, en unos días publicaremos la segunda parte de la serie 😉.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.