Strukturierte Antworten lokaler Modelle begrenzen

Strukturierte Antworten lokaler Modelle begrenzen

- Matt

In WhipRadio, meinem Hobbyprojekt für einen Radiosender, bei dem Musik, Moderation und Sendeplan aus lokalen Modellen kommen, plant ein Modell über llama.cpp jeden Song als JSON: Titel, Stil, Songtext und eine kurze Geschichte dazu. Das Schema wird per JSON Schema vorgegeben, llama.cpp übersetzt es in eine Grammatik und erzwingt beim Erzeugen gültiges JSON. Das klang nach einer sicheren Sache. Bis ich mir die Abbrüche angesehen habe.

Was schiefging

Von 126 Songplänen an einem Tag endeten 68 mit finish_reason: length. Das Muster war immer gleich: Das Modell dachte erst ausgiebig nach, dann wiederholte sich ein freies Textfeld, meist die Geschichte, bis das Token-Limit erreicht war. Das JSON war damit kaputt, der Aufruf wurde wiederholt, und jeder Plan kostete zwei bis vier Minuten statt einer halben.

Noch schlimmer war ein einzelner Aufruf ganz ohne Limit. Der lief 24 Minuten und knapp 40.000 Token lang und blockierte die Grafikkarte für alles andere.

Drei Grenzen, die jetzt immer gelten

Jedes Textfeld bekommt ein maxLength. Die Grammatik setzt das beim Dekodieren durch, das Modell kann gar nicht länger schreiben:

{
  "type": "object",
  "properties": {
    "title": { "type": "string", "maxLength": 120 },
    "story": { "type": "string", "maxLength": 1500 }
  },
  "required": ["title", "story"]
}

Wichtig: Das ist ein harter Schnitt, kein Hinweis. Ein Test mit 200 Zeichen bei einer Bitte um „1500 Wörter“ endete exakt nach 200 Zeichen, mitten im Satz. Die Grenzen müssen also großzügig sein. Ich habe sie aus den bisher gespeicherten Antworten abgeleitet, mit Luft über dem 90. Perzentil.

Jede Anfrage bekommt max_tokens. Nicht als Konvention für die Aufrufer, sondern zentral in der Schicht, die alle Anfragen verschickt. Wer kein Limit setzt, bekommt eins.

Das Nachdenken bekommt ein eigenes Budget. Bei Modellen mit Reasoning zählen die Denk-Token in llama.cpp gegen max_tokens. Ohne eigenes Budget dachte jede Aufgabe gleich lang, bei mir im Median rund 3.500 Token, egal ob schwer oder trivial. Mit 1.000 Token waren die Ergebnisse vergleichbar, nur deutlich schneller. Das Limit für die Antwort ist jetzt das Budget für den Inhalt plus das Denkbudget plus etwas Puffer.

Der Haken

maxLength schneidet ab, statt zu kürzen. Ein abgeschnittener Text ist gültiges JSON, aber inhaltlich vielleicht unbrauchbar, und das merkt keine Validierung. Für Felder, bei denen ein halber Satz schlimmer ist als ein Fehler, prüfe ich zusätzlich, ob der Text das Limit erreicht hat.

Welche Einstellungen bei llama-server sonst wichtig sind, hatte ich beim Umstieg von Ollama beschrieben. Was die Grammatik aus JSON Schema unterstützt, steht im llama.cpp-Repository.