MWCodebymw.de ↗
JavaScript

Verlässliches JSON aus einem LLM – mit Structured Outputs statt Hoffnung

Antworte nur mit JSON" reicht nicht – irgendwann kommt doch ein "Klar, hier ist dein JSON:" davor und dein JSON.parse() stürzt ab. Mit einem JSON-Schema erzwingst du garantiert gültige Struktur.

Wer ein LLM programmatisch nutzt, will die Antwort meist als Datenstruktur, nicht als Fließtext – z. B. { "kategorie": "...", "dringlichkeit": 3 }. Der naive Weg ist, im Prompt „Antworte nur mit JSON" zu schreiben und das Ergebnis durch JSON.parse() zu jagen. Das geht so lange gut, bis das Modell doch ein „Klar, hier ist dein JSON:" davorsetzt oder ein Feld weglässt – und deine App abstürzt. Die saubere Lösung heißt Structured Outputs: Du gibst ein JSON-Schema vor, und die API garantiert eine dazu passende Antwort.

Ein Schema erzwingen (OpenAI)

// Läuft auf dem Server – der API-Key gehört nicht ins Frontend
async function classify(text) {
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: `Klassifiziere: ${text}` }],
      response_format: {
        type: 'json_schema',
        json_schema: {
          name: 'ticket',
          strict: true,                       // erzwingt exakt dieses Schema
          schema: {
            type: 'object',
            properties: {
              kategorie:    { type: 'string', enum: ['bug', 'frage', 'lob'] },
              dringlichkeit: { type: 'integer' },
            },
            required: ['kategorie', 'dringlichkeit'],
            additionalProperties: false,      // keine Extra-Felder
          },
        },
      },
    }),
  })

  const data = await res.json()
  // Mit strict:true ist das immer gültiges, schema-konformes JSON
  return JSON.parse(data.choices[0].message.content)
}

const t = await classify('Der Login hängt seit dem Update')
console.log(t.kategorie, t.dringlichkeit)   // z. B. "bug" 3

Warum das den Unterschied macht

  • strict: true + additionalProperties: false garantieren: keine Vorrede, keine fehlenden Pflichtfelder, keine erfundenen Zusatzfelder. JSON.parse() kann nicht mehr an unerwartetem Text scheitern.
  • enum grenzt Werte ein – so bekommst du nicht mal „Bug", „BUG" und „fehler" durcheinander, sondern genau deine drei erlaubten Kategorien.
  • Trotzdem defensiv bleiben: Bei einem Sicherheits-Refusal oder abgeschnittener Antwort (Token-Limit erreicht) kann das JSON ausbleiben. Ein try/catch um JSON.parse gehört weiterhin dazu.

Fast jeder große Anbieter kennt dieses Muster: OpenAI über response_format, Claude (Anthropic) über output_config.format bzw. Tool-Use, Gemini über responseSchema. Das Prinzip ist überall gleich – Struktur erzwingen statt erhoffen.

Du willst KI-Ausgaben zuverlässig in deine App-Logik einbinden? Sprich mich an.

Quellen

#KI#JavaScript#JSON#OpenAI#Structured Outputs

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →