Show HN: Outlines - herramienta para garantizar que los LLM generen salidas estructuradas como JSON válido
(github.com/normal-computing)- Outlines es una herramienta de salida estructurada para LLM que permite especificar el tipo de salida deseado durante la generación y garantiza que los datos coincidan exactamente con esa estructura
- En lugar de corregir salidas erróneas después de generar usando parsing, expresiones regulares o código frágil, fuerza directamente una estructura válida en la etapa de generación
- La forma de uso es
model(prompt, output_type), y permite definir salidas alineadas con el sistema de tipos de Python, comoLiteral["Yes", "No"],into un modelo de Pydantic - Los tipos de salida compatibles incluyen opciones predefinidas, Function Calls basadas en firmas de función, esquemas JSON/Pydantic, patrones de expresiones regulares y forzado de estructura basado en gramáticas
- La integración con modelos se divide en vLLM y Ollama con soporte de servidor, transformers y llama.cpp para modelos locales, y OpenAI, Gemini y Dottxt con soporte de API
- Los flujos de trabajo de ejemplo incluyen convertir correos de clientes en tickets de servicio, transformar descripciones de productos en datos de categorías, devolver información estructurada o
"I don't know"a partir de descripciones incompletas de eventos, clasificar documentos en categorías predefinidas y convertir solicitudes de reuniones en lenguaje natural en parámetros de función - Las plantillas de prompts admiten
outlines.Template.from_stringbasado en Jinja y carga desde archivos, para separar prompts complejos del código y reutilizarlos - La instalación se ofrece con
pip install outlines, y la API de.txtaparece actualmente en estado de acceso anticipado
1 comentarios
Opiniones de Hacker News
Esta biblioteca parece usar una idea simple en cuanto a mecanismo: enmascarar parte del espacio léxico y avanzar de forma eficiente en cada paso de tiempo, y eso es excelente.
Pero, desde mi experiencia usando bibliotecas que imponen una estructura de salida sobre un LLM base, me pregunto si un modelo base como Llama2 realmente funciona bien.
En mi experiencia, la respuesta es más bien “para nada”, y para que funcione de verdad hizo falta bastante ajuste por instrucciones orientado a un uso específico.
Además, enmascarar a posteriori el espacio de estados durante la generación en un modelo ajustado por instrucciones termina cambiando la distribución de generación, así que resulta contraintuitivo que eso también pueda perjudicar el ajuste por instrucciones.
Sobre el segundo punto, si el objetivo es, por ejemplo, hacer que el modelo genere solo JSON, eso se puede lograr al 100% restringiendo qué tokens de salida se pueden usar y cuáles no.
Como puedo usar la GPU en una Mac M2, estoy usando la versión MLC con el plugin https://github.com/simonw/llm-mlc.
En el paper “Guiding Language Models of Code with Global Context using Monitors”(https://arxiv.org/abs/2306.10763) se propuso Monitor Guided Decoding, que conecta el LLM con análisis estático para guiarlo a generar código con consistencia de tipos.
Sin ningún fine-tuning, al guiar la generación a nivel de tokens con análisis estático en puntos específicos, la calidad del código generado mejoró mucho tanto en compilabilidad como en coincidencia con la respuesta correcta. Incluso modelos muy pequeños (1.1B) produjeron más código compilable que modelos mucho más grandes (175B), y también mejoraron la coincidencia con la respuesta correcta.
La principal diferencia es que la función generadora no era un LLM, sino una persona. No veo por qué no se podría quitar al humano del medio.
En el código tradicional, los casos límite eran literalmente pequeños casos especiales, pero con los LLM no puedes saber qué los hará salir disparados en una dirección absurda, y el código de parsing tiene que soportar ese caos.
Dicho de otro modo, siento que la proporción de casos que deben tratarse como casos límite aumentó drásticamente.
Con GPT-4, se puede lograr que devuelva JSON válido con solo poner ejemplos en el mensaje de sistema, y funciona nueve de cada diez veces.
Pero sigue siendo probabilístico, y nueve de cada diez no es suficiente.
A veces genera respuestas alucinadas como
{"key1": "value1", "key2": "value2" for i in range(n)}.Si se le vuelve a pedir con el mensaje de error de parsing, normalmente lo resuelve en el segundo intento.
Pero escapar comillas dobles y saltos de línea es menos confiable. Incluso dando varios ejemplos, solo los escapa bien alrededor de la mitad de las veces, y aunque se le vuelva a pedir por el error de escape, la tasa de éxito se queda cerca del 50%.
Ese prompt está aquí: https://github.com/microsoft/TypeChat/blob/c45460f4030938da3...
Creo que el enfoque basado en gramáticas que se ve aquí, o métodos como https://github.com/ggerganov/llama.cpp/pull/1773, son soluciones mucho más elegantes.
Entiendo que la gente quiera JSON, pero para mí se parece a intentar hacer nadar a un gato. Al final puede salir bien, pero no es su inclinación natural.
El error principal es no proporcionar una vía de escape. Como el LLM intenta encontrar la respuesta correcta, si le pides que tome textos y devuelva datos estructurados, pero alguno de los textos está vacío, le cuesta determinar la respuesta correcta y aparecen alucinaciones.
La solución es crear una vía de escape, por ejemplo poniendo como uno de los argumentos un booleano como
textIsMissing. Si tienes en cuenta este patrón de falla, funciona sin problemas.Lo reintenté varias veces e incluso le pedí correcciones posteriores, pero no pasó la validación, y nunca logró generar JSON 100% válido, así que terminé rindiéndome.
Primero, usa menos tokens porque no hace falta poner demasiados ejemplos en el prompt.
Segundo, se ve menos afectada por el problema del olvido.
Como ventaja menor, permite controlar con precisión dónde debe empezar la salida deseada. Aun así, en general lo veo como una buena función adicional, no como algo absolutamente esencial.
Una gran parte del poder de los LLM está en su distribución de probabilidad calibrada de las respuestas, y esta técnica probablemente parece descartar esa capacidad. Me pregunto por qué esto sería suficiente.
Como ejemplo simple, supongamos que las únicas salidas posibles de un LLM son “hello world”, “food”, “hello” y “good day”, y que, sin prompt, todas tienen la misma probabilidad. Supongamos que la gramática solo impone la restricción de que debe haber un espacio en alguna parte de la salida.
Si muestreamos salidas del LLM hasta que pasen la gramática, obtendremos “hello world” y “good day” con la misma probabilidad. Pero si aplicamos la técnica del sitio web, “hello world” saldrá el doble de veces que “good day”.
El problema central es que un prefijo de respuesta pudo haber tenido una probabilidad extremadamente baja de llevar a una respuesta válida, pero esta técnica, si se asume que tendrá éxito, construye una respuesta válida a partir de ese prefijo. Si hay suficiente independencia en los lugares adecuados podría estar bien, pero en los modelos autorregresivos los errores correlacionados se acumulan rápidamente.
Limitándonos a JSON: cuando un LLM produce una respuesta que no respeta el esquema, ¿también produce más —o menos— errores factuales, alucinaciones, cadenas truncadas, omisiones de personajes principales, etc.? Si la tasa de errores factuales está relacionada de forma no trivial con la tasa de errores de esquema, este camino es más peligroso de lo que parece. Viendo cómo ciertas palabras o frases pegadas pueden influir mucho en la salida de un LLM, parece muy probable que detalles como el cumplimiento del esquema también se propaguen a otras características de la salida.
Intenté pensar en un ejemplo donde “un prefijo de respuesta tenía una probabilidad muy baja de llevar a una respuesta válida, pero la técnica de algún modo construye una respuesta válida y causa problemas”, pero no se me ocurre fácilmente. Si hay un buen ejemplo, podría ser una pregunta de investigación interesante.
Relacionado con esto, LLama.cpp implementó el mes pasado muestreo basado en gramáticas.
https://news.ycombinator.com/item?id=36819906
https://github.com/ggerganov/llama.cpp/pull/1773
Nuestro método es mucho más eficiente. llama.cpp recorre todo el vocabulario, unos 50 mil tokens, en cada paso para crear la máscara.
Nosotros creamos un índice durante la inicialización, y en cada paso solo necesitamos hacer una búsqueda en un diccionario para construir la máscara. Es un intercambio de memoria por velocidad, y el muestreo es tan rápido como el muestreo estándar.
Dada la cantidad de papers relacionados, seguramente había varias más en otros lugares. El punto de esto y del trabajo actual es ofrecer una guía de muy bajo costo; para el caso de expresiones regulares llevaba un tiempo implementada y luego la extendimos a JSON.
Gracias por crear esto; el principio de funcionamiento es una idea tan obvia que sorprende que las plataformas de primera línea aún no lo hayan hecho.
Me pregunto cómo podría usarse, además de JSON, en otras tareas que requieren entradas estructuradas.
También podría ser resultado de recursos de desarrollo limitados. Sería sorprendente que una tecnología de 10 años careciera de una función básica como esta, pero en una tecnología de IA en plena carrera armamentista parece comprensible que todavía falten funciones de conveniencia.
No me queda claro en qué se diferencia esto de los siguientes proyectos:
https://github.com/1rgs/jsonformer
https://github.com/newhouseb/clownfish
https://github.com/mkuchnik/relm
https://github.com/ggerganov/llama.cpp/pull/1773
https://github.com/Shopify/torch-grammar
En general, hay muchos sistemas de guía basados en logits de este tipo, y la razón por la que no ganan mucha tracción es que los modelos de mayor rendimiento están detrás de APIs REST que no permiten este enfoque tan granular.
Como esos modelos son mucho mejores, la gente en general se conforma con volver a pedir una respuesta hasta que salga en el formato correcto. En GPT-4, en mi experiencia, ese tipo de falla además es bastante raro.
Nosotros recorremos el vocabulario una sola vez durante la inicialización para crear un índice, y luego la generación es tan rápida como la generación estándar.
Dicho de otra forma, ¿se actualiza una “máscara” de sesgo de logits cada vez que el LLM genera un token, para que el siguiente token solo pueda ser un token JSON válido? Muy genial
No estoy seguro de cómo este enfoque puede garantizar realmente el 100%
Edición: el paper explica esta parte con más cuidado
¿Este Brandon Willard es el breakdancer Brandon Willard de Detroit?
Edición: ¡sí! https://brandonwillard.github.io/
remilouf, como dijiste que tu experiencia es en “programación probabilística, relacional y simbólica”, supongo que entiendes que generar texto a partir de gramáticas regulares, gramáticas libres de contexto o, en la práctica, gramáticas de cualquier nivel, no es el problema
Por ejemplo, en Prolog, que es un lenguaje relacional, se puede hacer muy fácilmente con la notación de Definite Clause Grammars, siempre que se proporcione una gramática
A mi parecer, este enfoque requiere que el usuario proporcione una gramática. Entonces me pregunto cuál es la ventaja de usar un LLM para generar texto
¿Por qué no simplemente ejecutar la gramática como generador y producir el texto deseado? Así se podría reducir el gran esfuerzo y costo de entrenar un LLM desde el principio. Si solo se quiere generar texto estructurado, también me pregunto por qué se necesita un LLM, que es un modelo de lenguaje natural
La razón por la que se necesita un LLM, incluso si solo quieres texto estructurado, es para parsear texto no estructurado escrito por humanos y devolver datos estructurados que una máquina pueda usar
Si la solicitud es “¿cuáles son los primeros 10 dígitos de pi?” y restringiste la respuesta con la expresión regular
"[0-9]+\.[0-9]+", el objetivo es recibir la respuesta real, 3.1415926535, no una cadena aleatoria como “1.2346789” que simplemente coincida con el patrónInteresante; hace poco nosotros también hicimos una herramienta similar para restringir la salida de llama de modo que coincida con una interfaz de TypeScript[1]
Creo firmemente que las garantías de formato de salida serán importantes durante las próximas décadas, a medida que los LLM se usen para casos reales y no de juguete
[1] https://github.com/ggerganov/llama.cpp/discussions/2494