Mittlerweile gibt es viele Anwendungen, in denen man mit einem Large Language Modell chatten kann, um sich etwa Texte zusammenfassen oder Fragen zu Daten beantworten zu lassen. Doch dies ist nur eine Facette bei der Nutzung von KI-Modellen. Ehrlich gesagt, ist meiner Meinung nach die Generierung von textuellen Prosa-Antworten nur für ein sehr begrenztes Anwendungsgebiet nutzbar. In Enterprise-Anwendungen arbeiten wir mit komplexen, strukturierten Daten wie etwa Datentabellen, JSON-Dateien oder Klassen und Objekten. In diesem Blogbeitrag möchte ich zeigen, wie (lokale) Large Language Modelle in Anwendungen aufgerufen werden können, so dass sie Antworten liefern, die einer definierten Datenstruktur entsprechen.
Ein lokales KI-Modell einbinden
Es gibt mehrere Gründe, warum ich mich für die lokale Ausführung eines Modells in diesem Blogbeitrag entschieden habe:
- Die Daten verlassen nicht das eigene Netzwerk: Bei Unternehmensdaten ist Vertraulichkeit Voraussetzung, d. h. die Daten müssen vor unbefugtem Zugriff durch Dritte geschützt werden.
- Es findet kein Training auf den Daten statt. Cloud-Provider nutzen die Ein- und Ausgaben bei Nutzung der API oft zum Trainieren ihrer eigenen Modelle. Es gibt zwar meist Lizenzen, wo dies nicht der Fall ist, aber diese Komplexität möchte ich an dieser Stelle explizit vermeiden. Meine Kolleg:innen beraten Sie hierzu aber gerne.
- Es ist kein API-Key erforderlich. Meine Hoffnung ist, dass so die Beispiele einfacher nachvollzogen werden können.
Was meine ich, wenn ich von einem lokalen KI-Modell spreche? Im Kontext dieses Blogposts ist ein lokales KI-Modell ein Modell, welches auf eigene Hardware heruntergeladen wird und dort mit Hilfe spezieller Software ausgeführt wird. Eigene Hardware kann in diesem Fall mein eigener Entwickler-Laptop oder eben ein Server in irgendeinem Rechenzentrum sein. Die Ausführung findet also immer backendseitig statt. Die Ausführung eines Modells im Browser ist nicht Teil des Scopes.
In einem vorherigen Blogbeitrag habe ich gezeigt, wie sich lokale Modelle mit LM Studio betreiben lassen. In diesem Beitrag möchte ich ein weiteres Tool vorstellen: Ollama ist ein Open-Source-Projekt, mit dem lokale Modelle betrieben werden können. Außerdem bietet es eine gute TypeScript-Bibliothek.
Nach der Installation von Ollama kann über die GUI oder das Terminal mit dem Befehl ollama pull <Modellname> ein Modell heruntergeladen werden. Für dieses Beispiel werde ich das Open Weight Modell von OpenAI, gpt-oss:20b, nutzen. Um Modelle mit Ollama programmatisch mit TypeScript zu verwenden, bietet Ollama die Bibliothek ollama an. Diese kann über die Npm-Registry installiert werden.
import ollama from 'ollama';
const response = await ollama.chat({
model: 'gpt-oss:latest',
messages: [{ role: 'user', content: 'Was ist die Hauptstadt von Deutschland?' }],
})
console.log(response.message.content)
Im obenstehenden Beispiel frage ich das LLM, was die Hauptstadt von Deutschland ist. Das 20 Milliarden Parametermodell ist bei Ollama mit dem Tag latest versehen, deswegen kann ich es unter diesem Namen referenzieren. In unserem Beispiel verhalten wir uns wie ein Nutzende, welche mit dem Modell über eine grafische Benutzeroberfläche wie OpenWebUI chatten. In meinem Fall antwortet das Modell mit „Die Hauptstadt von Deutschland ist Berlin.“. Diese Antwort ist richtig, aber Prosa. Es könnte sein, dass das Modell bei der nächsten Ausführung mit einer anderen oder anders formulierten Antwort aufwartet. Dieses Verhalten ist für unser Programm ungeeignet. Wir brauchen strukturierten Output, auf den wir zugreifen können. In Webanwendungen ist die häufigste Form von strukturierten Daten wahrscheinlich JSON. Was also, wenn wir unserem Modellaufruf ein strukturiertes Schema mitgeben könnten?
Strukturierte Outputs mit zod
Nachdem wir unser KI-Modell heruntergeladen und per TypeScript aufgerufen haben, wollen wir uns nun ansehen, wie wir strukturierte Ausgaben mit Hilfe von zod erzeugen können. Zod ist eine Open Source TypeScript-Bibliothek, mit der sich Objektstrukturen und die dazugehörigen Validierungen implementieren lassen. Man nennt dies auch Schema Validierung. Um ein Zod-Schema zu erstellen, muss man das z importieren und ein passendes Objekt definieren:
import { z } from 'zod';
const mySchema = z
.object({
name: z.string().min(4),
});
Dieses Schema besteht nur aus dem Feld name. Standardmäßig sind alle Felder in einem Schema erforderlich, d. h. sie können nicht weggelassen werden. Im Beispiel wird name als String definiert, der mindestens aus drei Buchstaben bestehen muss. Namen, die kürzer als drei Buchstaben sind, entsprechen also nicht dem Schema. Die Einschränkung von Länge und Typ ist hier nur beispielhaft. Selbstverständlich können mit zod jede Menge weiterer Validierungen und Typen genutzt werden.
Wie können wir unser Schema jetzt mit Ollama nutzen? Text-basierte Large Language Modelle wie gpt-oss können gut mit Text umgehen. Wir müssen unser Schema also in ein maschinenlesbares Format übertragen. Dieses Format heißt JSON Schema. JSON Schema ist eine Spezifikation mit der sich JSON beschreiben lässt: Felder, Formate, Validierungen usw. Ein JSON-Schema ist selbst nur valides JSON.
Seit zod 4 kann man ein Zod-Schema via Zod-API direkt in ein JSON Schema umwandeln (vor zod 4 war hierfür ein eigenes Package notwendig) mit der Funktion z.toJSONSchema().Wenn wir diese Funktion mit unserem Schema aufrufen, bekommen wir folgendes JSON Schema zurück:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { name: { type: "string", minLength: 4 } },
required: [ "name" ],
additionalProperties: false
}
Das JSON Schema können wir dann im Parameter format an Ollama übergeben. Damit stellen wir sicher, dass der Output unseres LLM-Aufrufs exakt unserem Format entspricht. Kann das LLM keine passende Struktur zurückliefern, so ist die Antwort automatisch leer. Viele aktuelle LLMs unterstützen JSON Schema, um die erwartete Ausgabe eines LLMs zu beschreiben. So bietet etwa die API von OpenAI eine Möglichkeit ein JSON Schema mitzugeben. Mit dem Npm-Package kann sogar direkt ein Zod-Schema verwendet werden.
Die Antwort, die wir von Ollama bekommen, ist allerdings immer noch vom Typ String. Wenn wir uns diesen genauer ansehen, stellen wir fest, dass der Inhalt allerdings JSON ist, welches unserem Schema entspricht. Wir müssen die Ausgabe also parsen per JSON.parse(). Damit erhalten wir ein JavaScript-Objekt, aus dem wir jetzt den Namen der Hauptstadt von Deutschland zuverlässig auslesen können.
Das Konzept der LLM-Ausgabe, die exakt einem vorgegebenen Format entspricht, wird auch als Structured Output bezeichnet. Es ist ein echter Gamechanger: Wir können jetzt LLMs in unseren Anwendungen nutzen und bekommen Ausgaben, die wir einfach in unsere Datenstrukturen konvertieren können.
Ein „komplexeres“ Schema
In der Regel bestehen unsere Datenstrukturen nicht nur aus einem Feld name. Als Beispiel erweitere ich unser Schema um das Feld kfzKennzeichen, welches das Kennzeichen der Hauptstadt angeben soll. Weiterhin können wir mit .describe() Felder oder unser komplettes Schema beschreiben. Diese Beschreibungen werden auch ins JSON Schema übertragen. Sie sind wichtig, denn sie können dem LLM weiteren Kontext für seine Antworten geben. Interessanterweise reicht gpt-oss der Feldname kfzKennzeichen aus, um auf den gewünschten Inhalt zu schließen. Unseren Prompt an das LLM „Was ist die Hauptstadt von Deutschland?“ müssen wir gar nicht anpassen, eine Anpassung unseres Schemas genügt. Das untenstehende Code-Beispiel zeigt das neue Schema, den Aufruf von Ollama und die Konvertierung in ein Objekt:
import ollama from 'ollama';
import {z} from 'zod';
const mySchema = z
.object({
name: z.string().min(4),
kfzKennzeichen: z.string(),
})
.describe("Beschreibt die Hauptstadt eines Landes");
const jsonSchema = z.toJSONSchema(mySchema);
console.log(jsonSchema);
const response = await ollama.chat({
model: 'gpt-oss:latest',
messages: [{role: 'user', content: 'Was ist die Hauptstadt von Deutschland?'}],
format: jsonSchema
});
const answer = JSON.parse(response.message.content);
console.log(answer);
Et voilà: gpt-oss antwortet immer noch mit Berlin, zusätzlich allerdings auch mit richtigen Kennzeichen, B. Das Ganze funktioniert natürlich auch mit dem Prompt „Was ist die Hauptstadt von Österreich?“. Das Modell antwortet korrekt mit Wien und W.
Datenextraktion aus eigenen Daten mit LLMs
In den letzten beiden Abschnitten haben wir uns angesehen, wie das LLM uns Fragen strukturiert beantwortet. Das LLM antwortete aus „seinem Wissen“, welches es während des Modell-Trainings gelernt hat. Ich möchte ehrlich sein: Das Beispiel ist konstruiert. Fragen nach Gründungsjahr oder Einwohnerzahl einer Hauptstadt führen bereits zu inkorrekten Antworten. Dafür gibt es mehrere Gründe: Das Modell hat eine geringe Anzahl an Parametern (im Vergleich zu Frontier-Modellen wie ChatGPT) und die Trainingsdaten zu Einwohnerzahl und Gründungsjahr sind wahrscheinlich entweder nicht aktuell und / oder umfangreich. Kleine Modelle lassen sich jedoch hervorragend zur Datenextraktion nutzen. Nehmen wir an, wir haben einen klassischen Geschäftsbrief mit Kundennummer, Datum und Vorgangsnummer. Als Menschen erkennen wir sofort diese Daten schnell. Mit zod und Structured Outputs können wir das LLM anweisen, uns diese Daten zu extrahieren. Diesen Prozess nennt man Entity Extraction.
Mit zod können wir ein Schema für unsere relevanten Daten definieren. In meinem Beispiel-Schema habe ich zusätzlich noch die Empfänger-Daten mitaufgenommen. Jetzt nehmen wir noch unseren Ausgangsbrief mit in den Prompt auf und schon kann unser Modell die Daten extrahieren. Unten folgt das komplette Code-Beispiel:
import ollama from 'ollama';
import {z} from 'zod';
const mail = `
Beispiel AG
Frau Erika Beispiel
Beispielweg 34
67890 Beispielstadt
15. Februar 2025
Kundennummer: K102938
Vorgangsnummer: 2025-0457
Sehr geehrte Frau Beispiel,
bezugnehmend auf Ihre Anfrage vom 12. Februar 2025 teilen wir Ihnen mit, dass der von Ihnen bestellte Artikel
voraussichtlich in der kommenden Woche versendet wird. Sobald der Versand erfolgt ist,
erhalten Sie eine separate Benachrichtigung inklusive Sendungsverfolgung.
Für Rückfragen stehen wir Ihnen jederzeit gerne zur Verfügung.
Mit freundlichen Grüßen
Max Mustermann
`;
const briefSchema = z
.object({
kundennummer: z.string().describe("Eine Kundennummer gehört zu genau einem Kunden"),
vorgangsnummer: z.string().describe("Eine Vorgangsnummer bezieht sich auf einen Schriftverkehr"),
datum: z.string().describe("Datum des Briefs"),
empfaenger: z.object({
firma: z.string(),
person: z.string().describe("Der komplette Name einer Person ohne Anrede"),
strasseHausNr: z.string(),
plzStadt: z.string(),
}).describe("Empfänger des Briefs. Steht in der Regel oben links"),
})
.describe("Beschreibt einen Brief");
const jsonSchema = z.toJSONSchema(briefSchema);
console.log(jsonSchema);
const response = await ollama.chat({
model: "gpt-oss:latest",
messages: [
{role: "system", content: "Du bist gut darin Briefe zu sortieren. "},
{
role: "user", content: `Extrahiere die Daten aus folgendem Brief:
${mail}`
}],
format: jsonSchema
});
const answer = JSON.parse(response.message.content);
console.log(answer);
Das Modell antwortet mit folgender Datenstruktur:
{
kundennummer: "K102938",
vorgangsnummer: "2025-0457",
datum: "15. Februar 2025",
empfaenger: {
firma: "Beispiel AG",
person: "Frau Erika Beispiel",
strasseHausNr: "Beispielweg 34",
plzStadt: "67890 Beispielstadt"
}
}
All unsere gewünschten Daten werden zuverlässig extrahiert. Selbst unser verschachteltes Empfängerobjekt ist kein Problem.
Fazit und Ausblick
Wir haben mit TypeScript und Ollama ein lokales Modell heruntergeladen und eingebunden. Weiterhin haben wir mit zod und JSON Schema eine Möglichkeit gefunden, strukturierte Ausgaben von LLMs zu verlangen. Damit haben wir sowohl das Wissen des Modells abgefragt als auch per Entity Extraction Informationen aus unseren eigenen Daten extrahiert. Die Kombination aus lokaler Ausführung, welche volle Kontrolle über die eigenen Daten bietet, und den strukturierten Ausgaben des LLMs, welche im Programmverlauf weiterverarbeitet werden können, erschließt diverse neue Einsatzgebiete von Large Language Modellen. Außerdem ist TypeScript eine sehr verbreitete und beliebte Programmiersprache mit einem großen Ökosystem, so dass sich LLMs einfach in bestehende Projekte integrieren lassen. Ich bin überzeugt, die Zukunft der LLM-Integration in Business-Anwendungen ist nicht nur das Chatten mit KI-Modellen, sondern auch die Verarbeitung von Geschäftsdaten, um diese Ergebnisse dann strukturiert in Geschäftsprozessen weiterzuverwenden.