Automatice la redacción de sus indicaciones de LLM
Imagen creada por Serj Smorodinsky, coautor de Creación de aplicaciones LLM con DSPy

Probablemente todos hemos tenido la experiencia de recibir respuestas que no eran exactamente las que queríamos. Por lo general, intentaremos reformular las indicaciones varias veces hasta que obtengamos algo razonable. A veces tenemos que ser más claros, más precisos, dar ejemplos, describir por qué necesitamos la respuesta, presentar una persona o proporcionar suficiente contexto e información para que el LLM pueda brindar una respuesta adecuada.

Esto puede estar bien cuando trabajamos directamente con el LLM. Sin embargo, es bastante diferente cuando escribimos una aplicación basada en LLM: software que se ejecutará por sí solo y que, al hacerlo, interactuará con uno o más LLM. Aquí, el software funcionará con indicaciones predefinidas y las pasará a los LLM. Si no sale bien, no estaremos allí para reformular las indicaciones e intentarlo de nuevo. Lo que significa que, en primer lugar, deben estar escritos de una manera que sea sólida y confiable; necesitamos indicaciones que podamos estar seguros de que funcionarán bien de manera consistente en producción.

Crear un mensaje de este tipo puede resultar complicado. En este artículo, repasaremos por qué es así y también cómo una herramienta de Python llamada DSPy puede permitir la creación de mensajes que sean confiables. DSPy no solo genera mensajes automáticamente para usted, sino que también los evalúa minuciosamente, por lo que puede estar seguro de qué tan bien funcionarán en producción.
También proporcionaré un extracto de mi libro más reciente con Manning Publishing, Building LLM Applications with DSPy, en coautoría con Serj Smorodinsky. Eso proporciona una descripción completa de DSPy y cómo usarlo para crear aplicaciones basadas en LLM.

Imagen de portada del libro

El truco de crear un mensaje que pueda funcionar de manera confiable en producción

Parte de lo que dificulta la creación de un mensaje confiable es que no podemos predecir completamente la entrada que tendremos para el mensaje. Digamos, por ejemplo, que estamos creando una aplicación de software que procesará documentos. Los documentos pueden encontrarse en línea o posiblemente enviarse por los usuarios del software. Como parte del procesamiento de los documentos, la aplicación puede pedirle a un LLM que los resuma, los traduzca, extraiga información clave o realice alguna otra tarea similar. Para este ejemplo, digamos que el software le pedirá al LLM que critique qué tan plausible parece ser el contenido de los documentos. Para hacer eso podemos escribir un mensaje como:

Prompt_text = f"Evalúa qué tan plausible es el siguiente texto: {document_text}"

Utiliza una cadena f de Python para formar el mensaje, con un espacio para el texto del documento. Otros mensajes pueden tener múltiples espacios para las entradas, pero para simplificar, asumiremos aquí que cada mensaje tiene solo una entrada: la parte del contenido que desea que procese el LLM (que es la parte que es impredecible).

Este mensaje puede funcionar suficientemente bien, pero también puede que no. Hay varias formas en que el LLM puede responder de una manera que no nos guste, al menos ocasionalmente. Es posible que descubramos que el LLM detecta detalles irrelevantes en los documentos. O puede que tenga un sentido de "plausible" diferente al que pretendíamos. O puede indicar que casi todos los documentos son totalmente plausibles (o lo contrario, que casi ninguno lo es). O es posible que las respuestas no tengan el formato que deseamos.

Es posible que necesitemos modificar el mensaje para obtener consistentemente las respuestas que esperaríamos. Para comenzar, podemos probar este y algunos otros mensajes simples, pero el mensaje final puede terminar siendo considerablemente más largo y detallado que este.

Por lo general, a medida que probamos con más entradas (en este caso, más documentos), encontraremos más casos en los que el mensaje actual no maneja bien la entrada, por lo que modificaremos el mensaje para manejar mejor estos casos. A veces podemos reformular el mensaje para que sea más claro y otras veces agregar algunas oraciones al mensaje para manejar estos casos específicos. Por ejemplo, "Si el documento hace afirmaciones metafóricas, evalúe la intención general y no el significado literal". Podemos terminar con cualquier cantidad de instrucciones adicionales como esta en el mensaje, lo que puede ayudar a que el mensaje funcione bien en estos casos, pero, por supuesto, también puede hacer que el mensaje funcione peor para otras entradas.

Y, a medida que las indicaciones se vuelven más largas y complicadas, puede resultar más difícil modificarlas. Puede volverse cada vez menos claro cuál será el efecto de agregar, eliminar, reordenar o reformular frases en el mensaje.

Otras aplicaciones basadas en LLM pueden funcionar con otros tipos de datos de texto: mensajes de texto, correos electrónicos, ensayos, artículos de revistas, solicitudes de patentes, etc. O podrá procesar imagen, audio, video u otras modalidades. Pero, independientemente del tipo de entrada, para una aplicación no trivial, la entrada específica que la aplicación encuentre (y pase al LLM) será al menos algo impredecible. Lo que significa que necesitaremos un mensaje sólido y bien especificado para manejar una amplia gama de entradas realistas.

Para tomar el ejemplo del correo electrónico, si una aplicación basada en LLM está procesando una colección de correos electrónicos (que encontrará en producción y que no podemos predecir completamente), puede haber correos electrónicos que sean inusuales: largos, complejos, matizados, confusos, sinuosos o que no sean como esperábamos al generar el mensaje. La única forma de probar que su aplicación funcionará de manera confiable en producción es probar con un conjunto grande, diverso y realista de entradas (en este caso, una colección grande y diversa de correos electrónicos realistas).

Y para cada caso de prueba, debemos examinar cuidadosamente la respuesta del LLM y verificar que sea adecuada. En algunos casos, esto es sencillo. Por ejemplo, podemos pasar un texto a un LLM y pedirle que lo clasifique de alguna manera. El LLM puede clasificar el texto en términos de identificación del idioma (inglés, francés, etc.), sentimiento, toxicidad, etc. En estos casos, hay una clase verdadera para cada entrada y está la clase que devuelve el LLM. Sólo tenemos que comprobar que son iguales: si el texto está en español y el LLM predice español, es correcto; de lo contrario no. Muchas otras tareas de LLM producen resultados que también son fáciles de evaluar.

Sin embargo, en algunos casos, evaluar las respuestas no es tan sencillo. Un ejemplo es cuando le pedimos al LLM que genere una respuesta más larga, como un resumen, traducción, crítica, sugerencias para pasos de seguimiento o cualquier otro resultado extenso similar basado en los aportes. Si alguna vez ha analizado dos o más respuestas diferentes de un LLM (donde ambas tienen una o más oraciones completas, y posiblemente mucho más largas) y ha intentado evaluar cuál es mejor, sabrá que esto lleva mucho tiempo. Y propenso a errores. Algunas pueden ser más concisas, otras más matizadas y otras más claras. Sin embargo, por más difíciles que sean de evaluar, necesitamos evaluarlos para evaluar qué tan bien está funcionando cada mensaje que intentamos. Una de las cosas buenas de DSPy es que te permite automatizar esta evaluación.

Ingeniería inmediata

Para ver el valor de herramientas como DSPy, es bueno observar la alternativa y el problema que DSPy está resolviendo. Normalmente, la forma en que trabajamos con los LLM es utilizando una técnica conocida como ingeniería rápida. Al hacer esto, escribimos un mensaje, lo probamos (generalmente con solo unas pocas entradas y simplemente observando las salidas), escribimos otro mensaje, lo probamos de manera similar y continuamos.

En casos más simples, esto puede funcionar, pero tiene una serie de limitaciones. Una es: lleva mucho tiempo probar cada mensaje candidato con más que una pequeña cantidad de entradas. Entonces, en la práctica, normalmente probamos cada mensaje mucho menos de lo que deberíamos. Lo que puede causar problemas: probar cada mensaje con muy pocas entradas puede darnos una mala idea de qué mensajes funcionan mejor.

Para hacer esto más complicado, con cada entrada, realmente deberíamos probar el mensaje varias veces (y no solo una vez), ya que los LLM son estocásticos. Si se le da el mismo mensaje (incluidos los mismos valores en los espacios) varias veces, un LLM puede devolver respuestas diferentes cada vez. Y algunos pueden ser mejores que otros. Si tenemos, digamos, 20 documentos para probar (en el ejemplo donde se usará el LLM para estimar la plausibilidad de cada documento), lo ideal sería probar cada uno varias veces. Si probamos cada uno 3 veces, eso significa 60 pruebas en total. Lo cual, siendo realistas, en realidad no haremos. Probablemente ni siquiera cerca.

Y, como se indicó, esto es aún más difícil cuando los LLM arrojan resultados más largos, ya que lleva mucho tiempo leerlos y es casi imposible ser coherente en la forma en que los evaluamos.

Por lo tanto, probar cada mensaje candidato lleva mucho tiempo. Probar muchas indicaciones de candidatos lo es mucho más. Y no está claro que realmente podamos compararlos de manera justa.

Todo esto significa que, en general, la ingeniería rápida tiene la interesante cualidad de consumir mucho tiempo y ser poco confiable. Es un proceso muy lento, tedioso y propenso a errores. Los desarrolladores experimentados a menudo pueden dedicar horas, o incluso días, a un solo mensaje. Y al final, no puedo estar seguro de que el que eligieron sea realmente el más fuerte.

¿Existe una mejor manera?

Si retrocedemos un minuto, podemos ver cómo manejamos una situación similar cuando trabajamos con aprendizaje automático. Si estamos construyendo una red neuronal, un bosque aleatorio, un modelo XGBoost (o cualquier cosa por el estilo), cada vez que la entrenamos, no probamos manualmente cada elemento del conjunto de prueba, uno a la vez. De hecho, la idea de hacer eso parece un poco tonta. El proceso está automatizado; La prueba es bastante simple. Simplemente ejecutamos cada elemento del conjunto de pruebas a través del modelo, obtenemos una predicción para cada uno y ejecutamos una función para generar una puntuación general.

Por ejemplo, podemos usar el error cuadrático medio o R cuadrado para un problema de regresión y posiblemente la puntuación F1, MCC o AUROC para un problema de clasificación. Usando una herramienta como scikit-learn, podemos tomar las predicciones del modelo para el conjunto de prueba y los valores de verdad reales correspondientes, y simplemente pasarlos a una función para calcular la puntuación general. Luego tenemos un único número que indica qué tan bien funcionó ese modelo.

A continuación, si lo deseamos, podemos volver a intentarlo con diferentes características, diferentes hiperparámetros, diferentes datos de entrenamiento (o algún otro cambio similar del modelo anterior), volver a entrenar y volver a ejecutar las pruebas, obteniendo otra puntuación.

Entonces, con los proyectos de ML, tenemos un proceso limpio y eficiente. Pero cuando trabajamos con LLM, tendemos a hacer algo bastante diferente, algo más cercano a la ingeniería rápida: trabajar sin un marco para garantizar la coherencia, la repetibilidad y la eficiencia. Básicamente, ignoramos décadas de experiencia en el desarrollo de mejores prácticas para el desarrollo de software.

Sin embargo, eso no es necesario. Al trabajar con LLM, existen una serie de herramientas que nos permiten trabajar de manera similar a como lo hacemos cuando creamos modelos de aprendizaje automático: de una manera eficiente, exhaustiva y repetible. Es probable que DSPy sea el estado del arte en estos, al menos por el momento. Al usarlo, especificamos nuestros datos de prueba y un método para evaluar qué tan buena es una respuesta. Se necesita algo de tiempo para hacer eso, pero una vez hecho, casi todo lo demás lo manejamos por nosotros.

En el ejemplo en el que le pedimos a un LLM que estime la verosimilitud de los documentos, podríamos reunir un conjunto de documentos (posiblemente 10, 20 o 30, aunque más es mejor) para que sean nuestro conjunto de prueba. Y para cada uno de ellos podríamos proporcionar una verdad fundamental sobre su verosimilitud. Podría ser un valor numérico, digamos, en una escala de 0 a 10.

También tenemos que proporcionar una forma para que DSPy evalúe qué tan sólida es cada respuesta de LLM, en forma de una función de Python. Esta será una función que acepta la entrada al LLM y la respuesta del LLM, y que devuelve: 1) un valor numérico (que indica qué tan buena es la respuesta); o 2) un valor booleano (que indica simplemente si la respuesta es buena o mala). En este ejemplo, la función puede ser bastante simple, como:

def evaluar_respuesta(instancia_prueba, predicción_modelo): devolver abs(instancia_prueba.ground_truth – predicción_modelo)

Esta no es precisamente la sintaxis DSPy (estoy omitiendo algunos pequeños detalles por simplicidad, pero esto da una idea general). En este caso, asumimos que cada instancia de prueba contiene un documento que se puede enviar al LLM y un valor de verdad fundamental (un número entre 0 y 10, que indica qué tan plausible es realmente, probablemente basado en una evaluación humana). Y asumimos que la predicción del modelo también es un número entre 0 y 10. Para calificar la respuesta, simplemente tomamos la diferencia entre estas dos puntuaciones, de modo que cuanto menor sea la diferencia, mejor será la respuesta (cuanto más cerca esté de la verdad fundamental).

Para probar un mensaje determinado, DSPy ejecutaría automáticamente el mensaje en un LLM específico, una vez para cada uno de los documentos de prueba. En este ejemplo, para cada uno, pediría una puntuación de 0 a 10 que indique su plausibilidad y compararía la respuesta con la verdad fundamental.

Luego daría una puntuación general en el conjunto de pruebas (promediada de todas las instancias de prueba en el conjunto de pruebas), que es nuestra estimación de qué tan fuerte es ese mensaje.

Luego, si deseamos probar un mensaje diferente o un LLM diferente, simplemente podemos volver a ejecutar el proceso de prueba. Eso generará otra puntuación, que indicará qué tan fuerte es esa combinación de LLM y motivación. Si probamos varias sugerencias (o varios LLM), podemos ver cuál funciona mejor simplemente tomando la que tiene la mejor puntuación general.

Es un proceso que tiene mucho sentido. Requiere que recopilemos una cantidad decente de datos de prueba, pero esto es necesario si queremos proporcionar algún tipo de evaluación de un mensaje en cualquier caso. Y requiere que escribamos una función que pueda, dada una entrada al LLM y la respuesta del LLM, calificar qué tan fuerte es la respuesta. Esto puede suponer un poco de trabajo en algunos casos (¡explicamos cómo hacerlo en el libro!), pero, una vez escrito, podemos evaluar cualquier número de respuestas a cualquier número de indicaciones. Y nos permite hacerlo de una manera consistente e imparcial.

Como se indicó, si el LLM devuelve una respuesta corta, como en un problema de clasificación, escribir la función será muy fácil. Y, como acabamos de ver, cuando el LLM devuelve una puntuación numérica, la función también puede ser bastante sencilla.

Si el LLM arroja una respuesta más larga, a menudo (aunque no siempre) usaremos un enfoque de LLM como juez, donde conseguimos que un LLM evalúe la respuesta de otro LLM. Esto no es perfecto, pero elimina los prejuicios humanos y puede automatizarse. Lo que hace posible probar muchas indicaciones candidatas y probar cada una de ellas en profundidad.

Entonces, DSPy esencialmente hace por usted lo que probablemente terminaría codificando usted mismo si diera un paso atrás y pensara en cómo podría automatizar este proceso: cómo podría automatizar la búsqueda de un mensaje potente. Al menos, probablemente terminarías codificando esto tú mismo si tuvieras una enorme cantidad de tiempo libre y fueras la única persona en el mundo resolviendo este problema: el problema de tener que elaborar y evaluar muchas indicaciones de candidatos para cada tarea basada en LLM. Sin embargo, dado que muchos de nosotros enfrentamos los mismos desafíos, tener herramientas que se encarguen del trabajo repetitivo por nosotros es, al menos en retrospectiva, muy natural.

Qué hace DSPy por ti

DSPy hace por usted gran parte del trabajo que necesitaría hacer manualmente si adopta un enfoque de ingeniería rápido. Hace al menos tres cosas principales (en realidad, hace un poco más, pero en este artículo solo veremos las que probablemente sean las más importantes).

Genera automáticamente un mensaje para usted. Simplemente necesita proporcionar una breve descripción general de alto nivel de la tarea, que puede proporcionarse en una cadena (o en otros formatos, pero las cadenas son las más simples). En este ejemplo, podemos especificar: “documento -> evaluación_de_plausibilidad”. Otro ejemplo puede ser: “journal_article -> resumen, crítica”, que indica que el LLM debe tomar un artículo de revista y devolver un resumen del mismo y una crítica. DSPy también nos permite proporcionar más información sobre la tarea, pero generalmente podemos mantenerla en un nivel bastante alto. Evalúa automáticamente el mensaje por usted. Debe proporcionar los datos de prueba y una función de Python para evaluar cada respuesta, pero dado eso, DSPy le permite evaluar de manera completa y consistente cada mensaje (y cada LLM) que intente. Optimiza automáticamente el mensaje para usted. Este es posiblemente el elemento más poderoso de DSPy. Describiré esto a continuación.

Optimización de sus indicaciones

Para optimizar sus indicaciones, DSPy esencialmente entra en un bucle similar al siguiente (esto está un poco simplificado; lo describimos completamente en el libro, pero esto da una idea general):

best_prompt = "" bucle genera un nuevo mensaje de candidato evalúa este mensaje de candidato si este es el mejor mensaje hasta el momento: best_prompt = mensaje actual

Esto se repite durante el tiempo que usted indique (cuanto más tiempo busque mejores indicaciones, más fuertes tenderá a encontrar, aunque, por supuesto, hay rendimientos decrecientes). A medida que se repite, genera nuevas indicaciones de candidatos. Para hacer esto, DSPy utiliza una técnica llamada metaindicación, donde se usa un LLM para generar el mensaje utilizado para otro LLM. Para cada mensaje de candidato generado, DSPy lo evalúa.

Con indicaciones más débiles, DSPy puede en realidad utilizar la detención anticipada para lograr eficiencia y, por lo tanto, puede abandonar la evaluación antes de tiempo para cualquier indicación que parezca funcionar mal en relación con las indicaciones candidatas probadas previamente. Es decir, si genera mensajes que funcionan mal en una parte de los datos de prueba, no es necesario probar estos mensajes en el conjunto de prueba completo. Sin embargo, evaluará completamente las indicaciones más prometedoras y, por lo tanto, podrá identificar con confianza las indicaciones más sólidas que se probaron.

DSPy incluye varios procesos diferentes para generar los mensajes. Los más eficaces aprenden sobre la marcha. A medida que se evalúa cada mensaje candidato, DSPy puede aprender dónde funciona bien y dónde funciona mal (puede ver qué casos de prueba funcionan bien y mal, pero DSPy también puede ver por qué cada mensaje funciona bien en algunos casos y mal en otros). Luego puede aprovechar esto para sugerir cada vez más sugerencias de candidatos prometedores, por lo que las indicaciones tienden a funcionar cada vez mejor a medida que avanza el proceso.

Después de ejecutar DSPy

Una vez que haya ejecutado DSPy, recibirá un mensaje para su tarea y también tendrá una estimación de qué tan bien funcionará en producción, en función de qué tan bien se comporta en sus datos de prueba. (Al igual que con el aprendizaje automático, generalmente dividimos los datos que tenemos en datos de entrenamiento, validación y prueba, por lo que idealmente tendremos un conjunto de reserva usado solo para una evaluación final).

Eso puede proporcionar una buena base para decidir si es lo suficientemente fuerte como para ponerlo en producción o no. De lo contrario, puede dedicar más tiempo a optimizar el mensaje. O puede buscar otro LLM: una vez que su código esté configurado, evaluar otro LLM solo requiere especificar el LLM y volver a ejecutar el código. Tendrá que pagar por las llamadas de LLM (a menos que utilice un LLM alojado), pero probablemente no tendrá trabajo adicional que hacer.

Código de muestra

La mayoría de las veces, el código que necesitarás escribir para usar DSPy será bastante corto y simple. Incluiré un ejemplo aquí, aunque no lo explicaré completamente (con suerte, lo haré en artículos futuros). Sin embargo, esto debería darle una idea general de lo que implica trabajar con DSPy. Requiere una instalación de pip y algunas importaciones. Una vez que tengas eso, todo es bastante sencillo.

import dspy OPENAI_API_KEY = [indique su clave API] lm = dspy.LM("openai/gpt-4o-mini", api_key=OPENAI_API_KEY) dspy.settings.configure(lm=lm) predictor = dspy.Predict("pregunta, contexto -> respuesta, confianza") predicción = predictor(question="¿Cuál es la capital de Francia?", contexto="") imprimir(predicción.respuesta, predicción.confianza)

Este código no incluye ninguna optimización o evaluación (simplemente generará un mensaje y manejará la interacción con el LLM), pero muestra un programa DSPy completamente funcional. Primero importa dspy, luego especifica el LLM que se utilizará y la clave API para ello. En este ejemplo, se utiliza un modelo OpenAI, pero DSPy admite docenas de proveedores diferentes. Luego especifica a alto nivel la tarea: dada una pregunta y algo de contexto, el LLM debe devolver la respuesta y la confianza para esa respuesta. Luego hace una pregunta específica (en este ejemplo, "¿Cuál es la capital de Francia?", sin ningún contexto adicional) y muestra la respuesta. Al probar esto, recibimos consistentemente:

París, alto

Esto indica que la respuesta es París y que el LLM tiene mucha confianza en la respuesta.

Tras un poco de evaluación y optimización, el código será un poco más largo, pero no gigantesco. Este ejemplo muestra una tarea muy simple, pero con tareas más difíciles, la evaluación y optimización normalmente serán importantes. Hacer todo esto es bastante manejable, ya que DSPy mantiene la mayor parte de la complejidad bajo el capó.

Conclusiones

DSPy no puede garantizar un aviso extremadamente eficaz para cada tarea en cada LLM. Pero le ahorra mucho trabajo y tenderá a funcionar tan bien o mejor que un ingeniero profesional. En futuros artículos, espero cubrir algunos experimentos que enfrentan a DSPy con la ingeniería manual, pero en pocas palabras, DSPy ha salido adelante de manera consistente hasta ahora. Para cualquier aplicación basada en LLM que creemos, generalmente vale la pena usar DSPy para crear y evaluar las indicaciones. No lleva mucho tiempo aprender el marco y, una vez que lo haces, estarás listo para cualquier proyecto en el que trabajes.

De manera realista, no siempre usaré DSPy en contextos donde no necesito un mensaje fuerte, o donde la tarea es tan simple para un LLM que cualquier mensaje básico servirá. Pero cada vez que me encuentro en una situación en la que parece que necesito hacer algo de ingeniería rápidamente, uso DSPy para automatizar todo ese trabajo por mí. En lugar de crear y probar manualmente cada mensaje de candidato, puedo configurar un código DSPy y dejar que haga el trabajo. Es como tener mi propio asistente de ingeniería rápido.

La ejecución puede tardar algún tiempo. A menudo lo dejo funcionar durante 20 o 30 minutos o más para obtener un buen mensaje. Pero es él quien hace el trabajo, no yo. Una cosa a tener en cuenta son los costos de LLM, aunque DSPy le permite monitorearlos. En la mayoría de los casos, tener indicaciones de mayor calidad es más barato a largo plazo, aunque en algunos casos eso no será cierto, y deberíamos limitar el tiempo que DSPy dedica a intentar generar indicaciones más potentes.

Esto es bastante fácil de hacer: sólo debemos tener cuidado de especificar que se debe dedicar una cantidad de tiempo razonable a buscar el mejor mensaje que podamos encontrar. Podemos, por ejemplo, especificar probar solo una pequeña cantidad de indicaciones candidatas y elegir la más fuerte. En otros casos, puede valer la pena dejar que pruebe muchas indicaciones candidatas.

Con suerte, publicaré más artículos que expliquen DSPy en el futuro.