localaiforum
Anmelden
Guide › Arbeiten mit Modellen

Strukturierte Ausgabe & Function Calling

Bitten Sie ein Model um JSON, produziert es gern Fast-JSON: ein Komma zu viel, ein Key ohne Anführungszeichen, ein freundlicher Satz vor der ersten geschweiften Klammer. Freies Sampling kann immer ein ungültiges Token ausstoßen, weil der Sampler nichts über Ihr Schema weiß. Strukturierte Ausgabe schließt diese Lücke, und Function Calling ist derselbe Mechanismus mit anderem Hut.

Warum freies JSON bricht

Jedes generierte Token ist eine Stichprobe aus einer Verteilung. Bei jedem Schritt liegt etwas Wahrscheinlichkeitsmasse — klein, aber oberhalb von Temperatur 0 nie null — auf Tokens, die Ihr Format brechen würden: eine schließende Klammer vor ihrer öffnenden, ein Komma nach dem letzten Array-Element, ein kleingeschriebenes true, geschrieben True (eine Python-Angewohnheit, eingebacken in Trainingsdaten). Multiplizieren Sie „fast immer gültig“ über 200 Ausgabe-Tokens, und Sie bekommen intermittierende, nicht reproduzierbare Brüche — die schlimmste Art von Bug. Selbst Temperatur 0 macht die Ausgabe nur deterministisch, nicht schemagültig.

Die Lösungsleiter

Sprosse Mechanismus Garantie Wo
Nur Prompt („antworte mit gültigem JSON“) Freundlich bitten keine — Sampling kann trotzdem fehlgehen jede Engine
JSON-Modus angepromptet plus auf JSON-Syntax beschränkt syntaktisch gültiges JSON; Form nicht garantiert OpenAI-kompatible APIs, Ollama format: "json"
Schema-beschränkt Schema → Grammatik → Logits maskiert Ausgabe parst immer und matcht das Schema llama.cpp (json_schema / GBNF), vLLM guided_json, Ollama Structured Outputs, outlines / xgrammar

Jede Sprosse kostet Flexibilität und kauft Determinismus. Nutzen Sie die oberste Sprosse, wann immer der Konsument Code ist.

Grammatik-beschränkte Dekodierung

Kompilieren Sie das Schema in eine Grammatik. Bei jedem Dekodierungsschritt bekommt jedes Token, das ein gültiges Dokument nicht fortsetzen könnte, sein Logit vor dem Sampling auf minus Unendlich gesetzt. Ungültige Ausgabe ist nicht unwahrscheinlich — sie ist unmöglich. llama.cpp liefert GBNF-Grammatiken mit; eine Dezimalzahl-Grammatik ist eine Zeile:

root ::= "-"? [0-9]+ ("." [0-9]+)?

Dieselbe Idee umhüllt JSON Schema. Gegen einen lokalen llama.cpp-Server (OpenAI-kompatibler Endpunkt):

import json, requests

schema = {
    "type": "object",
    "properties": {
        "title":     {"type": "string"},
        "component": {"type": "string",
                      "enum": ["auth", "upload", "ui", "api"]},
        "severity":  {"type": "string",
                      "enum": ["low", "medium", "high"]}
    },
    "required": ["title", "component", "severity"],
    "additionalProperties": False
}

r = requests.post("http://localhost:8080/v1/chat/completions", json={
    "messages": [{"role": "user",
                  "content": "Extract the bug: " + report}],
    "response_format": {
        "type": "json_schema",
        "json_schema": {"name": "bug", "strict": True,
                        "schema": schema}
    },
    "temperature": 0
})
bug = json.loads(r.json()["choices"][0]["message"]["content"])

Ollama tut dasselbe mit einem JSON Schema im format-Feld; vLLM mit guided_json. Gleicher Mechanismus, drei Schreibweisen — die exakten Feldnamen Ihrer Engine leben in Engine-Regler.

Beschränkt heißt nicht korrekt

Die Grammatik garantiert Form, nicht Wahrheit. Das Model kann {"severity": "low"} für einen Absturzbericht ausgeben oder ein String-Feld mit Füllmaterial stopfen. Maskierung kann offenen Feldern auch schaden: Will das Model einen Absatzumbruch und die Grammatik verbietet den Zeilenumbruch, sinkt die Qualität. Praktische Regeln:

  • Enge Enums statt freier Strings, wann immer der Wertraum bekannt ist.
  • required für jedes Feld, das Sie konsumieren; additionalProperties: false, um Erfindungen zu stoppen.
  • Halten Sie String-Felder kurz; leiten Sie lange Prosa aus der beschränkten Antwort heraus.
  • Setzen Sie trotzdem ein durchgerechnetes Beispiel der gewünschten Ausgabe in den Prompt — es steuert den Inhalt, was die Grammatik nicht kann.

Function und Tool Calling

Tool Calling ist strukturierte Ausgabe plus eine Konvention: Das Model gibt zurück, welche Funktion es aufrufen will und mit welchen Argumenten; Ihr Code führt sie aus und hängt das Ergebnis an. Die OpenAI-tools-Form ist der De-facto-Dialekt, den lokale Engines nachahmen:

{
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "Current weather for a city",
      "parameters": {
        "type": "object",
        "properties": {
          "city": {"type": "string"},
          "unit": {"type": "string", "enum": ["C", "F"]}
        },
        "required": ["city"]
      }
    }
  }]
}

Das Model antwortet mit tool_calls in der Nachricht; Sie geben die Ausgabe als role: "tool"-Nachricht zurück, und die Schleife läuft weiter. Modelle sind auf spezifische Tool-Dialekte trainiert — Templates der Hermes- und Qwen-Linie sind auf lokalen Engines am besten unterstützt (llama.cpp mit --jinja, Ollama, vLLM mit automatischer Tool-Wahl). Ein Model ohne Tool-Training produziert plausible Aufrufe mit halluzinierten Argumentnamen, egal was Sie beschränken — wählen Sie also zuerst das Model für den Job: Modellauswahl.

Verifizieren Sie Parse und Inhalt

Zwei Checks nach jedem Aufruf. Erstens das Parsen — json.loads gelingt (die Grammatik garantiert das meist; der JSON-Modus nicht immer). Zweitens die Semantik: benötigte Keys vorhanden, Enums im Bereich, Zahlen plausibel, Strings nicht leer. Bei Fehlschlag einmal wiederholen, mit dem Validierungsfehler zurück in den Prompt gefüttert — beschränkte Wiederholungen sind billig und gelingen meist beim ersten Versuch.

Genau diese Schleife — einen Aufruf ausgeben, validieren, ausführen, beobachten — laufen Agenten den ganzen Tag: Agenten.

Verwandt: Agenten · Prompting · Sampling · RAG