Show HN: Outlines – Ein Tool, das sicherstellt, dass LLMs gültiges JSON und andere strukturierte Ausgaben erzeugen
(github.com/normal-computing)- Outlines ist ein Tool für strukturierte Ausgaben von LLMs und stellt sicher, dass Daten exakt der gewünschten Struktur entsprechen, indem der gewünschte Ausgabetyp während der Generierung festgelegt wird
- Statt fehlerhafte Ausgaben nach der Generierung mit Parsing, regulären Ausdrücken oder fragilen Codekonstrukten zu korrigieren, erzwingt es gültige Strukturen direkt im Generierungsschritt
- Die Nutzung erfolgt in der Form
model(prompt, output_type)und erlaubt die Festlegung der Ausgabe passend zum Python-Typsystem, etwa mitLiteral["Yes", "No"],intoder einem Pydantic-Modell - Zu den unterstützten Ausgabetypen gehören vordefinierte Auswahloptionen, Function Calls auf Basis von Funktionssignaturen, JSON-/Pydantic-Schemas, reguläre Ausdrucksmuster und strukturgebende Einschränkungen auf Basis von Grammatiken
- Die Modellintegration ist unterteilt in serverunterstützte vLLM- und Ollama-Modelle, lokal unterstützte Modelle über transformers und llama.cpp sowie API-unterstützte OpenAI-, Gemini- und Dottxt-Modelle
- Beispiel-Workflows sind das Umwandeln von Kunden-E-Mails in Service-Tickets, das Überführen von Produktbeschreibungen in Kategoriedaten, das Extrahieren strukturierter Informationen aus unvollständigen Ereignisbeschreibungen oder die Rückgabe von
"I don't know", die Klassifizierung von Dokumenten in vordefinierte Kategorien sowie das Umwandeln natürlicher Besprechungsanfragen in Funktionsparameter - Prompt-Templates unterstützen das auf Jinja basierende
outlines.Template.from_stringsowie das Laden aus Dateien, um komplexe Prompts vom Code zu trennen und wiederzuverwenden - Die Installation erfolgt über
pip install outlines, und die.txt APIist derzeit als Early Access gekennzeichnet
1 Kommentare
Meinungen auf Hacker News
Diese Bibliothek scheint vom Mechanismus her die einfache Idee zu verwenden, Teile des Vokabularraums zu maskieren und effizient Schritt für Schritt über die Zeitschritte vorzugehen; das ist großartig.
Aus der Perspektive von jemandem, der Bibliotheken ausprobiert hat, die einem Basis-LLM eine Ausgabestruktur aufzwingen, frage ich mich allerdings, ob Basismodelle wie Llama2 wirklich gut funktionieren.
Meiner Erfahrung nach ist die Antwort eher „überhaupt nicht“, und damit es in der Praxis funktioniert, war ziemlich viel Instruction Tuning für den jeweiligen Anwendungsfall nötig.
Außerdem wirkt es kontraintuitiv, dass das nachträgliche Maskieren des Zustandsraums während der Generierung bei einem instruction-getunten Modell letztlich die Generierungsverteilung verändert und damit dem Instruction Tuning sogar schaden könnte.
Zum zweiten Punkt: Wenn das Ziel beispielsweise ist, das Modell dazu zu bringen, nur JSON zu erzeugen, dann ist das zu 100 % möglich, indem man einschränkt, welche Ausgabetokens verwendet werden dürfen und welche nicht.
Auf einem M2 Mac kann ich die GPU nutzen, daher verwende ich die MLC-Version als Plugin über https://github.com/simonw/llm-mlc.
Im Paper „Guiding Language Models of Code with Global Context using Monitors“ (https://arxiv.org/abs/2306.10763) wurde Monitor Guided Decoding vorgeschlagen, bei dem ein LLM mit statischer Analyse verbunden wird, um es zur Erzeugung typkonsistenten Codes anzuleiten.
Ohne jegliches Fine-Tuning verbesserte die Steuerung der Token-Level-Generierung durch statische Analyse an bestimmten Stellen die Qualität des generierten Codes deutlich, sowohl bei der Kompilierbarkeit als auch bei der Übereinstimmung mit der richtigen Antwort. Selbst sehr kleine Modelle (1,1B) erzeugten mehr kompilierbaren Code als viel größere Modelle (175B) und verbesserten zugleich die Übereinstimmung mit der richtigen Antwort.
Der Hauptunterschied ist nur, dass die Generierungsfunktion kein LLM war, sondern ein Mensch. Ich sehe keinen Grund, warum man den Menschen in der Mitte nicht herausnehmen sollte.
In klassischem Code waren Randbedingungen buchstäblich eher kleine Sonderfälle, aber bei LLMs weiß man nicht, was sie in eine völlig falsche Richtung abdriften lässt, und der Parsercode muss mit diesem Chaos zurechtkommen.
Anders gesagt: Der Anteil der Fälle, die man als Randbedingungen behandeln muss, scheint dramatisch gestiegen zu sein.
GPT-4 kann man allein mit Beispielen in der System Message dazu bringen, gültiges JSON zurückzugeben, und in neun von zehn Fällen funktioniert das.
Aber es bleibt probabilistisch, und neun von zehn Fällen reichen nicht aus.
Gelegentlich erzeugt es auch halluzinierte Antworten wie
{"key1": "value1", "key2": "value2" for i in range(n)}.Wenn man es mit der Parsing-Fehlermeldung erneut promptet, ist es beim zweiten Versuch normalerweise gelöst.
Das Escapen von doppelten Anführungszeichen und Zeilenumbrüchen ist allerdings weniger zuverlässig. Selbst mit mehreren Beispielen wird nur etwa die Hälfte korrekt escaped, und auch wenn man Escaping-Fehler erneut promptet, bleibt die Erfolgsrate bei ungefähr 50 %.
Der betreffende Prompt ist hier: https://github.com/microsoft/TypeChat/blob/c45460f4030938da3...
Ich halte den hier gezeigten grammatikbasierten Ansatz oder Verfahren wie https://github.com/ggerganov/llama.cpp/pull/1773 für eine deutlich elegantere Lösung.
Ich verstehe, dass Leute JSON wollen, aber für mich ist das ein bisschen so, als wollte man eine Katze zum Schwimmen bringen. Am Ende kann es gelingen, aber es entspricht nicht ihrer natürlichen Veranlagung.
Der wichtigste Fehler ist, keinen Ausweg anzubieten. Da ein LLM versucht, die richtige Antwort zu finden, wird es schwierig, die richtige Antwort zu bestimmen, wenn man Texte hineingibt und strukturierte Daten zurückverlangt, aber einer der Texte leer ist; dann entstehen Halluzinationen.
Die Lösung besteht darin, einen Ausweg zu schaffen, etwa indem man eines der Argumente als Boolean wie
textIsMissinganlegt. Wenn man solche Fehlermuster berücksichtigt, funktioniert es tadellos.Nach mehreren erneuten Versuchen und anschließenden Korrekturen bestand es die Validierung immer noch nicht, und weil es kein einziges Mal zu 100 % gültiges JSON erzeugte, habe ich schließlich aufgegeben.
Erstens muss man nicht so viele Beispiele in den Prompt packen und verbraucht dadurch weniger Tokens.
Zweitens ist sie weniger anfällig für das Vergessensproblem.
Ein kleinerer Vorteil ist, dass man genau steuern kann, wo die gewünschte Ausgabe beginnen soll. Insgesamt sehe ich es allerdings eher als nützliche Zusatzfunktion und nicht als etwas absolut Wesentliches.
Ein großer Teil der Stärke von LLMs liegt in der kalibrierten Wahrscheinlichkeitsverteilung ihrer Antworten; diese Technik scheint diese Fähigkeit vermutlich aufzugeben. Ich frage mich, warum das ausreichen soll.
Nehmen wir als einfaches Beispiel an, die möglichen Ausgaben eines LLM seien nur „hello world“, „food“, „hello“ und „good day“, und ohne Prompt hätten alle dieselbe Wahrscheinlichkeit. Angenommen, die Grammatik beschränkt sich darauf, dass irgendwo in der Ausgabe ein Leerzeichen vorkommen muss.
Wenn man LLM-Ausgaben sampelt, bis sie die Grammatik erfüllen, erhält man „hello world“ und „good day“ mit gleicher Wahrscheinlichkeit. Wendet man jedoch die auf der Website beschriebene Technik an, erscheint „hello world“ doppelt so häufig wie „good day“.
Das Kernproblem ist: Auch wenn ein bestimmtes Antwortpräfix nur mit extrem geringer Wahrscheinlichkeit zu einer gültigen Antwort geführt hätte, konstruiert diese Technik — unter der Annahme, dass sie erfolgreich ist — aus diesem Präfix eine gültige Antwort. Wenn die Dinge an den richtigen Stellen hinreichend unabhängig sind, mag das in Ordnung sein; bei autoregressiven Modellen kumulieren korrelierte Fehler jedoch schnell.
Auf JSON beschränkt gefragt: Erzeugt ein LLM, wenn es Antworten produziert, die das Schema nicht einhalten, eher mehr oder eher weniger sachliche Fehler, Halluzinationen, abgeschnittene Strings, fehlende Hauptfiguren usw.? Wenn die Rate sachlicher Fehler auf nichttriviale Weise mit der Schemafehlerrate zusammenhängt, ist dieser Weg riskanter, als er aussieht. Wenn man sieht, wie stark bestimmte Wörter oder zusammengeklebte Wortgruppen die Ausgabe eines LLM beeinflussen, wirkt es sehr wahrscheinlich, dass sich Details wie Schema-Konformität auch auf andere Eigenschaften der Ausgabe auswirken.
Ich habe versucht, mir ein Beispiel vorzustellen, bei dem „ein Antwortpräfix zwar nur mit sehr geringer Wahrscheinlichkeit zu einer gültigen Antwort geführt hätte, die Technik aber irgendwie eine gültige Antwort konstruiert und dadurch Probleme verursacht“, mir fällt aber keines wirklich ein. Wenn es ein gutes Beispiel gibt, wäre das eine interessante Forschungsfrage.
Passend dazu hat LLama.cpp letzten Monat grammatikbasiertes Sampling implementiert.
https://news.ycombinator.com/item?id=36819906
https://github.com/ggerganov/llama.cpp/pull/1773
Unser Verfahren ist deutlich effizienter. llama.cpp iteriert bei jedem Schritt über das gesamte Vokabular von rund 50.000 Tokens, um eine Maske zu erstellen.
Wir erstellen bei der Initialisierung einen Index; um bei jedem Schritt die Maske zu konstruieren, genügt anschließend ein Dictionary-Lookup. Das tauscht Geschwindigkeit gegen Speicher, und das Sampling ist so schnell wie Standard-Sampling.
Angesichts der vielen einschlägigen Paper dürfte es auch anderswo mehrere Ansätze gegeben haben. Der Punkt hier und bei der aktuellen Arbeit ist, Guidance mit sehr geringen Kosten bereitzustellen; für den Regex-Fall war das schon eine Weile implementiert und wurde später auf JSON erweitert.
Danke, dass ihr so etwas gebaut habt; die Funktionsweise ist eine so naheliegende Idee, dass es überrascht, dass die First-Party-Plattformen das noch nicht anbieten.
Neben JSON frage ich mich, wie sich das für andere Aufgaben nutzen lässt, die strukturierte Eingaben benötigen.
Es könnte auch einfach an begrenzten Entwicklerressourcen liegen. Bei einer zehn Jahre alten Technologie wäre es überraschend, wenn eine solche Grundfunktion fehlt; bei einer KI-Technologie mitten im Wettrüsten wirkt es nachvollziehbar, dass noch Komfortfunktionen fehlen.
Mir ist nicht ganz klar, wie sich das von den folgenden Projekten unterscheidet:
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
Insgesamt gibt es sehr viele solcher logitbasierten Guidance-Systeme, und der Grund, warum sie nicht viel Traktion bekommen, ist wohl, dass die leistungsstärksten Modelle hinter REST-APIs liegen, die solche feingranularen Ansätze nicht erlauben.
Diese Modelle sind so viel leistungsfähiger, dass die Leute im Allgemeinen damit zufrieden sind, erneut anzufragen, bis das richtige Format herauskommt. Bei GPT-4 sind solche Fehlschläge meiner Erfahrung nach ohnehin ziemlich selten.
Wir iterieren bei der Initialisierung einmal über das Vokabular, um einen Index zu erstellen; danach ist die Generierung so schnell wie Standard-Generierung.
Anders ausgedrückt: Wird bei jedem Token, das das LLM erzeugt, die Logit-Bias-„Maske“ so aktualisiert, dass das nächste Token nur ein gültiges JSON-Token sein kann? Sehr cool
Ich bin nicht sicher, wie diese Methode wirklich 100 % garantieren kann
Edit: Im Paper wird dieser Teil vorsichtiger erklärt
Ist dieser Brandon Willard der Breakdancer Brandon Willard aus Detroit?
Edit: Ja! https://brandonwillard.github.io/
remilouf, da du deinen Hintergrund als „probabilistisches, relationales und symbolisches Programmieren“ beschrieben hast, nehme ich an, dass du verstehst, dass das Erzeugen von Text aus regulären Grammatiken, kontextfreien Grammatiken oder praktisch jeder Grammatikstufe kein Problem ist
In einer relationalen Sprache wie Prolog geht das zum Beispiel sehr einfach, wenn man die Grammatik in der Notation der Definite Clause Grammars vorgibt
Aus meiner Sicht erfordert dieser Ansatz, dass der Nutzer die Grammatik bereitstellt. Dann frage ich mich, was der Vorteil ist, für die Texterzeugung ein LLM zu verwenden
Warum nicht einfach die Grammatik als Generator ausführen und den gewünschten Text erzeugen? Dann könnte man sich von vornherein den großen Aufwand und die Kosten sparen, ein LLM zu trainieren. Wenn man nur strukturierten Text erzeugen will, frage ich mich auch, warum man ein LLM als Natural-Language-Modell überhaupt braucht
Selbst wenn man nur strukturierten Text will, braucht man ein LLM, um von Menschen geschriebenen unstrukturierten Text zu parsen und maschinenverwendbare strukturierte Daten zurückzugeben
Wenn die Anfrage lautet: „Was sind die ersten 10 Ziffern von Pi?“ und die Antwort durch den regulären Ausdruck
"[0-9]+\.[0-9]+"eingeschränkt ist, dann will man nicht irgendeinen zufälligen String wie „1.2346789“, der nur dem Muster entspricht, sondern die tatsächliche Antwort 3.1415926535Interessant; wir haben vor Kurzem auch ein ähnliches Tool gebaut, das die Ausgabe von llama so einschränkt, dass sie zu einem TypeScript-Interface passt[1]
Ich bin fest davon überzeugt, dass Garantien für das Ausgabeformat in den kommenden Jahrzehnten wichtig werden, wenn LLMs für echte, nicht spielzeughafte Anwendungsfälle eingesetzt werden
[1] https://github.com/ggerganov/llama.cpp/discussions/2494