¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Langchain
Langchain es un framework (biblioteca) disponible en Python y Javascript para facilitar el desarrollo de aplicaciones que integren modelos de lenguaje.
Las principales características de langchain son:
- Chains: encadenamiento de llamadas al modelo de IA.
- Memoria: para que el modelo “recuerde” el contexto y la conversación mantenida anteriormente.
- Interacción con fuentes de datos externas.
- Parsing de salida: permite obtener respuestas estructuradas para usar con programación tradicional.
- Agentes: sistemas automatizados que pueden interactuar con el entorno y tomar decisiones usando modelos de lenguaje.
La información acerca de langchain y uso se puede encontrar en su web.
Modelos, Prompts y Parsers de salida
Modelos
En primer lugar hay que tener el modelo correspondiente instalado y ejecutándose.
Lo modelos disponibles para funcionar con langchain están en https://python.langchain.com/docs/integrations/chat/
En mi caso trabajo habitualmente con Ollama en local, usando el modelo Llama 3.2 de 3 mil millones de parámetros, uno de los modelos pequeños que no son multimodales. Por ello, haré los ejemplos usando éste.
# Cargamos las librerías de langchain del modelo from langchain_ollama import ChatOllama from langchain_core.prompts import ChatPromptTemplate # Instanciamos el chat del modelo definiendo sus opciones chat = ChatOllama ( model = "llama3.2", temperature = 0.1 )
Plantillas
Las plantillas definen prompts por defecto que tienen algunas “variables” { entre llaves }, a las que se les puede asignar diferentes valores. De este modo, puede usarse para tareas recurrentes.
prompt = """Quiero que sugieras una receta \ de cocina para {hora_comida} que tenga al menos \ los siguientes ingredientes: {ingredientes}""" # Definimos la plantilla de prompt plantilla_prompt = ChatPromptTemplate.from_template(prompt) # Como el programa va de obtener recetas, vamos a incluir en las variables un código numérico # que indique la fecha para la que estamos calculando la receta hora_comida_111024 = "comer" # Otras posibilidades: cenar, almorzar, merendar, desayunar ingredientes_111024 = "arroz, curry, pollo" # Creamos el mensaje a pasar al modelo, indicando en la plantilla las variables correspondientes mensaje = plantilla_prompt.format_messages( hora_comida = hora_comida_111024, ingredientes = ingredientes_111024) # Obtenemos y visualizamos la receta respuesta = chat.invoke(mensaje) receta_111024 = respuesta.content print(receta_111024)
Parseo de salida
Se entiende por parser la capacidad de devolver respuestas estructuradas, con las que python pueda trabajar en programación tradicional, por ejemplo en formato JSON o como listas o diccionarios.
Langchain tiene sus propios parsers que pueden consultarse en su documentación, por ejemplo para extraer información del formato JSON:
from langchain_core.prompts import ChatPromptTemplate # En primer lugar podríamos hacer que el modelo nos diese directamente una cadena de # texto //string// en formato JSON y convertirla a través de un parser predefinido para # este tipo de archivos, en una variable de tipo diccionario import JSON plantilla_instrucciones = """\ De la siguiente receta extrae la siguiente información y no indiques nada más: nombre: extrae el título de la receta. ingredientes: extrae los ingredientes de la receta como una lista de python. pasos: extrae el número de pasos necesarios para cocinar la receta. comensales: extrae el número de comensales para los que está preparada la receta, si no se especifica, indicar -1. Formatea la salida como JSON con las siguientes keys, sólo el contenido del JSON: nombre ingredientes pasos comensales receta = {receta} """ # Suponemos que tenemos la variable receta_111024 del apartado anterior, copiarla para la realización del ejemplo. # Trabajamos como vimos en el apartado anterior plantilla_prompt = ChatPromptTemplate.from_template(plantilla_instrucciones) mensaje = plantilla_prompt.format_messages(receta=receta_111024) chat = ChatOllama ( model = "llama3.2", temperature = 0.0 ) respuesta = chat.invoke(mensaje) datos_json = respuesta.content try: dato_dict = json.loads(datos_json) print(dato_dict) print(type(dato_dict)) print(dato_dict["ingredientes"]) print(type(dato_dict["ingredientes"])) except json.JSONDecodeError: print("ERROR: No es un formato JSON válido") except Exception as e: print(f"Error inesperado: {e}")
Puede que en ocasiones necesitemos crear nuestros parsers personalizados, que se harían usando la biblioteca de langchain output_parsers
from langchain_core.prompts import ChatPromptTemplate from langchain.output_parsers import ResponseSchema from langchain.output_parsers import StructuredOutputParser # De nuevo, suponemos que tenemos la variable receta_111024 del apartado anterior, copiarla para la realización del ejemplo. # Definimos la estructura que tendrá el JSON, a patir de la información contenida en el texto de la receta esquema_nombre = ResponseSchema(name = "nombre", description = "Nombre de la receta.") esquema_ingredientes = ResponseSchema(name = "ingredientes", description = "Ingredientes necesarios para cocinar la receta.") esquema_pasos = ResponseSchema(name = "pasos", description = "Número de pasos necearios para cocinar la receta.") esquema_comensales = ResponseSchema(name = "comensales", description = "Número de comensales para los que está preparada la receta.\ Si no se especifican, el valor es -1.") esquema_respuesta = [esquema_nombre, esquema_ingredientes, esquema_pasos, esquema_comensales] # Una vez tenemos el esquema del JSON definido, creamos el parser de salida parser_de_salida = StructuredOutputParser.from_response_schemas(esquema_respuesta) instrucciones_de_formato = parser_de_salida.get_format_instructions() # Definimos las instrucciones para extraer la información, a partir del esquema que hemos hecho plantilla_instrucciones = """\ De la siguiente receta extrae la siguiente información: nombre: extrae el título de la receta. ingredientes: extrae los ingredientes de la receta como una lista de python. pasos: extrae el número de pasos necesarios para cocinar la receta. comensales: extrae el número de comensales para los que está preparada la receta, si no se especifica, indicar -1. receta = {receta} {instrucciones_de_formato} """ # Creamos como hemos hecho antes, la plantilla del prompt y el mensaje a enviar al modelo prompt = ChatPromptTemplate.from_template(template=plantilla_instrucciones) mensaje = prompt.format_messages(receta=receta_111024, instrucciones_de_formato = instrucciones_de_formato) respuesta = chat.invoke(mensaje) # Por último pasamos a una variable el parseo de la respuesta obtenida, ya que gracias # al esquema que hicimos en un principio, puede interpretar de la forma adecuada, que en # este caso será un diccionario Python, en el que además el elemento "ingredientes" es una lista diccionario_de_salida = parser_de_salida.parse(respuesta.content) print(diccionario_de_salida) print (type(diccionario_de_salida)) print(diccionario_de_salida.get("ingredientes")) print(type(diccionario_de_salida.get("ingredientes")))
Memoria
El modo en que se gestiona la memoria en este apartado a través de langchain, está obsoleto Deprecated.
Se debe de hacer a través de LangGraph, pero como esto escapa al objetivo actual (aprender a manejar langchain), se hará de este modo.
Si a un modelo le hacemos varias consultas seguidas, al responder cada una, no recuerda qué respondió las anteriores, por lo que es complicado realizar con él una conversación coherente.
from langchain_ollama import ChatOllama chat = ChatOllama( model = "llama3.2", temperature = 0.0, verbose = False ) respuesta = chat.invoke("Hola, soy Alberto") print(respuesta.content) respuesta = chat.invoke("¿Sabes cómo me llamo?") print(respuesta.content)
Buffer de conversación
Cuando instanciamos la clase ConversationBufferMemory, en ésta se guarda todo el historial de conversación.
Si le pasamos esta información con una plantilla de prompt en cada iteración, le estaremos proporcionando al modelo una memoria a corto plazo a costa de una gran cantidad de tokens.
from langchain_ollama import ChatOllama from langchain.memory import ConversationBufferMemory from langchain.chains.llm import LLMChain from langchain.prompts import PromptTemplate chat = ChatOllama( model = "llama3.2", temperature = 0.0, verbose = False ) # Definimos el prompt con una plantilla, en el prompt especificaremos el historial # de la conversación (transcripción) que pasaremos en cada nueva llamada prompt = PromptTemplate( input_variables=["historial", "input"], template="{historial}\nUser: {input}\nAssistant:", ) # Creamos la memoria de la conversación memoria = ConversationBufferMemory(memory_key="historial", return_messages=True) # Cadena LLMChain con el modelo, el prompt y la memoria, hay que hacerlo así chat_chain = LLMChain( llm = chat, prompt=prompt, memory=memoria, verbose=False ) # Obtenemos las respuestas del modelo respuesta = chat_chain.run("Hola, soy Alberto") #print(respuesta) respuesta = chat_chain.run("¿Cuánto es 1+1?") #print(respuesta) respuesta = chat_chain.run("¿Cómo me llamo?") #print(respuesta) # Podemos añadir contexto a la memoria memoria.save_context( {"input":"Hola"}, {"output":"Qué pasa"} ) # Muestro la conversación completa print(memoria.buffer_as_str)
Ventana del Buffer de conversación
Podemos seleccionar el número de interacciones (pregunta-respuesta) que queremos que el modelo “recuerde” en el buffer instanciando la clase ConversationBufferWindowMemory.
from langchain.memory import ConversationBufferWindowMemory # Instanciamos la memoria con una ventana de contexto de 2 iteraciones: de las tres que hay, no recordará la primera memoria = ConversationBufferWindowMemory(k = 2) # En lugar de preguntar a un modelo, le introducimos las interacciones directamente en memoria memoria.save_context( {"input":"Hola"}, {"output":"Cómo lo llevas"} ) memoria.save_context( {"input":"Colgando, ligeramente a la izquierda"}, {"output":"Eso está bien"} ) memoria.save_context( {"input":"Qué hay de tu vida"}, {"output":"Sobreviviendo, cuando me dejan"} ) print(memoria.load_memory_variables({}))
Ventana de tokens del buffer de conversación
Del mismo modo que hicimos anteriormente, podemos limitar la cantidad de información que recuerda el modelo, pero limitando el número de tokens.
De esta manera se recordarán los últimos tokens en el número definido en la clase ConversationTokenBufferMemory
# CUARTO EJEMPLO: Definimos el número máximo de tokens a recordar # Se necesita instalar el módulo "tiktoken": pip install tiktokenpip # Tembién he tenido que instalar el módulo "transformers": pip install transformers from langchain.memory import ConversationTokenBufferMemory from langchain_ollama import ChatOllama chat = ChatOllama( model = "llama3.2", temperature = 0.0, verbose = False ) # Definimos la memoria, indicando el modelo y el número máximo de tokens a recordar memoria = ConversationTokenBufferMemory(llm=chat, max_token_limit=25) memoria.save_context ( {"input":"¿Qué es la IA?"}, {"output":"¡La IA mola!"} ) memoria.save_context ( {"input":"¿Tiene memoria?"}, {"output":"En eso estamos..."} ) memoria.save_context ( {"input":"¿Recordarás?"}, {"output":"Si no se me olvida"} ) print(memoria.load_memory_variables({}))
Resumen de conversación
Con el fin de “gastar” menos tokens evitando “arrastrar” toda la conversación de una interacción a otra con el modelo, la clase ConversationSummaryBufferMemory nos permite guardar en un buffer un resumen de la conversación, cuyo tamaño en tokens podemos especificar.
from langchain_ollama import ChatOllama from langchain.memory import ConversationSummaryBufferMemory from langchain.chains.llm import LLMChain from langchain.prompts import PromptTemplate # Definimos el chat del modelo chat = ChatOllama( model = "llama3.2", temperature = 0.0, verbose = False # Podemos añadir como parámetro 'num_ctx' para cambiar el nº de tokens de contexto, por defecto 2048. El modelo Llama 3.2 de 3B (3 mil millones de parámetros), tiene una ventana total de 128k. ) # Texto con el que vamos a trabajar with open('articulo_eureka_pequeño.txt', 'r') as articulo: texto = articulo.read() # Definimos la memoria memoria = ConversationSummaryBufferMemory(llm=chat, max_token_limit=100) # Si no se especifica max_token_limit, éste es de 2000 # Añadimos contexto a la conversación, incluído el artículo a resumir memoria.save_context ( {"input":"Hola, cómo estás"}, {"output":"Bien, gracias"} ) memoria.save_context ( {"input":"Me gustan las cosas del espacio:"}, {"output":"También a mí"} ) memoria.save_context ( {"input":"Podrías darme el contenido del artículo del blog 'Eureka':"}, {"output":f"{texto}"} ) #print(memoria.load_memory_variables({})) #print("---") # Creamos el prompt sólo con la entrada prompt = PromptTemplate( input_variables=["input"], template="{input}" ) # Cadena (chain) de conversación conversacion = LLMChain( llm = chat, prompt = prompt, memory=memoria, verbose=True ) respuesta = conversacion.invoke("¿Qué problema producen las megaconstelaciones de satélites?") print(respuesta) #print("---") #print(memoria.load_memory_variables({}))
Chains
Chain simple
Un chain o “cadena” es el componente básico de langchain y pueden verse como “eslabones” de una cadena, en el que cada uno de ellos tiene una funcionalidad.
Por ejemplo, el chain más básico entrega a un modelo una pregunta (pueden usarse plantillas de prompts) y éste devuelve una respuesta.
Pueden combinarse diferentes, de forma que la salida de uno sea la entrada del siguiente y de este modo crear flujos más o menos complejos.
A continuación un ejemplo de un chain simple.
# Importamos dependencias # Cuando no son plantillas estáticas, si no que pueden variar como resultado de una # conversación de chat, usamos 'ChatPromptTemplate', en lugar de 'PromptTemplate' from langchain_ollama import ChatOllama from langchain.prompts import ChatPromptTemplate from langchain.chains.llm import LLMChain # Definimos el modelo con el que trabajaremos modelo = ChatOllama( model = "llama3.2:1b", temperature = 0.9, verbose = False ) #Definimos el prompt prompt = """Escribe SÓLO UN título para una novela \ del género: {genero}.""" plantilla_prompt = ChatPromptTemplate.from_template(prompt) # Definimos el "chain" o el eslabón de cadena, a la que pasamos cadena = LLMChain(llm=modelo, prompt=plantilla_prompt) # Chain básico: pasamos una pregunta al modelo y éste contesta # Ya sólo nos queda definir los datos de entrada y "correr" el preograma genero = "terror" titulo = cadena.run(genero) print(titulo)
Chain compuesto
La unión de diferentes chains, puede dar lugar a flujos, de modo que la salida de uno sea la entrada de otro.
A continuación vemos un ejemplo de hasta 4 chains seguidos.
# A partir de la transcripción de una reunión entre 3 profesores que pasamos en un archivo txt, realizamos varias acciones: # 1. Chain 1: Resumimos el contenido de la transcripción. # 2. Chain 2: Lo pasamos a inglés. # 3. Chain 3: El director responde en inglés a los profesores que mantuvieron la reunión. # 4. Chain 4: Se traduce a español la respuesta del director. # En primer lugar cargamos las dependencias from langchain_ollama import ChatOllama from langchain.prompts import ChatPromptTemplate from langchain.chains.llm import LLMChain from langchain.chains.sequential import SequentialChain import pprint # Definimos el modelo con el que trabajaremos modelo = ChatOllama( model = "llama3.2", temperature = 0.9, verbose = False ) # La transcripción se puede poner aquí, aunque también se podría poner al final si el código se va a reutilizar #with open("transcripcion_reunion.txt", 'r') as transcripcion: # reunion = transcripcion.read() # Primera plantilla de prompt: Crea un resumen a partir de la transcripción de la reunión prompt1 = ChatPromptTemplate.from_template( "Crea un resumen de la reunión mantenida por tres profesores, \ de la cual tenemos la siguiente transcripción: " "\n\n{reunion}" ) # Eslabón 1: input = transcripcion de la reunión, output = acta eslabon1 = LLMChain(llm=modelo, prompt=prompt1, output_key="resumen_es") # Segundo prompt: Traducción del resumen de la reunión del español al inglés prompt2 = ChatPromptTemplate.from_template( "Traduce el siguiente texto al inglés: " "\n\n{resumen_es}" ) # Segundo eslabón: input = resumen en español, output = resumen en inglés eslabon2 = LLMChain(llm=modelo, prompt=prompt2, output_key="resumen_en") # tercer prompt: Respuesta del director en inglés prompt3 = ChatPromptTemplate.from_template( "El director del instituto, que habla inglés, lee el resumen de la reunión mantenida por los profesores: " "\n\n{resumen_en}\n" "Escribe la respuesta, en inglés, del director a los profesores explicando si le parece bien o no el cambio." ) # tercer eslabón: input = resumen en inglés, output = respuesta en inglés eslabon3 = LLMChain(llm=modelo, prompt=prompt3, output_key="respuesta_en") # cuarto prompt: Traducción de la respuesta del inglés al español prompt4 = ChatPromptTemplate.from_template( "Traduce el siguiente texto del inglés a español: " "\n\n{respuesta_en}" ) # cuarto eslabón: input = acta en español, output = acta en inglés eslabon4 = LLMChain(llm=modelo, prompt=prompt4, output_key="respuesta_es") # Creamos el flujo, esto es la cadena total uniendo todos los eslabones cadena = SequentialChain( chains = [eslabon1, eslabon2, eslabon3, eslabon4], input_variables=["reunion"], output_variables=["resumen_es", "resumen_en", "respuesta_en", "respuesta_es"], verbose=True ) # Obtenemos la transcripción de la reunión with open("transcripcion_reunion.txt", 'r') as transcripcion: reunion = transcripcion.read() # Ejecutamos el flujo, pasando al flujo creado la entrada (trnacripción), y obtenemos todas las salidas intermedias y final contestacion = cadena(reunion) pprint.pprint(contestacion)
Cadena ruteada
En este caso la cadena tendrá diferentes ramas en paralelo y el sistema deberá rutear el flujo por una o por otra, en función del contexto, según los datos de entrada.
Vamos a ver un ejemplo de cadena con ruteo. Para construir el flujo, seguimos los sigientes pasos:
- Creamos un diccionario en que cada elemento tiene el nombre del prompt correspondiente a una de las ramas en paralelo, y el contenido es un chain básico que responderá a la entrada.
- Definimos un chain básico “por defecto”, por si la entrada no se ajusta a ninguna de las “ramas” que especificaremos, la entrada sea procesada por él.
- Definimos una “plantilla multiprompt”, en la que además de la entrada, explicamos el contexto e introducimos una lista de los prompts candidatos. Según este contexto, el modelo correspondiente deberá qué porompt candidato elegir (diccionario creado anteriormente). Importante: debe devolver la respuesta en JSON en el que se indica el nombre.
- Creamos el prompt de ruteo a partir de la plantilla anterior, especificamos la entrada (en nuestro caso será “input”) y especificamos el parser de salida, que tendrá que ser JSON.
- Creamos el “router chain” especificando el modelo que se usará para ejecutar el prompt anterior.
- Creamos el flujo final, que es un “Multiprompt chain” en el que especificamos: El “router chain” anterior para decidir qué “cadena” ejecutar, el diccionario con los diferentes chains básicos a ejecutar cuyo nombre debe corresponderse con los del JSON que genera el “router chain” y el “chain” básico por defecto.
- Ejecutamos el flujo anterior, que debe devolver una respuesta en la que podemos ver los pasos intermedios.
# Como ejemplo, tendremos como entrada una consulta a una empresa de # informática y electrónica que se ocupa tanto de servicio técnico # como de venta de artículos. El sistema deberá pasar la consulta # al departamento técnico o comercial, según proceda from langchain_ollama import ChatOllama from langchain.prompts import ChatPromptTemplate from langchain.prompts import PromptTemplate from langchain.chains.llm import LLMChain from langchain.chains.router import MultiPromptChain from langchain.chains.router.llm_router import LLMRouterChain, RouterOutputParser import pprint # Definimos el modelo con el que trabajaremos # En este caso una temperatura alta nos entorpece la salida, que debe de ser # un JSON. Al tener una temperatura alta, el modelo se vuelve "parlanchin" y # añade texto explicativo innecesario que genera un error modelo = ChatOllama( model = "llama3.2", temperature = 0.0, verbose = False ) # Definimos las plantillas de los distintos departamentos plantilla_comercial = """Eres un simpático comercial, comprensivo, empático \ y divertido. Estás encantado de responder a las consultas que te formulan \ los clientes, siempre encuentras una salida a las preguntas difíciles y \ sueles conseguir vender lo que te propones. Responde a la siguiente consulta de un cliente: {input}""" plantilla_tecnico = """Eres un técnico informático con conocimientos avanzados \ de electrónica. Eres serio, conciso, y que le gusta ir al grano. \ Respondes a las cuestiones técnicas que te plantean los clientes de forma profesional, \ y precisa. Sabes adaptar las explicaciones cuando el cliente parace tener pocos \ conocimientos técnicos. Responde a la siguiente consulta de un cliente: {input}""" # Definimos una lista con información de los departamentos info_departamentos = [ { "nombre": "comercial", "descripcion": "Bueno para contestar a preguntas acerca de cuestiones comerciales", "plantilla": plantilla_comercial }, { "nombre": "tecnico", "descripcion": "Bueno para contestar a preguntas técnicas de usuarios sin conocimientos de informática o electrónica", "plantilla": plantilla_tecnico } ] # 1. Ahora definimos la variable "ramal" que será un diccionario en el que cada elemento se corresponde # con uno de los posibles ramales de ruteo. Cada elemento tendrá como nombre el especificado en # info_departamentos, y su contenido será un chain de tipo LLMChain con la plantilla correspondiente # y el modelo de IA a usar ramales = {} for info_dpto in info_departamentos: nombre = info_dpto["nombre"] plantilla = info_dpto["plantilla"] prompt = ChatPromptTemplate.from_template(template=plantilla) eslabon = LLMChain(llm=modelo, prompt=prompt) # El objetivo del bucle es crear este chain y ... ramales[nombre] = eslabon # ... meterlo en una lista donde cada elemento son los posibles chains en paralelo # destinos: lista (también otra variable destinos_str tipo cadena de caracteres) que tiene en cada # elemento cadena "nombre: descripción" de cada departamento destinos = [f"{p['nombre']}:{p['descripcion']}" for p in info_departamentos] destinos_str = "\n".join(destinos) #pprint.pprint(destinos) #pprint.pprint(destinos_str) # 2. Definimos un prompt "por defecto" para usar si el sistema no identifica la consulta con ninguno # de los dos departamentos default_prompt = ChatPromptTemplate.from_template("{input}") default_chain = LLMChain(llm=modelo, prompt=default_prompt) # 3. Escribimos el prompt que seleccionará el ruteo (ramal) correspondiente plantilla_multiprompt_ruteo = """Dado un texto sin procesar correspondiente a una consulta \ para una empresa de informática y electrónica, selecciona la opción que mejor se adapte a \ la naturaleza de dicha consulta. Se proporcionarán los nombres \ de las opciones disponibles y una descripción para discernir cuál es la más adecuada. \ También puedes revisar la entrada original si crees que al revisarla, se obtendrá \ una mejor respuesta del modelo. <<FORMATO>> Devuelve SÓLO y ÚNICAMENTE un código en formato JSON de la siguiente manera: '''json {{{{ "destination": string \ nombre del mensaje a utilizar o "DEFAULT" "next_inputs": string \ una versión potencialmente modificada de la entrada original }}}} ''' RECUERDA: "destination" DEBE ser uno de los nombres candidatos especificados a continuación \ O puede ser "DEFAULT" si el mensaje no es adecuado para ninguno de los mensajes candidatos. RECUERDA: "next_inputs" puede ser simplemente el mensaje original si no crees \ que se necesiten modificaciones. RECUERDA: Devuelve SÓLO y ÚNICAMENTE el output en formato JSON <<PROMPTS CANDIDATOS>> {destinos} <<INPUT>> {{input}} <<OUTPUT>> """ # Sustituimos en la plantilla multiprompt, {destinos} por la cadena de destinos plantilla_ruteo = plantilla_multiprompt_ruteo.format(destinos=destinos_str) # 4. Creamos el prompt de ruteo a partir de las plantilla multiprompt prompt_ruteo = PromptTemplate( template=plantilla_ruteo, input_variables=["input"], output_parser=RouterOutputParser(), ) # 5. Creamos el "chain" de tipo "router chain", especificando modelo y prompt que define el ruteo cadena_ruteo = LLMRouterChain.from_llm(modelo,prompt_ruteo) # 6. Creamos el flujo completo definiendo los posibles destinos cadena_consulta = MultiPromptChain(router_chain=cadena_ruteo, destination_chains=ramales, default_chain=default_chain, verbose=True ) # 7. Ejecutamos #respuesta = cadena_consulta.run("Buenos días, tras cambiar algunos componentes del PC, no arranca y emite un pitido intermitente ¿Qué puedo hacer?") #respuesta = cadena_consulta.run("Buenas tardes, quiero saber qué ordenador necesito para trabajar. Sobre todo, mirar el correo, manejar programas ofimáticos y navegar port internet ¿Cuánto puede costar?") respuesta = cadena_consulta.run("Buenos días, me pongo en contacto con ustedes desde un distribuidor de componentes electrónicos. Nos gustaría concertar una cita para enseñarles nuestro catálogo y poder hacer negocios juntos.") pprint.pprint(respuesta)
Preguntas y respuestas acerca de documentos
Vamos a ver el modo en que los LLM gestionan la información de documentos, de modo que podamos preguntar a un modelo acerca de la información contenida en los mismos.
Para ello aprenderemos qué son los embeddings, concepto muy importante relacionado con la forma en que los modelos guardan la información, y veremos diferentes estrategias para guardar la información contenida en un documento.
De momento, no se va a documentar ningún caso práctico, ya que toda esta información está extraída del curso de Langchain “Langchain for Application Development” en DeepLearning.Ai. que si bien en el momento de su salida debía de ser exitoso, en la actualidad (diciembre 2024) está parcialmente obsoleto en este apartado.
He implementado los ejemplos que ofrece, y aunque se ejecutan sin dificultad, los resultados son erróneos y parece que no es capaz de relacionar bien el contenido de un documento CSV con lo que le pregunto. Por ello la parte práctica quedará pendiente.
En cuanto a la parte teórica es de gran valor, ya que es común a todos los LLM y es lo que pasaremos a ver.
Embeddings
Los modelos de lenguaje, o LLM, sólo pueden examinar unos pocos miles de palabras de una vez, por lo que tenemos que buscar una solución para documentos que sean largos.
Para almacenar la información de los documentos usamos embeddings. Los embeddings crean representaciones numéricas de partes de texto. Estas representaciones, o vectores numéricos, capturan de alguna manera el significado de estas partes de texto, de forma que textos con contenidos similares, tendrán vectores similares.
En el ejemplo de la imagen, los dos primeros casos hablan de mascotas, mientras que el tercero habla de un coche, por lo que los vectores entre los dos primeros son similares (cerca espacialmente, dado que son vectores), mientras que el tercero es muy diferente. Ya intuimos que esto se usa para realizar búsquedas de información.
Base de datos de vectores
A la base de datos de vectores le pasamos los vectores resultantes de aplicar los embeddings al texto de nuestros documentos.
Si los documentos son muy grandes, hay que “trocearlos” en partes más pequeñas para ayudar al modelo a procesar la información (tienen una capacidad limitada de cantidad de tokens a procesar de una vez). Se pasarán al modelo los “trozos” de información más relevantes para la consulta realizada. Estos trozos de información, son habitualmente denominados “documentos”.
Para pasar al modelo las partes más importantes del texto que requiera la consulta, necesitaremos crear un índice index que busque entre los vectores de las diferentes porciones de información.
La consulta se pasa a un vector a través de un embedding, y se compara con los diferentes vectores de cada porción de información. Se devuelven al modelo los más parecidos para poder devolver la respuesta definitiva.
Métodos de extracción de información
Hay varios métodos a partir de los cuales se puede extraer la información más importante (la más similar vectorialmente) de las diferentes partes en que se dividió la información.
Stuff
Este método es el más sencillo y consiste en pasarle TODA la información a la consulta como contexto y de ahí al modelo.
- Pros: Una única llamada al LLM. Se tiene acceso a todos los datos.
- Contras: Si hay muchos datos, puedes salirte de la ventana de contexto.
Es el método más usado, junto con Map reduce.
Map reduce
Cada trozo de texto se pasa junto con la consulta a un LLM que da respuestas individuales, y cada una de estas pasa a otro LLM que resume todas las respuestas y da una respuesta final.
Es muy potente y permite la inspección de muchos “documentos” (porciones de información), pero e hacen muchas llamadas a LLM. Además, no siempre se puede tratar a los “documentos” de forma independiente.
Es el método más usado, después de Stuff. También viene bien para realizar resúmenes.
Refine
Va construyendo la respuesta en función del “documento” (porción de información) anterior.
Muy bueno para combinar información e ir construyendo una respuesta en el tiempo.
Da lugar a respuestas largas, pero es muy lento.
Map rerank
Se llama al LLM por cada documento, y éste asigna una puntuación por la relevancia del mismo para la consulta. Se selecciona e más adecuado y a partir de él se genera la respuesta final.
Es algo experimental, tiene que hacer muchas llamadas, pero es más rápido que Refine.
Evaluación
Es importante saber cómo evaluar una aplicación con modelos LLM, para poder medir de algún modo con qué modelo funciona mejor, o con que retriever, o cualquier otro cambio.
Estas aplicaciones que usan LLM, en realidad son “chains” con concatenaciones de éstos, y es posible usar a su vez modelos LLM para evaluar nuestra aplicación.
Evaluación manual
Consiste en crear por mi parte unos ejemplos de pregunta - respuesta, y después hago que el modelo cree otros ejemplos de forma automática.
La idea es que el modelo responda a estas preguntas y ver si su respuesta es correcta: en las pruebas que he hecho deben ser igual que las mías, y en las generadas de forma automática, deben ser correctas.
Partimos del ejempo en el que obtenemos los datos de un CSV:
# Ejemplo en el que cargaremos información de un documento CSV y le preguntaremos al modelo # cuestiones acerca de esta información from langchain_ollama import ChatOllama from langchain_ollama import OllamaEmbeddings # Modelo embedding para crear las representaciones numéricas de los datos. from langchain.chains.retrieval_qa.base import RetrievalQA # Sirve para recuperar información de documentos from langchain_community.document_loaders import CSVLoader # Para cargar informaciónd de documentos CSV from langchain_community.vectorstores import DocArrayInMemorySearch # Forma de guardar la información en un "vector store" ó almacén de datos. Muy sencilla y no requiere conexión a base de datos from langchain.indexes import VectorstoreIndexCreator # Para crear un índice que nos ayudará a crear un almacén de datos de forma fácil import pprint # Cargamos el documento CSV file = 'profesores.csv' loader = CSVLoader(file_path=file, encoding="utf-8") data = loader.load() # Especificamos el modelo que generará los "embeddings" embeddings = OllamaEmbeddings (model="llama3.2") # Creamos el índice especificando su clase (tipo de almacén de datos) y la lista de "loaders" de donde extraer la información, que en nuestro caso sólo hay uno index = VectorstoreIndexCreator (vectorstore_cls=DocArrayInMemorySearch, embedding=embeddings).from_loaders([loader]) # Definimos el modelo con el que vamos a trabajar llm = ChatOllama( model = "llama3.2", temperature = 0.3, verbose = True ) # Definimos la interfaz con el vectorstore qa = RetrievalQA.from_chain_type( llm = llm, chain_type="stuff", retriever=index.vectorstore.as_retriever(), verbose=True, chain_type_kwargs={ "document_separator": "<<<<>>>>", } )
A continuación realizamos la comprobación “manual”, para lo cual:
- Generamos nuestros propios ejemplos.
- Usamos el chain QAGenerateChain que genera ejemplos de pregunta - respuesta a partir del LLM indicado.
- Añadimos a nuestros ejemplos hechos de forma manual, los generados automáticamente.
- A partir del retriever (RetrievarQA), ejecutamos las consultas de todos los ejemplos.
- Mostramos las respuestas para comprobar si el modelo a resuelto las consultas correctamente, como era esperado.
# Evaluación manual: Creo unos ejemplos de pregunta - respuesta y después creo otros de forma automática con LLM. # Hago que el modelo responda a la misma pregunta (mía o de las que ha creado él) para ver directamente si acierta o no. ejemplos = [ { "query": "¿Qué clase impartió Ana María de Santiago Nocito \ el 2/15/2024?", "answer": "RCP básica con DESA y fármacos." }, { "query": "¿Cuántas clases ha impartido en total Luis España Barrio?", "answer": "Luis España Barrio ha impartido 11 clases." } ] from langchain.evaluation.qa import QAGenerateChain import langchain langchain.debug = True chain_generador_ejemplos = QAGenerateChain.from_llm(llm) nuevos_ejemplos = chain_generador_ejemplos.apply_and_parse( [{"doc": t} for t in data[:5]] ) ejemplos += nuevos_ejemplos pprint.pprint(ejemplos[0]) #respuesta=qa.run(ejemplos[0]["qa_pairs"]["query"]) respuesta=qa.run(ejemplos[0]["query"]) langchain.debug = False pprint.pprint(respuesta)
Evaluación automática
Podemos programar la automatización de la comprobación de las respuestas.
Partimos de nuevo de la carga del documento CSV y la creación del vectorstore y el retrieval:
# Ejemplo en el que cargaremos información de un documento CSV y le preguntaremos al modelo # cuestiones acerca de esta información from langchain_ollama import ChatOllama from langchain_ollama import OllamaEmbeddings # Modelo embedding para crear las representaciones numéricas de los datos. from langchain.chains.retrieval_qa.base import RetrievalQA # Sirve para recuperar información de documentos from langchain_community.document_loaders import CSVLoader # Para cargar informaciónd de documentos CSV from langchain_community.vectorstores import DocArrayInMemorySearch # Forma de guardar la información en un "vector store" ó almacén de datos. Muy sencilla y no requiere conexión a base de datos from langchain.indexes import VectorstoreIndexCreator # Para crear un índice que nos ayudará a crear un almacén de datos de forma fácil import pprint # Cargamos el documento CSV file = 'profesores.csv' loader = CSVLoader(file_path=file, encoding="utf-8") data = loader.load() # Especificamos el modelo que generará los "embeddings" embeddings = OllamaEmbeddings (model="llama3.2") # Creamos el índice especificando su clase (tipo de almacén de datos) y la lista de "loaders" de donde extraer la información, que en nuestro caso sólo hay uno index = VectorstoreIndexCreator (vectorstore_cls=DocArrayInMemorySearch, embedding=embeddings).from_loaders([loader]) # Definimos el modelo con el que vamos a trabajar llm = ChatOllama( model = "llama3.2", temperature = 0.3, verbose = True ) # Definimos la interfaz con el vectorstore qa = RetrievalQA.from_chain_type( llm = llm, chain_type="stuff", retriever=index.vectorstore.as_retriever(), verbose=True, chain_type_kwargs={ "document_separator": "<<<<>>>>", } )
Ahora vamos a realizar la comprobación automática del software:
- En primer lugar volveremos a generar nuestros propios ejemplos.
- Volvemos a generar de forma automática ejemplos pregunta-respuesta con QAGenerateChain
- Ahora deberíamos juntar todos los ejemplos, como necesitamos que tengan un formato concreto, lo ajustamos.
- A partir del retriever, usamos la función apply(), que ejecuta las consultas ('query') que le pasamos como argumento, de forma que devuelve por cada una un diccionario con 3 elementos:
- query: consulta realizada.
- answer: respuesta que se generó anteriormente, o que hicimos nosotros de forma manual.
- result: respuesta gnerada ahora por el modelo especificado.
- Ahora con la función evaluate() del chain QAEvalChain le metemos los datos obtenidos, el LLM especificado compara los answer con los result, determinando que la respuesta es correcta correct si son los mismos, o incorrecta incorrect si son diferentes.
# Evaluación automática: Creo unos ejemplos de pregunta - respuesta y después creo otros de forma automática con LLM. # El modelo responda a la mismas preguntas (mías o de las que ha creado él) y deberá clasificar las respuestas en correctas o incorrectas. # Creo mis propios ejemlos de forma manual ejemplos = [ { "query": "¿Qué clase impartió Ana María de Santiago Nocito el 2/15/2024?", "answer": "RCP básica con DESA y fármacos." }, { "query": "¿Cuántas clases ha impartido en total Luis España Barrio?", "answer": "Luis España Barrio ha impartido 11 clases." } ] # Dependencias necesarias para la evaluación from langchain.evaluation.qa import QAGenerateChain # Generación automática de pares pregunta-respuesta from langchain.evaluation.qa import QAEvalChain # Evaluación automática de respuestas #Definimos el chain QAGenerateChain y lo usamos para definir pares pregunta-respuestas de forma automática chain_generador_ejemplos = QAGenerateChain.from_llm(llm) nuevos_ejemplos = chain_generador_ejemplos.apply_and_parse( [{"doc": t} for t in data[:5]] ) # Junto todos los ejemplos, ajustándolos a la forma query:answer, que es el formato necesario para usar después la función "apply" ejemplos = ejemplos + [ { "query": ejemplo['qa_pairs']["query"], "answer": ejemplo['qa_pairs']["answer"] } for ejemplo in nuevos_ejemplos ] # Ejecuto las consultas de los ejemplos y obtnego: la consulta (query), la respuesta del ejemlop, ya sea la mía o la que generó el modelo (answwer) y la que ha generado ahora (result) predicciones = qa.apply(ejemplos) # Evaluamos las respuestas, 'evaluate' compara a través del LLM especificado la respuesta del ejemlo y la obtenida posteriormente y determina si son 'correct' o 'incorrect' eval_chain = QAEvalChain.from_llm(llm) salidas_clasificadas = eval_chain.evaluate(ejemplos, predicciones) # Mostramos las conclusiones de la evaluación automática for i, eg in enumerate (ejemplos): print(f"Ejemplo {i}: ") print("Question: " + predicciones[i]['query']) print("Real Answer: " + predicciones[i]['answer']) print("Predicted Answer: " + predicciones[i]['result']) print("Clasificación de predicción: " + salidas_clasificadas[i]['results']) print() print(salidas_clasificadas[0])
En nuestro caso es siempre incorrecto, por el problema que arrastramos cuando vimos los chains QA para preguntar acerca del contenido de documentos: Como cargamos los datos de un CSV, los registros se pasan como “documentos” o “porciones de información” independientes, lo que hace que los pares preguntas-respuesta generadas sean de registros concretos. El problema es que cuando el modelo es consultado, el retriever no funciona correctamente y no recupera los datos correctos. Puede deverse a múltiples factores relacionados con la forma en que funciona el retriever, pero eso ahora mismo excede el objetivo de este texto formativo.
- usaremos el chain QAEvalChain
Depuración
También es importante saber que hay varias maneras de ver qué está pasando “tras bambalinas”, a la hora de depurar.
Esto se puede hacer de dos Maneras:
- verbose = true, como parámetro del chain, de modo que se podrá observar qué ocurre “dentro” de ese chain cuando se ejecuta.
# Anteriormente se define qa como un QAChain y data como el contenido de un loaderCSV from langchain.evaluation.qa import QAGenerateChain chain_generador_ejemplos = QAGenerateChain.from_llm(llm, verbose=True) # Indicamos que muestre lo que ocurre "dentro" de este chain ejemplos = chain_generador_ejemplos.apply_and_parse( [{"doc": t} for t in data[:5]] ) pprint.pprint(ejemplos[0]) respuesta=qa.run(ejemplos[0]["qa_pairs"]["query"]) pprint.pprint(respuesta)
- Debug Mode: importamos directamente langchain, para poder activar (y desactivar) el modo depuración, que provocará que se muestre TODO lo que ocurre, cada vez que se llama a una función de langchain.
# Anteriormente se define qa como un QAChain y data como el contenido de un loaderCSV from langchain.evaluation.qa import QAGenerateChain import langchain # Dependencia necesaria para entrar en el modo 'debug' langchain.debug = True chain_generador_ejemplos = QAGenerateChain.from_llm(llm) ejemplos = chain_generador_ejemplos.apply_and_parse( [{"doc": t} for t in data[:5]] ) pprint.pprint(ejemplos[0]) respuesta=qa.run(ejemplos[0]["qa_pairs"]["query"]) langchain.debug = False pprint.pprint(respuesta)
Agentes
Los agentes son una parte experimental de langchain y que debe ser tomada como tal.
Los agentes se crean con un framework de langchain. Al definir un agente, éste utiliza un LLM para realizar las funciones que se le pidan, con la salvedad de que pueden usar herramientas predefinidas que les permiten ser “buenos” en algo y realizar acciones. Hay una gran herramientas disponibles: 'llm-math' para realizar operaciones matemáticas, 'wikipedia', para realizar búsquedas en esta plataforma, etc.
Si se usan diferentes herramientas tools, el propio modelo elegirá cuál debe usar en cada situación.
Podemos ver todo esto en el siguiente ejemplo:
# Prueba agentes de langchain # Dependencias from langchain_ollama import ChatOllama from langchain.agents import load_tools, initialize_agent from langchain.agents import AgentType # Definimos modelo a utilizar llm = ChatOllama( model = "llama3.2", temperature = 0.0, verbose = True ) # cargamos las herramientas a disposición del agente. Deben haber sido previamente instaladas tools = load_tools(["llm-math", "wikipedia"], llm=llm) # Creamos agente agent = initialize_agent( tools, # Herramientas a disposición del agente definidas anteriormente llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, # Tipo de agente handle_parsing_errors=True, # Manejar errores de análisis, en lugar de fallar y detener verbose=True ) # El agente evaluará las herramientas que debe usar, de las que dispone, en función del contexto de la pregunta #respuesta = agent(f"¿Cuál es el 25% de 300?") respuesta = agent(f"¿Quién ganó la final de la copa del mundo de fútbol del año 2022?") print(respuesta)
Puede ser interesante usar el modo depuración de langchain para comprobar los prompts internos para entender su funcionamiento. Vamos a verlo en el siguiente ejemplo, en el cual se va a usar la herramienta PythonREPLTool.
Esta herramienta da la posibilidad de que el LLM programe sus propias funciones en python y las ejecute, como si dispusiese de un terminal interno, aunque sólo puede usar los comandos nativos sin bibliotecas de terceros:
# Prueba agentes de langchain # Dependencias from langchain_ollama import ChatOllama from langchain_experimental.agents.agent_toolkits import create_python_agent from langchain_experimental.tools import PythonREPLTool # Definimos modelo a utilizar llm = ChatOllama( model = "llama3.2", temperature = 0.0, verbose = True ) # Creamos agente agente = create_python_agent( llm, tool = PythonREPLTool(), # Esta herramienta permite ejecutar código Python como si tuviera internamente un terminal handle_parsing_errors = True, # Manejar errores de análisis, en lugar de fallar y detener la ejecución verbose = True ) lista_clientes = [["Harrison", "Chase"], ["Lang", "Chain"], ["Dolly", "Too"], ["Elle", "Elem"], ["Geoff","Fusion"], ["Trance","Former"], ["Jen","Ayai"] ] import langchain langchain.debug = True # Permite ver los prompts completos internos de langchain agente.run( f"""Ordena estos clientes por su apellido y después por su nombre e imprime la salida: {lista_clientes}""" ) langchain.debug = False









