Análisis de salida de LLM Llamada de función vs. Cadena de lenguaje

LLM Function Call Output Analysis vs. Language String

Cómo analizar de manera consistente las salidas de LLMs utilizando la API de Open AI y la función de llamada a LangChain: evaluando las ventajas y desventajas de los métodos

Imagen de Victor Barrios en Unsplash

Crear herramientas con LLMs requiere múltiples componentes, como bases de datos vectoriales, cadenas, agentes, separadores de documentos y muchas otras herramientas nuevas.

Sin embargo, uno de los componentes más cruciales es el análisis de las salidas de LLMs. Si no puedes recibir respuestas estructuradas de tu LLM, tendrás dificultades para trabajar con las generaciones. Esto se vuelve aún más evidente cuando queremos que una sola llamada al LLM produzca más de una pieza de información.

Ilustremos el problema con un escenario hipotético:

Queremos que el LLM produzca en una sola llamada los ingredientes y los pasos para hacer una cierta receta. Pero queremos tener ambos elementos por separado para usarlos en dos partes diferentes de nuestro sistema.

import openairecipe = 'Fish and chips'query = f"""¿Cuál es la receta para {recipe}? Devuelva la lista de ingredientes y los pasos por separado."""response = openai.ChatCompletion.create(    model="gpt-3.5-turbo-0613",    messages=[{"role": "user", "content": query}])response_message = response["choices"][0]["message"]print(response_message['content'])

Esto devuelve lo siguiente:

Ingredientes para pescado y papas fritas:- 1 libra de filetes de pescado blanco (como bacalao o eglefino)- 1 taza de harina común- 1 cucharadita de polvo de hornear- 1 cucharadita de sal- 1/2 cucharadita de pimienta negra- 1 taza de cerveza fría- Aceite vegetal, para freír- 4 papas grandes- Sal, al gustoPasos para hacer pescado y papas fritas:1. Precalienta el horno a 200°C (400°F).2. Pela las papas y córtalas en tiras gruesas y uniformes. Enjuaga las tiras de papas con agua fría para eliminar el exceso de almidón. Sécalas con una toalla de cocina limpia.3. En una olla grande o freidora profunda, calienta aceite vegetal a 175°C (350°F). Asegúrate de tener suficiente aceite para sumergir completamente las papas y el pescado.4. En un tazón, combina la harina, el polvo de hornear, la sal y la pimienta negra. Agrega gradualmente la cerveza fría y mezcla hasta obtener una masa suave. Deja la masa a un lado.5. Toma las tiras de papas secas y fríelas por lotes durante aproximadamente 5-6 minutos o hasta que estén doradas. Retira las papas fritas con una cuchara ranurada y colócalas en un plato forrado con papel toalla para que absorba el exceso de aceite. Mantenlas calientes en el horno precalentado.6. Sumerge cada filete de pescado en la masa preparada, asegurándote de que esté bien cubierto. Deja que el exceso de masa gotee antes de colocar cuidadosamente el filete en el aceite caliente.7. Fríe los filetes de pescado durante 4-5 minutos por cada lado o hasta que estén dorados y crujientes. Retíralos del aceite con una cuchara ranurada y colócalos en un plato forrado con papel toalla para que absorba el exceso de aceite.8. Sazona el pescado y las papas fritas con sal mientras aún estén calientes.9. Sirve el pescado y las papas fritas calientes con salsa tártara, vinagre de malta o ketchup si lo deseas.¡Disfruta de tu pescado y papas fritas caseras!

Esta es una cadena de texto muy larga y analizarla sería difícil porque el LLM puede devolver estructuras ligeramente diferentes que pueden romper cualquier código que escribas. Podrías argumentar que pedir en la solicitud que siempre devuelva “Ingredientes:” y “Pasos:” podría resolverlo y no estarías equivocado. Esto podría funcionar, sin embargo, aún necesitarías procesar la cadena manualmente y estar abierto a variaciones y alucinaciones eventuales.

Solución

Existen un par de formas en las que podríamos resolver este problema. Una se mencionó anteriormente, pero hay un par de formas probadas que podrían ser mejores. En este artículo, mostraré dos opciones:

  1. Llamada a función de Open AI;
  2. Analizador de salida de LangChain.

Llamada a función de Open AI

Este es un método que he estado probando y está dando los resultados más consistentes. Utilizamos la capacidad de llamada a función de la API de Open AI para que el modelo devuelva la respuesta como un JSON estructurado.

Esta funcionalidad tiene como objetivo proporcionar al LLM la capacidad de llamar a una función externa proporcionando las entradas como un JSON. Los modelos se ajustaron para entender cuándo necesitan usar una función determinada. Un ejemplo de esto es una función para el clima actual. Si le preguntas a GPT por el clima actual, no podrá decírtelo, pero puedes proporcionar una función que lo haga y pasarla a GPT para que sepa que se puede acceder a ella dada alguna entrada.

Si quieres profundizar en esta funcionalidad, aquí está el anuncio de Open AI y aquí tienes un gran artículo.

Entonces, veamos el código y cómo se vería esto dado nuestro problema actual. Desglosemos el código:

functions = [    {        "name": "return_recipe",        "description": "Devuelve la receta solicitada",        "parameters": {            "type": "object",            "properties": {                "ingredients": {                    "type": "string",                    "description": "La lista de ingredientes."                },                "steps": {                    "type": "string",                    "description": "Los pasos de la receta."                },            },            },            "required": ["ingredients","steps"],        }]

Lo primero que tenemos que hacer es declarar las funciones que estarán disponibles para el LLM. Tenemos que darle un nombre y una descripción para que el modelo entienda cuándo debe usar la función. Aquí le decimos que esta función se utiliza para devolver la receta solicitada.

Luego entramos en los parámetros. Primero, decimos que es de tipo objeto y las propiedades que puede usar son ingredientes y pasos. Ambos también tienen una descripción y un tipo para guiar al LLM en la salida. Finalmente, especificamos qué propiedades se requieren para llamar a la función (esto significa que podríamos tener campos opcionales que el LLM juzgaría si quiere usarlos).

Usémoslo ahora en una llamada al LLM:

import openairecipe = 'Fish and chips'query = f"¿Cuál es la receta para {recipe}? Devuelve la lista de ingredientes y los pasos por separado."response = openai.ChatCompletion.create(    model="gpt-3.5-turbo-0613",    messages=[{"role": "user", "content": query}],    functions=functions,    function_call={'name':'return_recipe'})response_message = response["choices"][0]["message"]print(response_message)print(response_message['function_call']['arguments'])

Aquí comenzamos creando nuestra consulta a la API formateando un prompt base con lo que podría ser una entrada variable (receta). Luego, declaramos nuestra llamada a la API usando “gpt-3.5-turbo-0613”, pasamos nuestra consulta en el argumento de mensajes y ahora pasamos nuestras funciones.

Hay dos argumentos relacionados con nuestras funciones. En el primero, pasamos la lista de objetos en el formato mostrado anteriormente con las funciones a las que el modelo tiene acceso. Y en el segundo argumento “function_call”, especificamos cómo el modelo debe usar esas funciones. Hay tres opciones:

  1. “Auto” -> el modelo decide entre la respuesta del usuario o llamar a la función;
  2. “none” -> el modelo no llama a la función y devuelve la respuesta del usuario;
  3. {“name”: “nombre_de_mi_función”} -> especificar un nombre de función obliga al modelo a usarla.

Puedes encontrar la documentación oficial aquí.

En nuestro caso, y para usarlo en el análisis de la salida, usamos la última opción:

function_call={'name':'return_recipe'}

Así que ahora podemos ver nuestras respuestas. La respuesta que obtenemos (después de este filtrado [“choices”][0][“message”]) es:

{  "role": "assistant",  "content": null,  "function_call": {    "name": "return_recipe",    "arguments": "{\n  \"ingredients\": \"Para el pescado:\\n- 1 libra de filetes de pescado blanco\\n- 1 taza de harina común\\n- 1 cucharadita de levadura en polvo\\n- 1 cucharadita de sal\\n- 1/2 cucharadita de pimienta negra\\n- 1 taza de agua fría\\n- Aceite vegetal, para freír\\nPara las papas fritas:\\n- 4 papas grandes\\n- Aceite vegetal, para freír\\n- Sal, al gusto\",\n  \"steps\": \"1. Comienza preparando el pescado. En un plato hondo, mezcla la harina, la levadura en polvo, la sal y la pimienta negra.\\n2. Añade gradualmente el agua fría y bate hasta que la masa quede suave.\\n3. Calienta el aceite vegetal en una sartén grande o freidora.\\n4. Sumerge los filetes de pescado en la masa, asegurándote de cubrirlos por completo.\\n5. Coloca los filetes cubiertos en el aceite caliente y fríelos durante 4-5 minutos por cada lado, o hasta que estén dorados y crujientes.\\n6. Retira el pescado frito del aceite y colócalo en un plato forrado con papel absorbente para eliminar el exceso de aceite.\\n7. Para las papas fritas, pela las papas y córtalas en trozos gruesos.\\n8. Calienta el aceite vegetal en una freidora o sartén grande.\\n9. Fríe las papas en tandas hasta que estén doradas y crujientes.\\n10. Retira las papas fritas del aceite y colócalas en un plato forrado con papel absorbente para eliminar el exceso de aceite.\\n11. Sazona las papas fritas con sal.\\n12. Sirve el pescado y las papas fritas juntos ¡y disfruta!\"\n}"  }}

Si lo analizamos aún más en la “llamada a la función”, podemos ver nuestra respuesta estructurada pretendida:

{  "ingredients": "Para el pescado:\n- 1 lb de filetes de pescado blanco\n- 1 taza de harina común\n- 1 cucharadita de polvo para hornear\n- 1 cucharadita de sal\n- 1/2 cucharadita de pimienta negra\n- 1 taza de agua fría\n- Aceite vegetal, para freír\nPara las papas fritas:\n- 4 papas grandes\n- Aceite vegetal, para freír\n- Sal, al gusto",  "steps": "1. Comienza preparando el pescado. En un plato llano, combina la harina, el polvo para hornear, la sal y la pimienta negra.\n2. Agrega gradualmente el agua fría y mezcla hasta obtener una masa suave.\n3. Calienta aceite vegetal en una sartén grande o freidora.\n4. Sumerge los filetes de pescado en la masa, asegurándote de cubrirlos por completo.\n5. Coloca suavemente los filetes cubiertos en el aceite caliente y fríelos durante 4-5 minutos por cada lado, o hasta que estén dorados y crujientes.\n6. Retira el pescado frito del aceite y colócalo en un plato forrado con papel toalla para que escurra el exceso de aceite.\n7. Para las papas fritas, pela las papas y córtalas en trozos gruesos.\n8. Calienta aceite vegetal en una freidora o sartén grande.\n9. Fríe las papas en tandas hasta que estén doradas y crujientes.\n10. Retira las papas fritas del aceite y colócalas en un plato forrado con papel toalla para que escurra el exceso de aceite.\n11. Sazona las papas fritas con sal.\n12. Sirve el pescado y las papas fritas juntos, ¡y disfruta!"}

Conclusión para llamadas a funciones

Es posible utilizar la función de llamada a la función directamente desde la API de Open AI. Esto nos permite obtener una respuesta estructurada en formato de diccionario con las mismas claves cada vez que se llama al LLM.

Su uso es bastante sencillo, solo tienes que declarar el objeto de las funciones especificando el nombre, la descripción y las propiedades centradas en tu tarea, pero especificando (en la descripción) que esta debe ser la respuesta del modelo. Además, al llamar a la API podemos obligar al modelo a utilizar nuestra función, lo que lo hace aún más consistente.

El principal inconveniente de este método es que no es compatible con todos los modelos y API de LLM. Por lo tanto, si quisiéramos utilizar la API de Google PaLM, tendríamos que utilizar otro método.

Analizadores de salida de LangChain

Una alternativa que tenemos que es independiente del modelo es utilizar LangChain.

Primero, ¿qué es LangChain?

LangChain es un marco de trabajo para desarrollar aplicaciones impulsadas por modelos de lenguaje.

Esa es la definición oficial de LangChain. Este marco de trabajo fue creado recientemente y ya se utiliza como el estándar de la industria para construir herramientas impulsadas por LLM.

Tiene una funcionalidad que es ideal para nuestro caso de uso llamada “Analizadores de salida”. En este módulo, se pueden crear múltiples objetos para devolver y analizar diferentes tipos de formatos de llamadas de LLM. Para lograr esto, primero se declara cuál es el formato y se pasa en la solicitud al LLM. Luego se utiliza el objeto creado previamente para analizar la respuesta.

Analicemos el código:

from langchain.prompts import ChatPromptTemplatefrom langchain.output_parsers import ResponseSchema, StructuredOutputParserfrom langchain.llms import GooglePalm, OpenAIingredients = ResponseSchema(        name="ingredients",        description="Los ingredientes de la receta, como una cadena única.",    )steps = ResponseSchema(        name="steps",        description="Los pasos para preparar la receta, como una cadena única.",    )output_parser = StructuredOutputParser.from_response_schemas(    [ingredients, steps])response_format = output_parser.get_format_instructions()print(response_format)prompt = ChatPromptTemplate.from_template("¿Cuál es la receta para {receta}? Devuelve la lista de ingredientes y los pasos por separado. \n {instrucciones_formato}")

Lo primero que hacemos aquí es crear nuestro esquema de respuesta que será la entrada para nuestro analizador. Creamos uno para los ingredientes y otro para los pasos, cada uno contiene un nombre que será la clave del diccionario y una descripción que guiará al LLM en la respuesta.

Luego creamos nuestro Analizador de Salida Estructurada a partir de esos esquemas de respuesta. Hay varias formas de hacer esto, con diferentes estilos de analizadores. Mira aquí para aprender más sobre ellos.

Por último, obtenemos nuestras instrucciones de formato y definimos nuestra solicitud que tendrá el nombre de la receta y las instrucciones de formato como entradas. Las instrucciones de formato son las siguientes:

"""El resultado debería ser un fragmento de código de markdown formateado en el siguiente esquema, incluyendo "```json" al inicio y "```" al final:```json{ "ingredients": string  // Los ingredientes de la receta, como una cadena única. "steps": string  // Los pasos para preparar la receta, como una cadena única.}  """

Ahora solo nos queda llamar a la API. Aquí demostraré tanto la API de Open AI como la API de Google PaLM.

llm_openai = OpenAI()llm_palm = GooglePalm()receta = 'Fish and chips'prompt_formateado = prompt.format(**{"receta":receta, "format_instructions":output_parser.get_format_instructions()})respuesta_palm = llm_palm(prompt_formateado)respuesta_openai = llm_openai(prompt_formateado)print("PaLM:")print(respuesta_palm)print(output_parser.parse(respuesta_palm))print("Open AI:")print(respuesta_openai)print(output_parser.parse(respuesta_openai))

Como puedes ver, es realmente fácil cambiar entre modelos. La estructura completa definida anteriormente puede utilizarse de la misma manera para cualquier modelo compatible con LangChain. También utilizamos el mismo analizador para ambos modelos.

Esto generó la siguiente salida:

# PaLM:{'ingredients': '''- 1 taza de harina común\n- 1 cucharadita de polvo de hornear\n- 1/2 cucharadita de sal\n- 1/2 taza de agua fría\n- 1 huevo\n- 1 libra de filetes de pescado blanco, como bacalao o abadejo\n- Aceite vegetal para freír\n- 1 taza de salsa tártara\n- 1/2 taza de vinagre de malta\n- Gajos de limón''', 'steps': '''1. En un tazón grande, mezcla la harina, el polvo de hornear y la sal.\n2. En otro tazón, bate el huevo y el agua.\n3. Sumerge los filetes de pescado en la mezcla de huevo y luego cúbrelos con la mezcla de harina.\n4. Calienta el aceite en una freidora profunda o en una sartén grande a 375 grados F (190 grados C).\n5. Fríe los filetes de pescado durante 3-5 minutos por cada lado, o hasta que estén dorados y cocidos por dentro.\n6. Escurre los filetes de pescado sobre toallas de papel.\n7. Sirve los filetes de pescado inmediatamente con salsa tártara, vinagre de malta y gajos de limón.'''}# Open AI{'ingredients': '1 ½ libras de filete de bacalao, cortado en 4 trozos, 2 tazas de harina común, 2 cucharaditas de polvo de hornear, 1 cucharadita de sal, 1 cucharadita de pimienta negra recién molida, ½ cucharadita de ajo en polvo, 1 taza de cerveza (o agua), aceite vegetal para freír, salsa tártara para servir', 'steps': '1. Precalienta el horno a 400°F (200°C) y forra una bandeja para hornear con papel pergamino. 2. En un tazón grande, mezcla la harina, el polvo de hornear, la sal, la pimienta y el ajo en polvo. 3. Vierte la cerveza y mezcla hasta obtener una masa espesa. 4. Sumerge el bacalao en la masa, cubriéndolo por todos lados. 5. Calienta aproximadamente 2 pulgadas (5 cm) de aceite en una olla grande o sartén a fuego alto. 6. Fríe el bacalao durante 3 a 4 minutos por cada lado, o hasta que esté dorado. 7. Transfiere el bacalao a la bandeja para hornear preparada y hornea durante 5 a 7 minutos. 8. Sirve caliente con salsa tártara.'}

Conclusión: Análisis de salida de LangChain

Este método también es muy bueno y tiene como principal característica la flexibilidad. Creamos un par de estructuras como Esquema de Respuesta, Analizador de Salida y Plantillas de Prompt que se pueden combinar fácilmente y usar con diferentes modelos. Otra buena ventaja es el soporte para múltiples formatos de salida.

La principal desventaja proviene de pasar las instrucciones de formato a través del prompt. Esto permite errores aleatorios y alucinaciones. Un ejemplo real fue en este caso específico donde tuve que especificar “como una cadena única” en la descripción del esquema de respuesta. Si no especificaba esto, el modelo devolvía una lista de cadenas con los pasos e instrucciones y esto causaba un error de análisis en el Analizador de Salida.

Conclusión

Existen múltiples formas de utilizar un analizador de salida para tu aplicación impulsada por LLM. Sin embargo, tu elección puede cambiar dependiendo del problema en cuestión. Personalmente, me gusta seguir esta idea:

Siempre uso un analizador de salida, incluso si solo tengo una salida del LLM. Esto me permite controlar y especificar mis salidas. Si estoy trabajando con Open AI, la Llamada a Funciones es mi elección porque ofrece el mayor control y evitará errores aleatorios en una aplicación de producción. Sin embargo, si estoy usando un LLM diferente o necesito un formato de salida diferente, mi elección es LangChain, pero con mucha prueba en las salidas para poder redactar el prompt con el menor número de errores.

Gracias por leer.

El código completo se puede encontrar aquí.

Si te gusta el contenido y quieres apoyarme, puedes comprarme un café:

Gabriel Cassimiro es un científico de datos que comparte contenido gratuito con la comunidad

¡Me encanta apoyar a los creadores!

www.buymeacoffee.com

Aquí tienes algunos otros artículos que podrían interesarte:

Llamadas asíncronas para cadenas con Langchain

Cómo hacer que las cadenas de Langchain funcionen con llamadas asíncronas a LLMs, acelerando el tiempo que lleva ejecutar una secuencia larga…

towardsdatascience.com

Resolviendo el entorno de Unity con Aprendizaje por Reforzamiento Profundo

Proyecto de principio a fin con código de una implementación de un agente de Aprendizaje por Reforzamiento Profundo en PyTorch.

towardsdatascience.com